How to Build a underwriting Agent Using CrewAI in Python for banking
An underwriting agent in banking takes a loan application, gathers the right evidence, checks policy and compliance rules, scores risk, and produces a decision memo for a human underwriter. The value is simple: faster turnaround on standard cases, tighter consistency on policy application, and better auditability than a manual email-and-spreadsheet process.
Architecture
A production underwriting agent in banking needs these components:
- •
Intake layer
- •Normalizes application data from CRM, LOS, or API payloads.
- •Validates required fields like income, DTI, employment status, and requested amount.
- •
Policy retrieval layer
- •Pulls bank-specific lending policy, product rules, and exception thresholds.
- •Keeps policy text versioned for audit and change control.
- •
Risk analysis layer
- •Evaluates affordability, debt service coverage, repayment history, and fraud signals.
- •Produces structured findings instead of free-form prose.
- •
Compliance guardrail layer
- •Checks KYC/AML flags, adverse action requirements, fair lending constraints, and prohibited attributes.
- •Blocks decisions when required evidence is missing.
- •
Decisioning layer
- •Converts findings into approve / refer / decline recommendations.
- •Emits rationale that can be reviewed by an underwriter.
- •
Audit and logging layer
- •Stores prompts, tool outputs, policy versions, timestamps, and final recommendations.
- •Supports model governance and regulator review.
Implementation
1) Define the inputs and the agent tasks
Use CrewAI’s Agent, Task, and Crew classes to separate policy review from underwriting analysis. For banking workflows, keep the output structured so it can be stored in an audit trail or handed to a human reviewer.
from crewai import Agent, Task, Crew, Process
from crewai.llm import LLM
llm = LLM(model="gpt-4o-mini", temperature=0)
policy_agent = Agent(
role="Credit Policy Analyst",
goal="Interpret bank underwriting policy and identify applicable rules.",
backstory="You are a senior credit policy analyst for a retail bank.",
llm=llm,
verbose=True,
)
underwriting_agent = Agent(
role="Underwriting Analyst",
goal="Assess borrower risk using provided application data and policy findings.",
backstory="You underwrite consumer loans with strict compliance discipline.",
llm=llm,
verbose=True,
)
policy_task = Task(
description=(
"Review the following policy excerpt and extract the approval rules, "
"exception thresholds, and mandatory decline conditions.\n\n"
"POLICY:\n{policy_text}"
),
expected_output="A concise bullet list of underwriting rules with thresholds.",
agent=policy_agent,
)
analysis_task = Task(
description=(
"Using this application data:\n{application_json}\n\n"
"and these policy findings:\n{policy_findings}\n\n"
"Produce a recommendation: APPROVE, REFER_TO_HUMAN, or DECLINE. "
"Include key risk factors and compliance concerns."
),
expected_output="A structured underwriting recommendation with rationale.",
agent=underwriting_agent,
)
2) Run the crew with sequential processing
For underwriting you usually want deterministic flow: policy first, analysis second. Process.sequential keeps that order explicit.
crew = Crew(
agents=[policy_agent, underwriting_agent],
tasks=[policy_task, analysis_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff(
inputs={
"policy_text": """
Minimum FICO: 680
Max DTI: 43%
Decline if bankruptcy within last 12 months
Refer if income documents are missing
Manual review required for self-employed applicants
""",
"application_json": """
{
"applicant_id": "A10293",
"fico": 702,
"dti": 41.8,
"employment_type": "salaried",
"income_verified": true,
"bankruptcy_last_12_months": false,
"loan_amount": 25000
}
"""
}
)
print(result)
This pattern works because each task has one job. The first task extracts policy logic; the second applies it to the case file. That separation makes audits easier when model behavior needs to be explained to risk or compliance teams.
3) Add tool access for controlled data retrieval
In banking you should not dump raw core banking data into prompts. Use tools that fetch only the minimum necessary fields from approved systems.
from crewai.tools import BaseTool
class GetCreditBureauSummaryTool(BaseTool):
name: str = "get_credit_bureau_summary"
description: str = "Fetches a masked bureau summary for an applicant."
def _run(self, applicant_id: str) -> str:
# Replace with real API call to your internal service
return (
f"Applicant {applicant_id}: open tradelines=4, delinquencies_24m=1, "
f"utilization=32%, inquiries_6m=2"
)
risk_agent = Agent(
role="Risk Analyst",
goal="Summarize bureau and repayment risk without exposing unnecessary PII.",
backstory="You work inside a regulated lending operations team.",
tools=[GetCreditBureauSummaryTool()],
llm=llm,
)
risk_task = Task(
description=(
"Call get_credit_bureau_summary for applicant_id {applicant_id} "
"and summarize the repayment risk in two sentences."
),
expected_output="A short risk summary suitable for underwriting review.",
agent=risk_agent,
)
4) Return an auditable decision memo
The final output should be machine-readable enough for downstream systems and human-readable enough for operations. Keep the recommendation tied to rule references and input values.
| Field | Example |
|---|---|
| decision | REFER_TO_HUMAN |
| reason_codes | Missing income docs; self-employed exception |
| key_metrics | FICO 702; DTI 41.8% |
| policy_version | Retail-Lending-v3.2 |
| reviewer_required | true |
That structure is what lets you build an adverse action workflow later without reworking the whole agent.
Production Considerations
- •
Deploy behind internal boundaries
- •Keep the crew inside your VPC or private network segment.
- •Do not send raw PII to external endpoints unless your legal/compliance team has approved it.
- •Enforce data residency by region if your bank operates across jurisdictions.
- •
Log everything needed for audit
- •Store prompt inputs, retrieved policy version, tool outputs, timestamps, model name, and final recommendation.
- •Make logs immutable or write-once where possible.
- •Tie every decision to an application ID and case worker ID.
- •
Add hard guardrails
- •Block use of protected attributes like race, religion, health status, or other prohibited proxies.
- •Require human review on low-confidence cases or exception paths.
- •Validate output against schema before it reaches LOS or CRM systems.
- •
Monitor drift and exception rates
- •Track approval rates by product type, channel, geography allowed by law, and underwriter queue.
- •Watch for rising referral volume after policy updates.
- •Re-test prompts whenever credit policy changes.
Common Pitfalls
- •
Letting the model make unsupported decisions
- •Mistake: asking the agent to “decide” without giving it explicit rules or thresholds.
- •Fix: pass versioned policy text into a dedicated task before any recommendation is made.
- •
Exposing too much customer data
- •Mistake: sending full statements or raw bureau files into the prompt.
- •Fix: use tools that return masked summaries and only retrieve fields needed for the decision.
- •
Skipping auditability
- •Mistake: storing only the final answer with no trace of inputs or reasoning.
- •Fix: persist task outputs, tool calls, model version, policy version, and timestamps for every case.
If you build it this way—policy first, analysis second, human override always available—you get an underwriting agent that fits banking operations instead of fighting them.
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