How to Build a underwriting Agent Using LangChain in TypeScript for healthcare

By Cyprian AaronsUpdated 2026-04-21
underwritinglangchaintypescripthealthcare

A healthcare underwriting agent reviews patient, plan, and policy inputs, then produces a recommendation with evidence: approve, deny, route to manual review, or request more data. It matters because underwriting in healthcare is high-stakes work where speed, consistency, auditability, and compliance all matter at the same time.

Architecture

  • Input normalization layer
    • Converts raw intake data from EMR exports, PDF forms, CRM notes, and eligibility APIs into a consistent underwriting schema.
  • Policy retrieval layer
    • Pulls relevant plan rules, medical policy docs, exclusion clauses, and state-specific regulations using a vector store retriever.
  • Decision agent
    • Uses LangChain tools and an LLM to classify the case and generate a recommendation with structured reasoning.
  • Audit trail store
    • Persists every prompt, retrieved policy chunk, model output, and final decision for compliance review.
  • Human review queue
    • Routes ambiguous or high-risk cases to an underwriter instead of auto-deciding.
  • PII/PHI guardrail layer
    • Redacts or minimizes protected health information before sending anything to the model.

Implementation

1) Define the underwriting schema and model wrapper

Keep the output structured. For healthcare workflows, free-form text is not enough because you need deterministic downstream handling and audit logs.

import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";

const UnderwritingDecisionSchema = z.object({
  decision: z.enum(["approve", "deny", "manual_review", "request_more_info"]),
  riskScore: z.number().min(0).max(100),
  rationale: z.string(),
  evidence: z.array(z.string()).min(1),
  missingInformation: z.array(z.string()).default([]),
});

export type UnderwritingDecision = z.infer<typeof UnderwritingDecisionSchema>;

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

2) Load policy documents into a retriever

Use LangChain’s document loaders and vector store so the agent can cite current policy language instead of relying on memory.

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

const policyDocs = [
  new Document({
    pageContent:
      "Chronic conditions require manual review if prior authorization is missing.",
    metadata: { source: "medical_policy_2025.pdf", jurisdiction: "US" },
  }),
  new Document({
    pageContent:
      "Experimental procedures are excluded unless explicitly approved by medical director.",
    metadata: { source: "plan_exclusions.md", jurisdiction: "US" },
  }),
];

const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 500, chunkOverlap: 50 });
const chunks = await splitter.splitDocuments(policyDocs);

const vectorStore = await MemoryVectorStore.fromDocuments(
  chunks,
  new OpenAIEmbeddings()
);

const retriever = vectorStore.asRetriever(4);

3) Build the agent with retrieval + structured output

This pattern keeps the model grounded in policy text and forces a typed result. In production you can swap MemoryVectorStore for Pinecone, pgvector, or OpenSearch without changing the agent logic.

import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";

const prompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    `You are a healthcare underwriting assistant.
Use only the provided policy context and case facts.
If information is missing or ambiguous, choose manual_review or request_more_info.
Do not expose PHI beyond what is necessary.`,
  ],
  [
    "human",
    `Case facts:
{caseFacts}

Policy context:
{policyContext}

Return a JSON object matching this schema:
{formatInstructions}`,
  ],
]);

async function underwriteCase(caseFacts: string): Promise<UnderwritingDecision> {
  const docs = await retriever.getRelevantDocuments(caseFacts);
  const policyContext = docs.map((d) => d.pageContent).join("\n---\n");

  const chain = RunnableSequence.from([
    async (input: { caseFacts: string }) => ({
      caseFacts: input.caseFacts,
      policyContext,
      formatInstructions:
        'decision must be one of approve|deny|manual_review|request_more_info',
    }),
    prompt,
    llm.withStructuredOutput(UnderwritingDecisionSchema),
  ]);

  return chain.invoke({ caseFacts });
}

4) Add an audit log and human review routing

Healthcare underwriting needs traceability. Store inputs, retrieved evidence, model output, and final disposition so compliance teams can reconstruct every decision.

type AuditRecord = {
  requestId: string;
  timestamp: string;
  caseFacts: string;
  retrievedSources: string[];
};

async function runUnderwriting(requestId: string, caseFacts: string) {
  const decision = await underwriteCase(caseFacts);

  const auditRecord: AuditRecord = {
    requestId,
    timestamp: new Date().toISOString(),
    caseFacts,
    retrievedSources: ["medical_policy_2025.pdf", "plan_exclusions.md"],
  };

  console.log("AUDIT_RECORD", JSON.stringify(auditRecord));
  
  if (decision.decision === "manual_review" || decision.riskScore > 70) {
    return { status: "queued_for_underwriter", decision };
    }

   return { status: "completed", decision };
}

Production Considerations

  • Compliance controls
    • Treat PHI as regulated data. Minimize fields sent to the LLM, encrypt logs at rest, and make sure your vendor setup supports HIPAA obligations and BAAs where required.
  • Data residency
    • Keep policy documents and case data in-region if your regulatory environment requires it. If you operate across jurisdictions, separate indexes by region/state/market.
  • Monitoring
    • Track approval rate drift, manual-review rate, retrieval hit quality, latency per case, and hallucination rate on cited policy text.
  • Guardrails
    • Enforce structured outputs with withStructuredOutput, reject unsupported decisions server-side, and route low-confidence cases to humans instead of forcing automation.

Common Pitfalls

  • Sending raw PHI into prompts

    Avoid dumping full clinical notes into the model. Redact identifiers first and only pass the fields needed for underwriting.

  • Using generic retrieval without jurisdiction filters

    Healthcare rules vary by state and plan type. Filter retrieved documents by jurisdiction and product line before generation.

  • Letting the model make final decisions on edge cases

    High-risk or incomplete cases should go to manual review. Use the agent to triage and explain; do not let it override policy gaps or ambiguous medical history.


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