How to Build a underwriting Agent Using CrewAI in Python for lending

By Cyprian AaronsUpdated 2026-04-21
underwritingcrewaipythonlending

An underwriting agent for lending takes applicant data, pulls in supporting evidence, evaluates policy rules, and produces a decision package that a human underwriter can review or approve. It matters because lending decisions need speed, consistency, and traceability; if your agent cannot explain why it approved or declined a file, you do not have something production-ready.

Architecture

  • Intake layer

    • Normalizes borrower data from LOS, CRM, bank statements, payroll APIs, and credit bureau outputs.
    • Validates required fields before the agent starts reasoning.
  • Policy engine

    • Encodes underwriting rules like minimum income, DTI thresholds, employment tenure, and adverse action triggers.
    • Keeps hard stops outside the LLM so decisions stay deterministic.
  • Research/retrieval layer

    • Lets the agent inspect supporting documents and pull relevant facts from application files.
    • Useful for exception handling and missing-data follow-up.
  • Underwriting analyst agent

    • Synthesizes facts into a recommendation: approve, decline, or refer.
    • Produces a structured rationale tied to policy criteria.
  • Compliance/audit logger

    • Stores inputs, outputs, tool calls, and final rationale.
    • Supports model governance, adverse action reviews, and regulator audits.
  • Human review handoff

    • Routes borderline cases to an underwriter when confidence is low or policy exceptions are present.
    • Prevents silent automation on risky files.

Implementation

1) Define the task shape and the decision contract

Keep the output structured. In lending, free-form text is not enough; you need machine-readable fields for downstream systems and audit trails.

from pydantic import BaseModel, Field
from typing import Literal, List

class UnderwritingDecision(BaseModel):
    decision: Literal["approve", "decline", "refer"]
    confidence: float = Field(ge=0.0, le=1.0)
    reasons: List[str]
    policy_checks: List[str]
    missing_information: List[str]

This schema becomes your contract with the rest of the lending stack. If the agent cannot populate it cleanly, you route to manual review.

2) Create a CrewAI agent with explicit underwriting instructions

Use Agent, Task, and Crew. The prompt should force policy-first reasoning and prohibit unsupported assumptions.

from crewai import Agent, Task, Crew, Process
from crewai.llm import LLM

underwriter = Agent(
    role="Loan Underwriter",
    goal="Evaluate lending applications against stated credit policy and produce a structured recommendation",
    backstory=(
        "You are an experienced underwriting analyst. "
        "You must follow policy thresholds exactly and never invent missing facts."
    ),
    llm=LLM(model="gpt-4o-mini"),
    verbose=True,
)

task = Task(
    description=(
        "Review the applicant packet below. Determine whether the file should be approved, declined,"
        " or referred to a human underwriter. Use only the provided facts.\n\n"
        "Applicant packet:\n"
        "{application_json}\n\n"
        "Policy:\n"
        "- Minimum FICO: 680\n"
        "- Maximum DTI: 43%\n"
        "- Minimum employment tenure: 12 months\n"
        "- Decline if bankruptcy within last 24 months\n"
        "- Refer if any required document is missing"
    ),
    expected_output="A JSON object matching the UnderwritingDecision schema.",
    agent=underwriter,
)

crew = Crew(
    agents=[underwriter],
    tasks=[task],
    process=Process.sequential,
)

This is the basic pattern. The important part is that policy is embedded as hard guidance in the task description while final output remains structured.

3) Run the crew with real application data and validate the result

In production you should parse the model output into your schema before anything downstream consumes it.

import json

application_json = json.dumps({
    "borrower_id": "B12345",
    "fico": 702,
    "dti": 39.2,
    "employment_months": 18,
    "bankruptcy_last_24_months": False,
    "documents": ["id", "paystub", "bank_statement"]
})

result = crew.kickoff(inputs={"application_json": application_json})
print(result.raw)

decision = UnderwritingDecision.model_validate_json(result.raw)
print(decision.decision)
print(decision.reasons)

If validation fails, do not guess. Send the file to manual review or ask for a corrected response format.

4) Add guardrails around exceptions and adverse action logic

For lending workflows, exceptions must be explicit. A common pattern is to reject any file that violates hard policy checks before the LLM gets involved.

def hard_policy_screen(app):
    if app["fico"] < 680:
        return {"route": "decline", "reason": "FICO below minimum"}
    if app["dti"] > 43:
        return {"route": "decline", "reason": "DTI above maximum"}
    if app["employment_months"] < 12:
        return {"route": "refer", "reason": "Insufficient employment tenure"}
    if app["bankruptcy_last_24_months"]:
        return {"route": "decline", "reason": "Recent bankruptcy"}
    if len(app["documents"]) < 3:
        return {"route": "refer", "reason": "Missing required documents"}
    return {"route": "agent"}

screen = hard_policy_screen({
    "fico": 702,
    "dti": 39.2,
    "employment_months": 18,
    "bankruptcy_last_24_months": False,
    "documents": ["id", "paystub", "bank_statement"]
})

That split matters. Deterministic rules handle eligibility; CrewAI handles synthesis, explanation, and exception analysis.

Production Considerations

  • Keep PII inside your controlled boundary

    • Strip unnecessary personal data before sending context to the model.
    • Enforce data residency requirements by running inference in-region and logging only what compliance allows.
  • Log every decision path

    • Store input snapshot hashes, retrieved documents used, prompt version, model version, tool calls, and final output.
    • You need this for auditability and adverse action notices.
  • Separate hard declines from discretionary referrals

    • Hard rules like recent bankruptcy should never depend on model judgment.
    • Use the agent for borderline cases where human review is expected.
  • Monitor drift by segment

    • Track approval rates by channel, geography, product type, and risk band.
    • If one segment starts getting more referrals or declines than expected, inspect prompt changes or upstream data quality issues.

Common Pitfalls

  1. Letting the LLM make eligibility decisions from scratch

    • Avoid this by screening against deterministic policy first.
    • The agent should explain decisions, not invent underwriting standards.
  2. Returning plain text instead of structured output

    • Avoid this by validating against a Pydantic schema like UnderwritingDecision.
    • Downstream lending systems need stable fields for workflow routing and audit logs.
  3. Ignoring compliance artifacts

    • Avoid this by storing prompts, outputs, timestamps, policy versions, and source documents used.
    • Without this trail you cannot support model governance or adverse action reviews.
  4. Sending too much sensitive data to the model

    • Avoid this by minimizing payloads and redacting unnecessary PII.
    • Lending systems should follow least privilege just like any other regulated system.

Keep learning

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

Related Guides