How to Build a underwriting Agent Using LangGraph in Python for banking
An underwriting agent for banking takes a loan or credit application, gathers the relevant facts, checks policy and compliance rules, scores risk, and produces a decision package that a human underwriter can review or approve. It matters because banks need faster turnaround without losing control: every decision must be explainable, auditable, and consistent with credit policy.
Architecture
- •
Input intake node
- •Normalizes applicant data from CRM, LOS, PDFs, or API payloads.
- •Validates required fields like income, liabilities, collateral, and jurisdiction.
- •
Document extraction node
- •Pulls structured facts from bank statements, payslips, tax returns, and KYC documents.
- •Should return citations or source references for audit trails.
- •
Policy and compliance rules node
- •Checks hard constraints such as minimum DSCR, LTV caps, sanctions flags, and residency requirements.
- •Separates policy failures from soft risk concerns.
- •
Risk scoring node
- •Produces a recommendation using deterministic rules plus optional model outputs.
- •Should output a structured decision object, not free text.
- •
Human review gate
- •Routes borderline or high-risk cases to an underwriter.
- •Captures override reasons for governance.
- •
Audit logger
- •Persists state transitions, inputs used, outputs generated, and final decision.
- •Required for model risk management and regulator review.
Implementation
1. Define the graph state and decision schema
Use typed state so every node knows what it can read and write. In banking workflows, this keeps the agent from drifting into unstructured output.
from typing import TypedDict, Optional, Literal
from langgraph.graph import StateGraph, START, END
Decision = Literal["approve", "decline", "manual_review"]
class UnderwritingState(TypedDict):
applicant_id: str
income: float
monthly_debt: float
loan_amount: float
collateral_value: float
jurisdiction: str
sanctions_hit: bool
dscr: Optional[float]
ltv: Optional[float]
decision: Optional[Decision]
rationale: Optional[str]
2. Add deterministic underwriting nodes
Keep the core policy logic explicit. For banking use cases, deterministic checks are easier to audit than opaque agent behavior.
def compute_ratios(state: UnderwritingState):
income = state["income"]
debt = state["monthly_debt"]
loan_amount = state["loan_amount"]
collateral = state["collateral_value"]
dscr = round(income / debt if debt else 0.0, 2)
ltv = round(loan_amount / collateral if collateral else 1.0, 2)
return {"dscr": dscr, "ltv": ltv}
def apply_policy(state: UnderwritingState):
if state["sanctions_hit"]:
return {
"decision": "decline",
"rationale": "Sanctions screening hit; automatic decline per policy."
}
if state["jurisdiction"] not in {"US", "UK", "EU"}:
return {
"decision": "manual_review",
"rationale": "Unsupported jurisdiction; requires compliance review."
}
if state["dscr"] < 1.25 or state["ltv"] > 0.80:
return {
"decision": "manual_review",
"rationale": f"Policy threshold not met (DSCR={state['dscr']}, LTV={state['ltv']})."
}
return {
"decision": "approve",
"rationale": f"Meets policy thresholds (DSCR={state['dscr']}, LTV={state['ltv']})."
}
3. Build routing with StateGraph and conditional edges
This is the LangGraph pattern you want in production: compute facts first, then route based on those facts. Use add_conditional_edges to separate automatic approval from manual review.
def route_decision(state: UnderwritingState):
return state["decision"]
graph = StateGraph(UnderwritingState)
graph.add_node("ratios", compute_ratios)
graph.add_node("policy", apply_policy)
graph.add_edge(START, "ratios")
graph.add_edge("ratios", "policy")
graph.add_conditional_edges(
"policy",
route_decision,
{
"approve": END,
"decline": END,
"manual_review": END,
},
)
underwriting_app = graph.compile()
4. Run the workflow and persist the outcome
In a real bank you would attach persistence through a checkpointer or external audit store. Even without that here, the compiled app returns a full result that can be logged downstream.
input_state: UnderwritingState = {
"applicant_id": "APP-10021",
"income": 12000.0,
"monthly_debt": 3000.0,
"loan_amount": 180000.0,
"collateral_value": 250000.0,
"jurisdiction": "US",
"sanctions_hit": False,
"dscr": None,
"ltv": None,
"decision": None,
"rationale": None,
}
result = underwriting_app.invoke(input_state)
print(result["decision"])
print(result["rationale"])
print(result["dscr"], result["ltv"])
If you want an LLM in the loop for narrative explanations only, keep it outside the decision path. The model should summarize why the rule engine decided what it decided; it should not be the source of truth for approve/decline logic.
Production Considerations
- •
Use durable checkpoints
- •Persist graph state with LangGraph checkpointing or your own audit store.
- •Banking workflows need replayability for disputes, QA sampling, and regulator requests.
- •
Separate PII from prompt context
- •Minimize what enters any model call.
- •Mask account numbers, SSNs/NINs, and addresses unless they are required for the specific step.
- •
Enforce data residency
- •Keep application data in-region if your bank operates under residency constraints.
- •Make sure any external model endpoint is approved for that jurisdiction.
- •
Instrument every decision path
- •Log node inputs/outputs plus reason codes.
- •Track approval rate drift by product type, geography, channel, and underwriter override rate.
Common Pitfalls
- •
Letting the LLM make final credit decisions
- •Avoid this by making policy checks deterministic and using the model only for summarization or document extraction.
- •Final disposition should come from explicit rules or approved scoring models.
- •
Returning unstructured text instead of typed outputs
- •This breaks auditability and makes downstream systems brittle.
- •Use typed state fields like
decision,dscr,ltv, andrationale.
- •
Ignoring compliance branches
- •A sanctions hit or unsupported jurisdiction must short-circuit into decline or manual review immediately.
- •Put these checks early in the graph so they cannot be overridden by later nodes.
A good underwriting agent is not “smart” in the vague sense. It is controlled: deterministic where regulation demands it, traceable where auditors care about evidence, and flexible only in places where automation does not change credit policy.
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