How to Build a underwriting Agent Using LangChain in TypeScript for investment banking

By Cyprian AaronsUpdated 2026-04-21
underwritinglangchaintypescriptinvestment-banking

An underwriting agent in investment banking takes deal inputs, company financials, market data, and policy constraints, then helps analysts produce a first-pass credit or equity underwriting memo. The point is not to replace the banker; it is to compress repetitive analysis, standardize risk checks, and keep every recommendation traceable for compliance and audit.

Architecture

  • Input normalizer

    • Cleans deal packets from PDFs, spreadsheets, CRM notes, and market feeds.
    • Converts them into a structured underwriting request with issuer profile, instrument type, tenor, covenants, and key risks.
  • Policy and compliance layer

    • Enforces house rules: sector exclusions, leverage limits, KYC status, sanctions checks, and jurisdiction constraints.
    • Blocks model output when required inputs are missing.
  • Retrieval layer

    • Pulls from internal research notes, prior deals, covenant templates, credit policy docs, and approved market commentary.
    • Uses VectorStoreRetriever or a retriever built from a vector store for grounded responses.
  • Reasoning and memo generation

    • Uses a LangChain chat model to draft the underwriting summary, risk factors, questions for diligence, and recommendation.
    • Produces structured output so downstream systems can route it into an approval workflow.
  • Audit logger

    • Stores prompts, retrieved documents, model version, timestamps, and final recommendation.
    • This is mandatory if you want defensible decisions in a regulated environment.

Implementation

1) Install dependencies and define the underwriting schema

Use LangChain’s TypeScript packages plus a schema library like zod for strict outputs. In banking workflows, free-form text is not enough because you need deterministic fields for review and archiving.

npm install langchain @langchain/openai @langchain/community zod
import { z } from "zod";

export const UnderwritingRequestSchema = z.object({
  issuerName: z.string(),
  industry: z.string(),
  instrumentType: z.enum(["loan", "bond", "revolver", "equity"]),
  amountUsd: z.number(),
  tenorMonths: z.number(),
  leverageRatio: z.number().optional(),
  kycComplete: z.boolean(),
  sanctionsClear: z.boolean(),
});

export const UnderwritingOutputSchema = z.object({
  recommendation: z.enum(["approve", "approve_with_conditions", "reject"]),
  summary: z.string(),
  keyRisks: z.array(z.string()),
  diligenceQuestions: z.array(z.string()),
});

2) Build the retriever over approved internal content

For production underwriting agents, your retrieval corpus should be curated. Do not point the model at random shared drives; use approved research notes and policy documents only.

import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { Document } from "@langchain/core/documents";

const docs = [
  new Document({
    pageContent:
      "Credit policy: Max leverage ratio for industrial issuers is 4.5x unless CRO approval is granted.",
    metadata: { source: "credit-policy", docId: "CP-001" },
  }),
  new Document({
    pageContent:
      "Underwriting memo template requires downside case analysis, covenant package review, and liquidity runway assessment.",
    metadata: { source: "template", docId: "UT-014" },
  }),
];

const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" });
const vectorStore = await MemoryVectorStore.fromDocuments(docs, embeddings);
const retriever = vectorStore.asRetriever(3);

3) Compose the LangChain pipeline with structured output

This pattern uses ChatOpenAI, PromptTemplate, RunnableSequence, and withStructuredOutput. That gives you grounded generation plus machine-readable results.

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
import { UnderwritingRequestSchema, UnderwritingOutputSchema } from "./schemas";

const llm = new ChatOpenAI({
  model: "gpt-4o-mini",
  temperature: 0,
});

const prompt = PromptTemplate.fromTemplate(`
You are an investment banking underwriting assistant.
Use only the provided context and request data.
If KYC or sanctions are not clear, recommend reject.

Request:
{request}

Context:
{context}

Return:
- recommendation
- summary
- keyRisks
- diligenceQuestions
`);

const structuredModel = llm.withStructuredOutput(UnderwritingOutputSchema);

const chain = RunnableSequence.from([
  async (input: unknown) => {
    const request = UnderwritingRequestSchema.parse(input);
    const contextDocs = await retriever.getRelevantDocuments(
      `${request.issuerName} ${request.industry} ${request.instrumentType}`
    );
    return {
      request,
      context: contextDocs.map((d) => d.pageContent).join("\n\n"),
    };
  },
  async ({ request, context }) => {
    const formattedPrompt = await prompt.format({
      request: JSON.stringify(request),
      context,
    });
    return structuredModel.invoke(formattedPrompt);
  },
]);

const result = await chain.invoke({
  issuerName: "Northstar Industrial Holdings",
  industry: "Manufacturing",
  instrumentType: "bond",
  amountUsd: 250000000,
  tenorMonths: 60,
});
console.log(result);

4) Add hard guardrails before the model runs

Banking agents need pre-checks that stop bad requests early. If KYC or sanctions are missing, do not ask the LLM to infer anything.

function validateEligibility(input: z.infer<typeof UnderwritingRequestSchema>) {
  if (!input.kycComplete || !input.sanctionsClear) {
    return {
      recommendation: "reject" as const,
      summary:
        "Mandatory compliance checks are incomplete. Escalate to compliance before underwriting.",
      keyRisks: ["KYC incomplete", "Sanctions status unresolved"],
      diligenceQuestions: ["Confirm beneficial ownership", "Resolve sanctions screening"],
    };
    
}

Production Considerations

  • Deployment

    • Keep the retrieval corpus in-region if your bank has data residency requirements.
    • Separate public-model traffic from internal-deal traffic; use private networking where possible.
  • Monitoring

    • Log every prompt version, retrieved document ID, model name, latency bucket, and final recommendation.
    • Track drift in approval rates by sector so you can catch prompt regressions or retrieval contamination.
  • Guardrails

    • Hard-fail on missing KYC/sanctions/compliance flags.
    • Require human approval for any “approve_with_conditions” outcome above a defined exposure threshold.
    • Block unsupported claims like “guaranteed repayment” or “risk-free” language in generated memos.
  • Auditability

    • Store input payloads and outputs immutably with correlation IDs.
    • Make sure reviewers can reconstruct why a recommendation was produced months later.

Common Pitfalls

  • Letting the model invent financial facts

    Avoid this by grounding on approved internal documents only and forcing structured output. If a number is not in the input or retrieved context, treat it as unknown.

  • Skipping compliance gates

    Do not rely on the LLM to notice missing KYC or sanctions data. Put deterministic validation in front of the chain and reject incomplete cases before generation starts.

  • Using one generic prompt for every deal type

    A bond underwriting memo is not the same as an equity placement or revolving credit facility review. Split prompts by instrument type so covenant checks, risk language, and recommendation criteria stay relevant.


Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides