How to Build a underwriting Agent Using LlamaIndex in TypeScript for lending
An underwriting agent for lending takes borrower documents, extracts the right facts, checks them against policy, and produces a decision-ready summary for a human underwriter or an automated workflow. It matters because lending decisions need to be fast, consistent, auditable, and grounded in policy — not buried in ad hoc prompts and brittle scripts.
Architecture
- •
Document ingestion layer
- •Pulls bank statements, payslips, tax returns, credit memos, and application forms from object storage or your loan origination system.
- •Normalizes PDFs and text into
Documentobjects for LlamaIndex.
- •
Policy knowledge base
- •Stores underwriting rules, product criteria, exception thresholds, and compliance notes.
- •Indexed with
VectorStoreIndexso the agent can retrieve the exact policy passages it needs.
- •
Case retrieval layer
- •Retrieves borrower-specific evidence from uploaded docs using
VectorStoreIndex.asQueryEngine(). - •Keeps answers grounded in source text instead of model memory.
- •Retrieves borrower-specific evidence from uploaded docs using
- •
Decision orchestration
- •Uses an LLM-backed
ReActAgentto compare borrower facts against policy and produce a structured recommendation. - •Routes ambiguous cases to human review.
- •Uses an LLM-backed
- •
Audit and trace store
- •Persists retrieved chunks, model outputs, tool calls, and final recommendations.
- •Needed for compliance review, adverse action support, and internal model governance.
Implementation
1. Load policy docs and borrower files into LlamaIndex
Use separate indexes for policy and case documents. That gives you cleaner retrieval boundaries and better auditability.
import {
Document,
VectorStoreIndex,
Settings,
} from "llamaindex";
import { OpenAI } from "@llamaindex/openai";
Settings.llm = new OpenAI({
model: "gpt-4o-mini",
});
async function buildIndexes() {
const policyDocs = [
new Document({
text: `
Underwriting Policy:
- Minimum FICO for unsecured personal loans: 680
- Maximum DTI: 40%
- Require proof of income for all applicants
- Manual review required if bank statement cash deposits exceed 30% of monthly income
`,
metadata: { source: "policy-manual-v3" },
}),
];
const borrowerDocs = [
new Document({
text: `
Applicant: Jane Doe
Monthly income: $8,500
Monthly debt payments: $2,900
FICO: 702
Cash deposits on bank statement: $3,100
`,
metadata: { source: "borrower-upload", applicantId: "app-123" },
}),
];
const policyIndex = await VectorStoreIndex.fromDocuments(policyDocs);
const caseIndex = await VectorStoreIndex.fromDocuments(borrowerDocs);
return { policyIndex, caseIndex };
}
2. Create query engines for policy lookup and case evidence
The underwriting agent should not “guess” policy. It should retrieve the relevant rule text and the relevant borrower evidence separately.
async function createRetrievers(policyIndex: VectorStoreIndex, caseIndex: VectorStoreIndex) {
const policyQueryEngine = policyIndex.asQueryEngine({
similarityTopK: 3,
responseMode: "compact",
});
const caseQueryEngine = caseIndex.asQueryEngine({
similarityTopK: 3,
responseMode: "compact",
});
return { policyQueryEngine, caseQueryEngine };
}
3. Wire a ReAct agent around explicit tools
This is the pattern that works well in lending: one tool for policy retrieval, one tool for case retrieval. The agent then writes a decision memo with citations from both sources.
import {
FunctionTool,
ReActAgent,
} from "llamaindex";
async function buildUnderwritingAgent(
policyQueryEngine: ReturnType<VectorStoreIndex["asQueryEngine"]>,
caseQueryEngine: ReturnType<VectorStoreIndex["asQueryEngine"]>,
) {
const policyTool = FunctionTool.from(async ({ question }: { question: string }) => {
const result = await policyQueryEngine.query({ queryStr: question });
return result.response;
}, {
name: "lookup_policy",
description: "Retrieve underwriting policy language relevant to the applicant question.",
parameters: {
type: "object",
properties: {
question: { type: "string" },
},
required: ["question"],
},
});
const caseTool = FunctionTool.from(async ({ question }: { question: string }) => {
const result = await caseQueryEngine.query({ queryStr: question });
return result.response;
}, {
name: "lookup_case",
description: "Retrieve borrower-specific facts from submitted documents.",
parameters: {
type: "object",
properties: {
question: { type: "string" },
},
required: ["question"],
},
});
return ReActAgent.fromTools([policyTool, caseTool], {
llmOptions: {
modelName: "gpt-4o-mini",
temperature":0,
},
systemPrompt:
"You are an underwriting assistant. Compare borrower facts to lending policy. Return a concise recommendation with reasons, exceptions, and missing documents. Never invent facts.",
});
}
4. Ask for a decision memo with hard constraints
Force the output format so downstream systems can parse it. In lending, free-form prose is where bad decisions hide.
async function runUnderwrite(agent) {
const prompt = `
Evaluate this application:
1) Retrieve current underwriting rules for FICO, DTI, income verification, and cash deposit exceptions.
2) Retrieve applicant facts relevant to those rules.
3) Return JSON with:
- decision ("approve" | "refer" | "decline")
- reasons (array)
- missing_items (array)
- cited_policy (array)
- cited_facts (array)
Do not make up any values.
`;
const response = await agent.chat({ message });
console.log(response.response);
}
A practical production pattern is to parse the final output into a schema before it reaches your loan origination system. If the JSON fails validation, route it to manual review instead of trying to “fix” it with another model call.
Production Considerations
- •
Keep data residency explicit
- •Store borrower data in-region and pin your vector store plus LLM endpoints to approved jurisdictions.
- •For regulated lenders, document where embeddings are stored because they can still be considered derived customer data.
- •
Log every decision path
- •Persist retrieved chunks, tool inputs/outputs, final recommendation, model version, prompt version, and timestamp.
- •This gives you auditability for internal controls and adverse action reviews.
- •
Add guardrails before automation
- •Hard-stop on missing income verification or stale credit data.
- •
Monitor drift by product segment
Common Pitfalls
- •Mixing policy docs with borrower docs in one index
This makes retrieval noisy and harder to audit. Keep separate indexes so you can show exactly which source drove which part of the decision.
- •Letting the model infer missing financial facts
If DTI or income is absent, the agent should return refer or missing_items, not estimate from context. In lending, invented numbers become compliance incidents fast.
- •Skipping structured output validation
Don’t trust raw text responses in production. Validate against a schema like decision/reasons/cited_policy/cited_facts before writing anything to your LOS or CRM.
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