How to Build a underwriting Agent Using LangChain in TypeScript for healthcare
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.
- •Enforce structured outputs with
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
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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