How to Build a loan approval Agent Using LangGraph in Python for payments

By Cyprian AaronsUpdated 2026-04-21
loan-approvallanggraphpythonpayments

A loan approval agent for payments takes an application, checks the applicant against policy and risk rules, scores the request, and either approves, rejects, or escalates it for human review. In payments, this matters because bad credit decisions turn into chargebacks, compliance issues, and direct loss exposure; the workflow has to be deterministic, auditable, and easy to override.

Architecture

  • State model
    • Holds applicant data, KYC status, income, requested amount, risk score, decision, and audit trail.
  • Policy engine
    • Encodes hard rules like minimum KYC completion, debt-to-income thresholds, sanction screening flags, and country restrictions.
  • Scoring node
    • Produces a numeric risk score from application inputs and external signals.
  • Decision router
    • Routes to approve, reject, or manual_review based on policy outputs and score thresholds.
  • Audit logger
    • Captures every intermediate decision for regulators and internal controls.
  • Human review handoff
    • Sends edge cases to an operations queue with full context.

Implementation

1) Define the graph state and helper functions

Use a typed state so every node reads and writes predictable fields. For payment lending workflows, keep the state explicit; don’t pass free-form blobs around.

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END

Decision = Literal["approve", "reject", "manual_review"]

class LoanState(TypedDict):
    applicant_id: str
    country: str
    kyc_passed: bool
    income_monthly: float
    existing_obligations: float
    requested_amount: float
    risk_score: int
    decision: Decision
    reason: str
    audit_log: list[str]

def init_audit(state: LoanState) -> dict:
    return {"audit_log": [f"application_received:{state['applicant_id']}"]}

def policy_check(state: LoanState) -> dict:
    if not state["kyc_passed"]:
        return {
            "decision": "reject",
            "reason": "KYC failed",
            "audit_log": state["audit_log"] + ["policy_reject:kyc_failed"],
        }

    if state["country"] in {"IR", "KP"}:
        return {
            "decision": "reject",
            "reason": "Restricted jurisdiction",
            "audit_log": state["audit_log"] + ["policy_reject:sanctions_or_country_block"],
        }

    return {"audit_log": state["audit_log"] + ["policy_passed"]}

2) Add scoring and routing nodes

This is where LangGraph starts paying off. Each step is isolated, testable, and traceable.

def score_application(state: LoanState) -> dict:
    dti = state["existing_obligations"] / max(state["income_monthly"], 1.0)
    amount_ratio = state["requested_amount"] / max(state["income_monthly"], 1.0)

    score = 100
    if dti > 0.5:
        score -= 35
    if amount_ratio > 3:
        score -= 25
    if state["income_monthly"] < 2000:
        score -= 15

    score = max(0, min(100, score))
    return {
        "risk_score": score,
        "audit_log": state["audit_log"] + [f"scored:{score}"],
    }

def route_decision(state: LoanState) -> Decision:
    if state.get("decision") == "reject":
        return END

    if state["risk_score"] >= 80:
        return "approve"
    if state["risk_score"] >= 55:
        return "manual_review"
    return "reject"

def approve_node(state: LoanState) -> dict:
    return {
        "decision": "approve",
        "reason": f"Approved with risk score {state['risk_score']}",
        "audit_log": state["audit_log"] + ["decision_approved"],
    }

def reject_node(state: LoanState) -> dict:
    return {
        "decision": "reject",
        "reason": f"Rejected with risk score {state['risk_score']}",
        "audit_log": state["audit_log"] + ["decision_rejected"],
    }

def manual_review_node(state: LoanState) -> dict:
    return {
        "decision": "manual_review",
        "reason": f"Needs analyst review at risk score {state['risk_score']}",
        "audit_log": state["audit_log"] + ["decision_manual_review"],
    }

3) Build the LangGraph workflow

Use StateGraph, add nodes with add_node, connect them with add_edge, then use add_conditional_edges for the decision branch. This is the actual pattern you want in production.

graph = StateGraph(LoanState)

graph.add_node("init_audit", init_audit)
graph.add_node("policy_check", policy_check)
graph.add_node("score_application", score_application)
graph.add_node("approve", approve_node)
graph.add_node("reject", reject_node)
graph.add_node("manual_review", manual_review_node)

graph.add_edge(START, "init_audit")
graph.add_edge("init_audit", "policy_check")
graph.add_edge("policy_check", "score_application")

graph.add_conditional_edges(
    "score_application",
    route_decision,
    {
        "approve": "approve",
        "reject": "reject",
        "manual_review": "manual_review",
        END: END,
    },
)

graph.add_edge("approve", END)
graph.add_edge("reject", END)
graph.add_edge("manual_review", END)

app = graph.compile()

4) Run it with a real input payload

In payments systems, the input should come from a validated API boundary or event stream. Keep PII handling outside the graph when possible; only pass what each node needs.

input_state = {
    "applicant_id": "app_12345",
    "country": "ZA",
    "kyc_passed": True,
    "income_monthly": 4500.0,
    "existing_obligations": 1200.0,
    "requested_amount": 8000.0,
}

result = app.invoke(input_state)

print(result["decision"])
print(result["reason"])
print(result["risk_score"])
print(result["audit_log"])

Production Considerations

  • Compliance first
    • Log every transition with timestamps, model/version identifiers, and rule outcomes.
    • Keep an immutable audit trail for credit decisions and regulator requests.
  • Data residency
    • Route applicant data through region-bound infrastructure.
    • Don’t send PII or financial records to non-compliant services or cross-border endpoints.
  • Guardrails
    • Hard-block restricted jurisdictions before scoring.
    • Enforce minimum KYC completion and sanctions checks as deterministic rules.
  • Monitoring
    • Track approval rate by country, channel, loan size, and analyst override rate.
    • Alert on drift in rejection rates or sudden spikes in manual review volume.

Common Pitfalls

  • Mixing policy with scoring
    • Don’t let an LLM or heuristic scorer override hard compliance rules.
    • Keep sanctions/KYC/eligibility checks as deterministic nodes ahead of any probabilistic logic.
  • Weak auditability
    • If you only store final decisions, you can’t explain why a loan was approved or rejected.
    • Persist node-level outputs and graph version hashes.
  • Ignoring fallback paths
    • Every ambiguous case needs a safe route to manual review.
    • Never auto-approve when required fields are missing or external checks fail.

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