How to Build a underwriting Agent Using LangChain in TypeScript for wealth management

By Cyprian AaronsUpdated 2026-04-21
underwritinglangchaintypescriptwealth-management

An underwriting agent for wealth management takes client data, product rules, risk policies, and compliance constraints, then turns them into a consistent decision support workflow. It matters because advisors and operations teams need faster suitability checks, cleaner documentation, and a defensible audit trail without handing final judgment to a black box.

Architecture

  • Client intake layer

    • Normalizes inputs from CRM, account opening forms, KYC/AML systems, and advisor notes.
    • Converts messy text into structured underwriting fields.
  • Policy retrieval layer

    • Pulls the right suitability rules, product constraints, jurisdiction rules, and firm policies.
    • Uses retrieval over approved internal documents only.
  • Reasoning and decision layer

    • Produces a recommendation such as approve, reject, or escalate.
    • Forces the model to explain which policy clauses drove the outcome.
  • Audit and logging layer

    • Stores prompts, retrieved sources, model output, timestamps, and human overrides.
    • Needed for compliance review and post-trade or onboarding investigations.
  • Guardrail layer

    • Blocks unsupported advice, missing disclosures, and disallowed data use.
    • Enforces “no decision without evidence” behavior.
  • Integration layer

    • Sends results back to CRM, case management, or advisor workflow tools.
    • Keeps the agent inside existing operational controls.

Implementation

1. Define the underwriting schema

Start by forcing structured input. In wealth management, free-form client notes are not enough because you need consistent fields for suitability and audit.

import { z } from "zod";

export const UnderwritingInputSchema = z.object({
  clientId: z.string(),
  jurisdiction: z.string(),
  age: z.number().int().min(18),
  netWorthUsd: z.number().nonnegative(),
  annualIncomeUsd: z.number().nonnegative(),
  investmentObjective: z.enum(["income", "growth", "preservation", "speculation"]),
  riskTolerance: z.enum(["low", "medium", "high"]),
  liquidityNeedMonths: z.number().int().min(0),
  accreditedInvestor: z.boolean(),
  sourceOfFundsVerified: z.boolean(),
});

export type UnderwritingInput = z.infer<typeof UnderwritingInputSchema>;

This schema becomes your contract between the UI, orchestration layer, and LLM chain.

2. Build a retrieval-backed policy context

Use MemoryVectorStore for local development and swap it for your approved enterprise vector store in production. The key is that the agent only reasons over curated policy content.

import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnableSequence } from "@langchain/core/runnables";
import { Document } from "@langchain/core/documents";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { OpenAIEmbeddings } from "@langchain/openai";

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

const embeddings = new OpenAIEmbeddings();
const policyDocs = [
  new Document({
    pageContent:
      "Suitability rule: recommend speculative products only when risk tolerance is high and liquidity need is under 6 months.",
    metadata: { source: "wealth_policy_001", jurisdiction: "US" },
  }),
  new Document({
    pageContent:
      "Escalate any case where source of funds is unverified or client data is incomplete.",
    metadata: { source: "wealth_policy_014", jurisdiction: "US" },
  }),
];

const vectorStore = await MemoryVectorStore.fromDocuments(policyDocs, embeddings);
const retriever = vectorStore.asRetriever(3);

const prompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    [
      "You are an underwriting assistant for wealth management.",
      "Use only the provided policy context.",
      "Return JSON with fields: decision, rationale, evidence[], escalationRequired.",
      "Do not provide investment advice.",
    ].join("\n"),
  ],
  ["human", `Client input:\n{input}\n\nPolicy context:\n{context}`],
]);

const chain = RunnableSequence.from([
  async (input: string) => {
    const docs = await retriever.invoke(input);
    return {
      input,
      context: docs.map((d) => `[${d.metadata.source}] ${d.pageContent}`).join("\n"),
    };
  },
  prompt,
  llm,
  new StringOutputParser(),
]);

This pattern keeps retrieval explicit. That matters when compliance asks why a recommendation was made.

3. Validate input before invoking the chain

Never pass raw request bodies straight into the model. Validate first, then serialize into a controlled prompt payload.

export async function underwriteClient(inputRaw: unknown) {
  const input = UnderwritingInputSchema.parse(inputRaw);

  const promptInput = JSON.stringify(
    {
      clientId: input.clientId,
      jurisdiction: input.jurisdiction,
      age: input.age,
      netWorthUsd: input.netWorthUsd,
      annualIncomeUsd: input.annualIncomeUsd,
      investmentObjective: input.investmentObjective,
      riskTolerance: input.riskTolerance,
      liquidityNeedMonths: input.liquidityNeedMonths,
      accreditedInvestor: input.accreditedInvestor,
      sourceOfFundsVerified: input.sourceOfFundsVerified,
    },
    null,
    2
  );

  const result = await chain.invoke(promptInput);
  return JSON.parse(result);
}

In production you would also persist the validated payload and the model output in an immutable audit store.

4. Add deterministic escalation logic

Wealth management underwriting should not be fully autonomous. Use simple rules to escalate edge cases before letting the LLM produce a final recommendation.

export function requiresHumanReview(input: UnderwritingInput) {
  if (!input.sourceOfFundsVerified) return true;
  if (input.liquidityNeedMonths < 3 && input.investmentObjective === "speculation") return true;
  if (input.jurisdiction !== "US") return true; // route cross-border cases to regional compliance
}

export async function processUnderwriting(inputRaw: unknown) {

---

## Keep learning

- [The complete AI Agents Roadmap](/blog/ai-agents-roadmap-2026) — my full 8-step breakdown
- [Free: The AI Agent Starter Kit](/starter-kit) — PDF checklist + starter code
- [Work with me](/contact) — I build AI for banks and insurance companies

*By Cyprian Aarons, AI Consultant at [Topiax](https://topiax.xyz).*

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