How to Build a underwriting Agent Using CrewAI in TypeScript for retail banking
An underwriting agent in retail banking takes a loan application, gathers the right facts, checks them against policy, and produces a decision package for a human underwriter or an automated approval flow. It matters because underwriting is where you control credit risk, regulatory exposure, and turnaround time; if you get the workflow wrong, you either lose good customers or approve bad risk.
Architecture
- •
Application intake layer
- •Normalizes applicant data from CRM, LOS, PDF statements, and bureau responses.
- •Validates required fields before any agent work starts.
- •
Policy retrieval component
- •Pulls product rules, eligibility thresholds, and exception policies from a controlled source.
- •Keeps the agent aligned with current underwriting policy.
- •
CrewAI orchestration
- •Uses
Agent,Task, andCrewto split work into analysis steps. - •One agent can assess income stability, another can evaluate risk flags, and a final one can compile the decision memo.
- •Uses
- •
Tooling layer
- •Exposes bank-approved tools for bureau lookup, affordability calculation, KYC checks, and document extraction.
- •Keeps external access auditable and constrained.
- •
Decision output service
- •Produces a structured underwriting recommendation: approve, refer, or decline.
- •Stores rationale for audit and model governance.
- •
Compliance logging
- •Captures inputs, tool calls, outputs, timestamps, and policy version.
- •Supports audit trails, explainability reviews, and dispute handling.
Implementation
- •
Install CrewAI and define your banking tools
In TypeScript projects, keep your tools explicit. For retail banking, each tool should map to an approved internal service or a read-only external dependency.
import { Tool } from "@crewai/core"; export const calculateDTITool = new Tool({ name: "calculate_dti", description: "Calculate debt-to-income ratio from verified income and obligations", execute: async ({ monthlyIncome, monthlyDebtPayments }: { monthlyIncome: number; monthlyDebtPayments: number }) => { if (monthlyIncome <= 0) throw new Error("monthlyIncome must be > 0"); return { dti: monthlyDebtPayments / monthlyIncome, }; }, }); export const kycCheckTool = new Tool({ name: "kyc_check", description: "Check KYC status using internal compliance service", execute: async ({ customerId }: { customerId: string }) => { // Replace with internal API call return { customerId, kycStatus: "passed", sanctionsHit: false, pepHit: false, }; }, }); export const bureauFetchTool = new Tool({ name: "fetch_credit_bureau", description: "Fetch credit bureau summary for underwriting", execute: async ({ ssnLast4 }: { ssnLast4: string }) => { return { score: 721, delinquencies12M: 0, utilization: 0.28, inquiries6M: 2, }; }, }); - •
Create agents for analysis and decisioning
Use separate agents for separation of concerns. That gives you better traceability than one giant prompt that does everything.
import { Agent } from "@crewai/core"; import { calculateDTITool, kycCheckTool, bureauFetchTool } from "./tools"; export const riskAnalyst = new Agent({ role: "Retail Banking Risk Analyst", goal: "Assess applicant risk using verified financial data and bank policy", backstory: "You are a conservative retail lending analyst who prioritizes policy adherence, affordability, and fraud signals.", tools: [calculateDTITool, bureauFetchTool], verbose: true, }); export const complianceReviewer = new Agent({ role: "Bank Compliance Reviewer", goal: "Validate KYC/AML status and identify policy exceptions", backstory: "You ensure every recommendation is compliant with retail banking rules and auditable.", tools: [kycCheckTool], verbose: true, }); export const decisionWriter = new Agent({ role: "Underwriting Decision Writer", goal: "Summarize findings into an underwriting recommendation with rationale", backstory: "You produce concise decision memos for human underwriters and audit teams.", verbose: true, }); - •
Define tasks with structured outputs
Keep task outputs structured enough to store in your loan origination system. For banking workflows, free-form text alone is not enough.
import { Task } from "@crewai/core"; import { riskAnalyst, complianceReviewer, decisionWriter } from "./agents"; export const assessRiskTask = new Task({ description: "Analyze applicant affordability and credit quality using verified income of $8500/month and debt obligations of $2450/month. Return DTI analysis plus key risk flags.", expectedOutput: "A JSON-like summary containing dti, credit observations, and recommendation inputs.", agent: riskAnalyst, }); export const complianceTask = new Task({ description: "Verify KYC status for customerId CUST-10492. Identify any sanctions or PEP concerns.", expectedOutput: "A JSON-like summary containing kycStatus, sanctionsHit, pepHit, and compliance notes.", agent: complianceReviewer, }); export const memoTask = new Task({ description: "Write the final underwriting memo using prior findings. Recommend approve, refer, or decline with reasons.", expectedOutput: "A concise underwriting memo suitable for audit review.", agent: decisionWriter, context: [assessRiskTask as any, complianceTask as any], }); - •
Run the crew and persist the result
The core pattern is
Crew -> kickoff() -> persist output. In production you should wrap this behind an API endpoint or queue consumer.import { Crew } from "@crewai/core"; import { assessRiskTask, complianceTask, memoTask } from "./tasks"; async function main() { const crew = new Crew({ agents: [], tasks: [assessRiskTask, complianceTask, memoTask], verbose: true, processType: "sequential", }); const result = await crew.kickoff(); console.log("Underwriting result:", result); // Persist result + policy version + request metadata to your audit store here } main().catch((err) => { console.error(err); process.exit(1); });
Production Considerations
- •
Data residency
- •Keep applicant PII inside your approved region.
- •If CrewAI calls external services or LLMs are involved downstream, ensure model hosting stays within your regulatory boundary.
- •
Auditability
- •Log every task input/output pair with timestamps and policy version.
- •Store tool invocation metadata so reviewers can reconstruct how a decision was reached.
- •
Guardrails
- •Hard-block unsupported decisions like “approve despite failed KYC.”
- •Add deterministic rules outside the agent for minimum score thresholds, max DTI limits, blacklist checks, and exception routing.
- •
Monitoring
- •Track approval rate drift by product segment.
- •Alert on abnormal referral spikes because they usually indicate policy mismatch or upstream data quality issues.
Common Pitfalls
- •
Letting the agent make final credit decisions without hard rules
- •Avoid this by enforcing deterministic policy checks before any recommendation is accepted.
- •The agent should recommend; your rules engine should decide whether that recommendation is admissible.
- •
Passing raw sensitive data into prompts unnecessarily
- •Avoid sending full account numbers or unmasked identifiers.
- •Use tokenized customer IDs and only the minimum attributes required for underwriting.
- •
Skipping versioned policy context
- •If you do not stamp each run with a policy version, audit becomes messy fast.
- •Store the exact eligibility rules used for every request so disputes can be traced later.
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