How to Build a loan approval Agent Using LangGraph in Python for pension funds
A loan approval agent for pension funds takes an application, checks policy and risk rules, gathers missing facts, and returns a decision path: approve, reject, or escalate to a human underwriter. For pension funds, this matters because lending decisions must be explainable, auditable, and aligned with fiduciary duty, compliance rules, and strict data handling requirements.
Architecture
- •
Input normalization layer
- •Converts raw application data into a typed state object.
- •Validates required fields like borrower identity, amount, tenor, collateral, and jurisdiction.
- •
Policy evaluation node
- •Applies pension-fund-specific lending rules.
- •Checks concentration limits, prohibited sectors, minimum DSCR, LTV thresholds, and delegated authority limits.
- •
Risk scoring node
- •Produces a structured risk assessment from financial ratios and borrower history.
- •Keeps the output deterministic and machine-readable for audit.
- •
Compliance and KYC/AML gate
- •Flags missing KYC artifacts, sanctions hits, beneficial ownership gaps, or residency conflicts.
- •Forces escalation instead of silent auto-approval.
- •
Decision router
- •Uses explicit branching to send the request to approve, reject, or human review.
- •This is where LangGraph fits well: stateful control flow with clear transitions.
- •
Audit trail writer
- •Persists every node output, rule hit, and final decision.
- •Required for internal audit and regulator review.
Implementation
1) Define the graph state
Use a typed state so every node reads and writes the same contract. For production systems, keep the state small and structured; do not pass raw documents through the graph.
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
class LoanState(TypedDict):
applicant_id: str
amount: float
annual_income: float
debt_service_coverage_ratio: float
loan_to_value: float
kyc_complete: bool
sanctions_clear: bool
jurisdiction_ok: bool
risk_score: int
decision: Literal["approve", "reject", "review"]
reason: str
2) Add deterministic nodes for policy and risk
Keep business rules explicit. Pension funds need predictable outcomes that compliance teams can trace back to rule logic.
def assess_policy(state: LoanState) -> LoanState:
if not state["kyc_complete"]:
state["decision"] = "review"
state["reason"] = "KYC incomplete"
return state
if not state["sanctions_clear"]:
state["decision"] = "reject"
state["reason"] = "Sanctions screening failed"
return state
if not state["jurisdiction_ok"]:
state["decision"] = "review"
state["reason"] = "Jurisdiction requires legal review"
return state
return state
def score_risk(state: LoanState) -> LoanState:
score = 0
if state["debt_service_coverage_ratio"] >= 1.5:
score += 40
elif state["debt_service_coverage_ratio"] >= 1.2:
score += 20
if state["loan_to_value"] <= 60:
score += 40
elif state["loan_to_value"] <= 75:
score += 20
if state["annual_income"] >= (state["amount"] * 0.25):
score += 20
state["risk_score"] = score
return state
def decide(state: LoanState) -> LoanState:
if state.get("decision") in {"reject", "review"}:
return state
if state["risk_score"] >= 80:
state["decision"] = "approve"
state["reason"] = "Meets policy and risk thresholds"
elif state["risk_score"] >= 50:
state["decision"] = "review"
state["reason"] = "Borderline risk profile"
else:
state["decision"] = "reject"
state["reason"] = "Insufficient risk score"
return state
3) Wire the workflow with StateGraph
This is the core LangGraph pattern. You define nodes, connect them with edges, compile the graph once, then invoke it per application.
workflow = StateGraph(LoanState)
workflow.add_node("assess_policy", assess_policy)
workflow.add_node("score_risk", score_risk)
workflow.add_node("decide", decide)
workflow.add_edge(START, "assess_policy")
workflow.add_edge("assess_policy", "score_risk")
workflow.add_edge("score_risk", "decide")
workflow.add_edge("decide", END)
app = workflow.compile()
input_state: LoanState = {
"applicant_id": "APP-10027",
"amount": 250000.0,
"annual_income": 180000.0,
"debt_service_coverage_ratio": 1.35,
"loan_to_value": 68.0,
"kyc_complete": True,
"sanctions_clear": True,
"jurisdiction_ok": True,
"risk_score": 0,
"decision": "review",
"reason": ""
}
result = app.invoke(input_state)
print(result["decision"], result["reason"], result["risk_score"])
4) Add conditional routing for human escalation
For pension funds you usually need a human-in-the-loop path when policy is unclear or risk is borderline. LangGraph supports this cleanly with add_conditional_edges.
def route_after_policy(state: LoanState):
if not state["kyc_complete"]:
return END
if not state["sanctions_clear"]:
return END
return "score_risk"
workflow2 = StateGraph(LoanState)
workflow2.add_node("assess_policy", assess_policy)
workflow2.add_node("score_risk", score_risk)
workflow2.add_node("decide", decide)
workflow2.add_edge(START, "assess_policy")
workflow2.add_conditional_edges("assess_policy", route_after_policy)
workflow2.add_edge("score_risk", "decide")
workflow2.add_edge("decide", END)
app2 = workflow2.compile()
That pattern gives you a clear audit story:
- •policy gate first
- •risk scoring second
- •final decision last
For more complex deployments, replace direct function nodes with tool-backed nodes that fetch credit bureau data or internal portfolio exposure from approved services only.
Production Considerations
- •
Enforce data residency
- •Keep applicant PII inside approved regions.
- •If the pension fund operates across jurisdictions, route data only through region-specific deployments and log cross-border transfers explicitly.
- •
Store full decision traces
- •Persist input snapshot, node outputs, rule hits, timestamps, and final outcome.
- •Audit teams will want to reconstruct why an application was rejected or escalated months later.
- •
Add guardrails around delegated authority
- •Hard-code approval ceilings by role and fund mandate.
- •If an application exceeds threshold exposure or sector concentration limits, force
reviewregardless of model output.
- •
Monitor drift in policy outcomes
- •Track approval rates by jurisdiction, asset class, tenor band, and risk bucket.
- •A sudden shift often means upstream data quality issues or policy misconfiguration rather than real portfolio change.
Common Pitfalls
- •
Using LLMs to make the final decision
- •Don’t let a model directly approve or reject loans.
- •Use deterministic rules for final disposition; reserve LLMs for document extraction or summarization where needed.
- •
Passing unvalidated free-form inputs into the graph
- •Validate with Pydantic or strict schema checks before
invoke. - •Bad inputs create bad decisions fast; pension funds cannot tolerate silent coercion of missing values.
- •Validate with Pydantic or strict schema checks before
- •
Ignoring human review paths
- •Borderline cases need escalation lanes.
- •If your graph only has approve/reject branches, you will end up encoding ambiguous cases as false certainty.
- •
Skipping audit metadata
- •Every node should emit reasons in structured fields like
reasonorrule_hits.
- •Every node should emit reasons in structured 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