How to Build a underwriting Agent Using LangGraph in Python for lending
An underwriting agent for lending takes a loan application, gathers the needed facts, checks policy and risk rules, and returns a decision path: approve, decline, or route to manual review. It matters because lending decisions need consistency, auditability, and speed, and a graph-based agent gives you deterministic control over each step instead of letting an LLM freestyle through regulated workflows.
Architecture
- •Application intake node
- •Normalizes borrower data, loan amount, income, liabilities, collateral, and requested product.
- •Document extraction node
- •Pulls structured fields from pay stubs, bank statements, tax forms, or business financials.
- •Policy/rules node
- •Applies hard constraints like minimum DSCR, max DTI, residency rules, and prohibited-use checks.
- •Risk scoring node
- •Produces a score or band using internal models and external bureau signals.
- •Decision node
- •Converts policy outputs and risk score into
approve,decline, ormanual_review.
- •Converts policy outputs and risk score into
- •Audit/logging node
- •Persists every input, intermediate output, and final rationale for compliance review.
Implementation
1) Define the state and graph nodes
Use StateGraph with a typed state object. Keep the state explicit so every field that affects credit decisions is visible in the graph.
from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
import operator
class UnderwritingState(TypedDict):
applicant_id: str
loan_amount: float
annual_income: float
monthly_debt: float
country: str
product_type: str
docs_verified: bool
dti: float
risk_score: int
decision: Literal["approve", "decline", "manual_review"]
reasons: Annotated[list[str], operator.add]
def intake_node(state: UnderwritingState):
dti = state["monthly_debt"] * 12 / state["annual_income"]
return {"dti": dti}
def policy_node(state: UnderwritingState):
reasons = []
if state["country"] != "US":
reasons.append("Non-US residency requires manual compliance review")
if state["product_type"] == "secured" and not state["docs_verified"]:
reasons.append("Collateral docs not verified")
if state["dti"] > 0.45:
reasons.append(f"DTI too high: {state['dti']:.2f}")
return {"reasons": reasons}
def risk_node(state: UnderwritingState):
score = 720 if state["annual_income"] > 100000 else 640
return {"risk_score": score}
def decision_node(state: UnderwritingState):
if any("manual review" in r.lower() for r in state["reasons"]):
return {"decision": "manual_review"}
if state["risk_score"] >= 700 and state["dti"] <= 0.4:
return {"decision": "approve"}
return {"decision": "decline"}
2) Wire the workflow with add_node, add_edge, and add_conditional_edges
This is where LangGraph helps most. You model underwriting as a controlled process instead of one big prompt.
def build_graph():
graph = StateGraph(UnderwritingState)
graph.add_node("intake", intake_node)
graph.add_node("policy", policy_node)
graph.add_node("risk", risk_node)
graph.add_node("decision", decision_node)
graph.add_edge(START, "intake")
graph.add_edge("intake", "policy")
graph.add_edge("policy", "risk")
graph.add_edge("risk", "decision")
def route_after_decision(state: UnderwritingState):
return state["decision"]
graph.add_conditional_edges(
"decision",
route_after_decision,
{
"approve": END,
"decline": END,
"manual_review": END,
},
)
return graph.compile()
app = build_graph()
3) Run an application through the compiled app
The compiled graph exposes .invoke(). In production you’d wrap this in an API layer and persist the full state transition log.
result = app.invoke(
{
"applicant_id": "A-10021",
"loan_amount": 25000.0,
"annual_income": 98000.0,
"monthly_debt": 1200.0,
"country": "US",
"product_type": "secured",
"docs_verified": True,
"dti": 0.0,
"risk_score": 0,
"decision": "manual_review",
"reasons": [],
}
)
print(result["decision"])
print(result["reasons"])
print(result["dti"], result["risk_score"])
4) Add an LLM only where it belongs
For lending, do not let the model make the final credit call. Use it for document summarization or explanation drafting after rules have already decided the path.
A common pattern is to insert an LLM-backed node between extraction and policy review:
- •summarize income evidence from documents
- •extract missing fields into structured JSON
- •draft adverse action explanation text for compliance review
Keep that node isolated so you can test it independently and disable it without breaking core underwriting logic.
Production Considerations
- •Deployment
- •Run the graph behind a service boundary with strict schema validation on inputs.
- •Store raw application payloads and node outputs in immutable audit storage.
- •Monitoring
- •Track approval rate, manual review rate, decline reasons, model drift, and policy override frequency.
- •Alert on sudden shifts by product type, geography, or channel.
- •Guardrails
- •Hard-block unsupported jurisdictions before any model call.
- •Separate PII handling from reasoning steps; redact sensitive fields before sending text to an LLM.
- •Compliance
- •Persist rationale for every adverse decision to support fair lending reviews.
- •Version your policies so you can reproduce decisions from a specific date.
Common Pitfalls
- •
Letting the LLM decide credit outcomes
- •Avoid this by keeping approval logic in deterministic Python nodes.
- •Use the model only for extraction or explanation generation.
- •
Mixing compliance checks with scoring logic
- •Keep policy enforcement separate from risk scoring.
- •That makes audits easier and prevents “high score but illegal to book” failures.
- •
Ignoring data residency and retention rules
- •Don’t send borrower PII to unmanaged endpoints.
- •Route EU/UK data to approved regions and log where every field was processed.
- •
Building graphs without traceable outputs
- •Every node should return explicit fields like
reasons,risk_score, ordecision. - •If you can’t explain why a loan was declined in one pass through the logs, your workflow is not ready for lending.
- •Every node should return explicit fields like
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