How to Build a underwriting Agent Using AutoGen in TypeScript for lending
An underwriting agent for lending takes a loan application, pulls the right facts from source systems, applies policy rules, and produces a decision package a human underwriter can review or approve. It matters because lending decisions need speed without losing control: every recommendation must be explainable, auditable, and consistent with credit policy.
Architecture
- •
Application intake service
- •Accepts borrower data, requested amount, product type, and consent flags.
- •Normalizes the payload before it reaches the agent.
- •
Data retrieval tools
- •Pulls bureau summaries, bank transaction signals, KYC/AML status, and internal exposure.
- •Keep these as explicit tool functions so the agent never “guesses” missing facts.
- •
Underwriting assistant agent
- •Uses AutoGen
AssistantAgentto reason over policy and assemble a recommendation. - •Produces structured output: approve, refer, or decline with reasons.
- •Uses AutoGen
- •
Policy engine / rules layer
- •Enforces hard constraints outside the LLM: minimum income ratio, max DTI, blacklist checks, residency restrictions.
- •The model can explain; the rules decide.
- •
Audit and case store
- •Persists prompts, tool calls, model output, policy version, and final decision.
- •This is mandatory for lending audits and adverse action review.
- •
Human review workflow
- •Routes borderline cases to an underwriter through a UI or queue.
- •Keeps the agent in advisory mode for regulated decisions.
Implementation
- •Install AutoGen and define your underwriting data model
Use the TypeScript AutoGen package and keep your outputs structured. For lending, you want deterministic fields that map cleanly to your decision engine and audit logs.
npm install @autogen/agentchat zod
import { AssistantAgent } from "@autogen/agentchat";
import { z } from "zod";
const UnderwritingDecisionSchema = z.object({
decision: z.enum(["approve", "refer", "decline"]),
riskGrade: z.enum(["A", "B", "C", "D"]),
reasons: z.array(z.string()).min(1),
requiredConditions: z.array(z.string()).default([]),
});
type UnderwritingDecision = z.infer<typeof UnderwritingDecisionSchema>;
type LoanApplication = {
applicationId: string;
borrowerId: string;
amount: number;
annualIncome: number;
monthlyDebtPayments: number;
productType: "personal_loan" | "auto_loan" | "small_business_loan";
};
- •Create an underwriting agent with explicit instructions
The key pattern is to force the agent to work from supplied facts only. Do not let it invent credit metrics or compliance outcomes.
const underwritingAgent = new AssistantAgent({
name: "underwriting_agent",
systemMessage: `
You are an underwriting assistant for lending.
Use only provided application facts and tool results.
Do not invent values or assume missing data.
Return concise underwriting reasoning with:
- decision
- riskGrade
- reasons
- requiredConditions
Flag any compliance or data residency concerns when relevant.
`,
});
- •Add tools for policy checks and external signals
In AutoGen TypeScript, expose real functions through registerTool. Keep these tools narrow. A lending agent should query facts; it should not directly make database decisions.
async function calculateDTI(input: { annualIncome: number; monthlyDebtPayments: number }) {
const monthlyIncome = input.annualIncome / 12;
const dti = input.monthlyDebtPayments / monthlyIncome;
return { dti };
}
async function checkPolicy(input: { amount: number; dti: number }) {
const approve =
input.amount <= 50000 &&
input.dti < 0.43;
return {
approve,
policyVersion: "2026.01",
notes: approve ? ["Meets base policy"] : ["Fails base lending policy"],
};
}
underwritingAgent.registerTool(
{
name: "calculateDTI",
description: "Calculate debt-to-income ratio from verified income and debt payments.",
parameters: z.object({
annualIncome: z.number(),
monthlyDebtPayments: z.number(),
}),
execute: calculateDTI,
},
);
underwritingAgent.registerTool(
{
name: "checkPolicy",
description: "Apply hard underwriting rules.",
parameters: z.object({
amount: z.number(),
dti: z.number(),
}),
execute: checkPolicy,
},
);
- •Run the assessment and parse a structured result
This is where you combine tool outputs with the model’s reasoning. In production, persist both the raw response and the parsed decision.
async function underwrite(app: LoanApplication): Promise<UnderwritingDecision> {
const dtiResult = await calculateDTI({
annualIncome: app.annualIncome,
monthlyDebtPayments: app.monthlyDebtPayments,
});
const policyResult = await checkPolicy({
amount: app.amount,
dti: dtiResult.dti,
});
const prompt = `
Application:
${JSON.stringify(app)}
Verified metrics:
${JSON.stringify(dtiResult)}
Policy result:
${JSON.stringify(policyResult)}
Return JSON only matching this schema:
{
"decision": "approve" | "refer" | "decline",
"riskGrade": "A" | "B" | "C" | "D",
"reasons": string[],
"requiredConditions": string[]
}
`;
const response = await underwritingAgent.run(prompt);
const content = typeof response === "string" ? response : JSON.stringify(response);
return UnderwritingDecisionSchema.parse(JSON.parse(content));
}
Production Considerations
- •
Keep hard credit policy outside the model
Use deterministic code for DTI thresholds, exposure caps, KYC status, residency rules, and prohibited-product logic. The LLM should explain outcomes, not override them.
- •
Log everything needed for audit
Store application snapshot, retrieved data versions, prompt text, tool calls, final JSON output, model version, and policy version. Lending teams will need this for adverse action notices and regulator reviews.
- •
Control data residency
If borrower data must stay in-region, run the model endpoint and all tool backends in that same jurisdiction. Do not send raw PII to third-party services without a legal basis and retention controls.
- •
Add monitoring on decision drift
Track approval rate by segment, referral rate by product type, false declines reviewed by humans, and latency per decision. Alert when distributions shift after model or policy changes.
Common Pitfalls
- •
Letting the model decide credit policy
Avoid prompts like “approve if it looks good.” That creates inconsistent decisions. Put thresholds in code and use the agent for explanation plus exception handling.
- •
Passing raw PII into long prompts
Don’t dump full bank statements or identity documents into context unless you truly need them. Redact what you can and pass derived features instead.
- •
Skipping structured output validation
Free-form text is not acceptable for underwriting workflows. Validate with Zod or equivalent before writing anything to your case system.
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