How to Build a underwriting Agent Using CrewAI in Python for retail banking
A underwriting agent in retail banking collects applicant data, checks policy rules, scores risk, and produces a decision packet for a human underwriter or an automated approval flow. It matters because retail lending is high-volume, regulated, and sensitive to inconsistency; if you can standardize the first-pass decisioning layer, you reduce turnaround time without losing auditability.
Architecture
- •
Input ingestion layer
- •Pulls application data from CRM, loan origination systems, and document stores.
- •Normalizes fields like income, employment status, existing liabilities, and requested credit amount.
- •
Policy and compliance checker
- •Applies bank-specific underwriting rules.
- •Enforces regulatory constraints such as fair lending checks, adverse action requirements, and KYC/AML flags.
- •
Risk analysis agent
- •Evaluates affordability, debt-to-income ratio, credit profile signals, and exception conditions.
- •Produces a structured risk summary with rationale.
- •
Decision synthesis agent
- •Combines policy results and risk analysis into an underwriting recommendation.
- •Outputs approve / refer / decline with reasons.
- •
Audit logger
- •Captures inputs, tool outputs, prompts, model version, timestamps, and final recommendation.
- •Supports internal audit and model governance.
- •
Human review handoff
- •Routes borderline or policy-flagged cases to a human underwriter.
- •Prevents fully automated decisions where policy requires manual review.
Implementation
1) Install CrewAI and define the underwriting tools
Use CrewAI for orchestration and keep business logic in tools. In banking systems, that separation matters because tools are easier to test, version, and audit than free-form agent behavior.
from crewai import Agent, Task, Crew
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Type
class UnderwritingInput(BaseModel):
annual_income: float = Field(..., description="Applicant annual gross income")
monthly_debt: float = Field(..., description="Total monthly debt obligations")
requested_amount: float = Field(..., description="Requested loan amount")
credit_score: int = Field(..., description="FICO-like score")
employment_months: int = Field(..., description="Months at current employer")
class DTIComputationTool(BaseTool):
name: str = "dti_computation"
description: str = "Compute debt-to-income ratio for retail underwriting"
args_schema: Type[BaseModel] = UnderwritingInput
def _run(self, annual_income: float, monthly_debt: float,
requested_amount: float, credit_score: int,
employment_months: int) -> str:
monthly_income = annual_income / 12.0
dti = monthly_debt / monthly_income if monthly_income else 999.0
return f"DTI={dti:.2f}, monthly_income={monthly_income:.2f}"
class PolicyCheckTool(BaseTool):
name: str = "policy_check"
description: str = "Apply simple retail banking underwriting rules"
args_schema: Type[BaseModel] = UnderwritingInput
def _run(self, annual_income: float, monthly_debt: float,
requested_amount: float, credit_score: int,
employment_months: int) -> str:
flags = []
if credit_score < 620:
flags.append("low_credit_score")
if employment_months < 12:
flags.append("short_employment_history")
if requested_amount > annual_income * 0.4:
flags.append("high_requested_amount")
return "flags=" + ",".join(flags) if flags else "flags=none"
2) Create specialized agents with narrow responsibilities
Do not build one giant agent that “does underwriting.” Split responsibilities so each step is explainable and easier to govern.
risk_agent = Agent(
role="Retail Credit Risk Analyst",
goal="Assess applicant affordability and risk using provided inputs",
backstory="You analyze retail lending applications with a focus on consistency and explainability.",
tools=[DTIComputationTool()],
verbose=True,
)
policy_agent = Agent(
role="Underwriting Policy Analyst",
goal="Check the application against bank policy and compliance rules",
backstory="You enforce underwriting policy without making unsupported assumptions.",
tools=[PolicyCheckTool()],
verbose=True,
)
decision_agent = Agent(
role="Underwriting Decision Maker",
goal="Synthesize risk findings into a recommendation for approve/refer/decline",
backstory="You produce conservative recommendations suitable for human review.",
verbose=True,
)
3) Define tasks and wire them into a Crew
CrewAI’s Task objects give you the control point for outputs. For banking workflows, ask for structured output so downstream systems can store it cleanly.
risk_task = Task(
description=(
"Analyze this applicant using the provided tools. "
"Return a concise risk summary including DTI interpretation."
),
expected_output="A short risk summary with numeric DTI context.",
agent=risk_agent,
)
policy_task = Task(
description=(
"Run policy checks on the same applicant. "
"Identify any rule violations relevant to retail lending."
),
expected_output="A list of policy flags or 'none'.",
agent=policy_agent,
)
decision_task = Task(
description=(
"Based on the prior analysis and policy checks, recommend "
"'approve', 'refer', or 'decline' with two reasons."
),
expected_output="A recommendation with rationale suitable for audit.",
agent=decision_agent,
)
crew = Crew(
agents=[risk_agent, policy_agent, decision_agent],
tasks=[risk_task, policy_task, decision_task],
verbose=True,
)
4) Run the crew on an application payload
In production you would pass sanitized application data from your loan origination system. Keep PII handling outside the prompt where possible.
application_context = {
"annual_income": 96000,
"monthly_debt": 1850,
"requested_amount": 22000,
"credit_score": 684,
}
result = crew.kickoff(inputs=application_context)
print(result)
Production Considerations
- •
Data residency
- •Keep application data in-region if your bank has country-specific residency requirements.
- •Do not send raw PII to external services unless your legal/compliance team has approved the route.
- •
Auditability
- •Persist every task input/output pair with timestamps and model identifiers.
- •Store the exact prompt template version used for each decision so internal audit can replay it later.
- •
Guardrails
- •Add hard stops for protected-class inference and prohibited variables.
- •Force refer-to-human outcomes when confidence is low or when policy exceptions are detected.
- •
Monitoring
- •Track approval rates by segment, manual referral rates, override rates by underwriter team, and drift in key fields like DTI or score bands.
- •Alert on sudden shifts that may indicate upstream data issues or model behavior changes.
Common Pitfalls
- •
Putting all logic inside one LLM prompt
- •This makes decisions hard to test and impossible to govern cleanly.
- •Move deterministic checks like DTI thresholds into tools or service code.
- •
Ignoring structured outputs
- •Free-form text is bad for downstream LOS integration.
- •Require explicit fields like
decision,reasons,flags, andreview_required.
- •
Skipping compliance review of prompts and tools
- •A prompt can accidentally introduce prohibited reasoning paths or unsupported explanations.
- •Treat prompts as regulated artifacts and run them through legal/compliance review before production release.
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