How to Build a loan approval Agent Using LangGraph in Python for retail banking
A loan approval agent automates the first-pass decisioning workflow for retail banking: it collects applicant data, validates policy rules, checks risk signals, and routes the case to approve, decline, or manual review. It matters because banks need faster turnaround without losing control over compliance, auditability, and consistent underwriting.
Architecture
- •
Input normalization layer
- •Takes raw application payloads from a web form, CRM, or core banking API.
- •Converts them into a strict schema so downstream nodes don’t guess at field names.
- •
Policy validation node
- •Checks hard rules like minimum income, DTI thresholds, age requirements, and missing documents.
- •Returns deterministic outcomes; this is not an LLM problem.
- •
Risk assessment node
- •Pulls bureau score, fraud flags, internal exposure, and affordability metrics.
- •Produces a structured risk summary that can be audited later.
- •
Decision engine node
- •Applies bank policy to the validated inputs and risk summary.
- •Outputs one of
approve,decline, ormanual_review.
- •
Audit logging node
- •Persists every state transition and decision reason.
- •Required for model governance, regulatory review, and customer dispute handling.
- •
Human review route
- •Sends borderline cases to an underwriter queue.
- •Keeps the system safe when policy confidence is low or data is incomplete.
Implementation
- •Define the state and decision schema
Use TypedDict for graph state so every node reads and writes predictable fields. In retail banking, this avoids brittle ad hoc dictionaries that break when compliance adds a new field.
from typing import TypedDict, Literal, Optional
from langgraph.graph import StateGraph, START, END
Decision = Literal["approve", "decline", "manual_review"]
class LoanState(TypedDict):
applicant_id: str
income: float
monthly_debt: float
requested_amount: float
credit_score: int
dti: Optional[float]
policy_pass: Optional[bool]
risk_band: Optional[str]
decision: Optional[Decision]
reason: Optional[str]
- •Build deterministic nodes for validation and decisioning
Keep underwriting logic explicit. Use plain Python functions for rule checks so compliance can review them line by line.
def normalize_inputs(state: LoanState) -> LoanState:
dti = state["monthly_debt"] / state["income"] if state["income"] > 0 else None
return {**state, "dti": dti}
def validate_policy(state: LoanState) -> LoanState:
policy_pass = (
state["dti"] is not None
and state["dti"] <= 0.45
and state["credit_score"] >= 620
and state["requested_amount"] <= state["income"] * 10
)
return {
**state,
"policy_pass": policy_pass,
"risk_band": "prime" if state["credit_score"] >= 720 else "near_prime" if state["credit_score"] >= 660 else "subprime",
}
def decide(state: LoanState) -> LoanState:
if not state["policy_pass"]:
return {**state, "decision": "decline", "reason": "Failed hard policy checks"}
if state["risk_band"] == "subprime":
return {**state, "decision": "manual_review", "reason": "Subprime profile requires underwriter review"}
return {**state, "decision": "approve", "reason": "Passed automated underwriting rules"}
- •Wire the graph with conditional routing
StateGraph is the right tool here because loan approvals are not linear. Some cases go straight through; others branch into manual review based on risk or missing data.
from langgraph.graph import StateGraph, START, END
def route_case(state: LoanState) -> str:
return state["decision"] or "manual_review"
builder = StateGraph(LoanState)
builder.add_node("normalize_inputs", normalize_inputs)
builder.add_node("validate_policy", validate_policy)
builder.add_node("decide", decide)
builder.add_edge(START, "normalize_inputs")
builder.add_edge("normalize_inputs", "validate_policy")
builder.add_edge("validate_policy", "decide")
builder.add_conditional_edges(
"decide",
route_case,
{
"approve": END,
"decline": END,
"manual_review": END,
},
)
graph = builder.compile()
result = graph.invoke(
{
"applicant_id": "LN-10001",
"income": 8500.0,
"monthly_debt": 2600.0,
"requested_amount": 12000.0,
"credit_score": 689,
"dti": None,
"policy_pass": None,
"risk_band": None,
"decision": None,
"reason": None,
}
)
print(result["decision"], result["reason"])
- •Add auditability before production use
For retail banking, every decision needs traceability. Store input snapshot, computed DTI, rule outcome, final decision, timestamp, and reviewer handoff in your database or event stream.
A practical pattern is to persist the final LoanState plus metadata from your API layer:
- •request ID
- •customer ID
- •model/version used
- •policy version used
- •operator override if any
That gives you reproducibility when auditors ask why a customer was declined three months later.
Production Considerations
- •
Deployment isolation
- •Keep the agent in a private VPC or on-prem environment if your data residency rules require it.
- •Do not send PII to external LLM APIs unless your legal team has signed off on transfer controls.
- •
Monitoring
- •Track approval rate by product segment, decline reasons, manual review rate, and drift in credit-score distribution.
- •Alert when policy-pass rates shift suddenly; that often means a bad upstream feed or a rule regression.
- •
Guardrails
- •Hard-code non-negotiable policy thresholds outside the model path.
- •Require human approval for borderline cases like thin-file applicants or inconsistent income documents.
- •
Audit logging
- •Log every node output with immutable timestamps.
- •Include policy versioning so you can prove which rule set made the decision.
Common Pitfalls
- •
Using an LLM to make the actual credit decision
- •Don’t do this.
- •Use LLMs only for document extraction or summarization; keep approval logic deterministic and testable.
- •
Skipping schema enforcement
- •If you let free-form JSON flow through the graph, one malformed payload will break underwriting.
- •Define explicit types with
TypedDictor Pydantic models at the edge of the system.
- •
Ignoring explainability
- •“Model said no” is not acceptable in retail banking.
- •Always return a reason code mapped to policy language like
DTI_TOO_HIGH,LOW_CREDIT_SCORE, orMANUAL_REVIEW_REQUIRED.
- •
Not versioning policies
- •A loan approved under one rule set may be declined under another.
- •Version both code and underwriting rules together so audit teams can reproduce outcomes exactly.
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