How to Build a compliance checking Agent Using LangGraph in Python for lending

By Cyprian AaronsUpdated 2026-04-21
compliance-checkinglanggraphpythonlending

A compliance checking agent for lending reviews an application, pulls the relevant policy rules, checks the applicant data against those rules, and returns a decision with an audit trail. It matters because lending teams need consistent decisions, defensible explanations, and a clean record for regulators when a loan is approved, referred, or rejected.

Architecture

  • Input normalization layer

    • Converts raw application payloads into a strict schema.
    • Validates fields like income, debt-to-income ratio, employment status, and jurisdiction.
  • Policy retrieval node

    • Fetches the current lending policy version.
    • Pulls product-specific rules such as minimum credit score, max LTV, or prohibited geographies.
  • Compliance evaluation node

    • Applies deterministic checks first.
    • Uses an LLM only for explanation drafting, not for deciding whether a rule passed.
  • Decision router

    • Routes to approve, refer, or reject based on rule outcomes.
    • Keeps decisions deterministic and easy to audit.
  • Audit logging node

    • Persists the full state: input, rules used, checks run, timestamps, and final decision.
    • Stores immutable records for model risk and regulatory review.
  • Human review handoff

    • Escalates edge cases like missing documents, sanctions hits, or conflicting data.
    • Lets compliance officers override with reason codes.

Implementation

1) Define the state and rule checks

Use a typed state so every node reads and writes predictable fields. In lending, that matters because auditability breaks fast when your graph state is loose and inconsistent.

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

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

class LendingState(TypedDict):
    applicant_id: str
    jurisdiction: str
    income: float
    debt: float
    credit_score: int
    ltv: float
    policy_version: str
    findings: list[str]
    decision: Optional[Decision]
    rationale: Optional[str]

def normalize_input(state: LendingState) -> LendingState:
    findings = []
    if state["income"] <= 0:
        findings.append("Invalid income")
    if state["debt"] < 0:
        findings.append("Invalid debt")
    return {**state, "findings": findings}

2) Add deterministic compliance checks

Do not let the LLM decide whether a borrower passes policy. Use code for the actual rule evaluation so the outcome is stable across runs.

def evaluate_policy(state: LendingState) -> LendingState:
    findings = list(state["findings"])

    if state["credit_score"] < 620:
        findings.append("Credit score below minimum threshold")
    if state["ltv"] > 0.80:
        findings.append("LTV above maximum threshold")
    dti = state["debt"] / state["income"]
    if dti > 0.43:
        findings.append(f"DTI too high: {dti:.2f}")

    return {**state, "findings": findings}

def route_decision(state: LendingState) -> Decision:
    if any("below minimum" in f or "above maximum" in f or "too high" in f for f in state["findings"]):
        return "reject"
    if state["findings"]:
        return "refer"
    return "approve"

3) Build the LangGraph workflow

This is the core pattern. StateGraph gives you explicit nodes and edges; add_conditional_edges makes routing readable; compile() produces the runnable graph you deploy behind an API.

from langgraph.graph import StateGraph, START, END

def draft_rationale(state: LendingState) -> LendingState:
    if state["decision"] == "approve":
        rationale = "Application passed all automated lending compliance checks."
    elif state["decision"] == "refer":
        rationale = f"Manual review required due to: {', '.join(state['findings'])}"
    else:
        rationale = f"Rejected due to policy failures: {', '.join(state['findings'])}"
    return {**state, "rationale": rationale}

def build_graph():
    graph = StateGraph(LendingState)

    graph.add_node("normalize_input", normalize_input)
    graph.add_node("evaluate_policy", evaluate_policy)
    graph.add_node("draft_rationale", draft_rationale)

    graph.add_edge(START, "normalize_input")
    graph.add_edge("normalize_input", "evaluate_policy")
    graph.add_conditional_edges(
        "evaluate_policy",
        route_decision,
        {
            "approve": "draft_rationale",
            "refer": "draft_rationale",
            "reject": "draft_rationale",
        },
    )
    graph.add_edge("draft_rationale", END)

    return graph.compile()

app = build_graph()

result = app.invoke({
    "applicant_id": "LN-10021",
    "jurisdiction": "US-NY",
    "income": 120000.0,
    "debt": 30000.0,
    "credit_score": 640,
    "ltv": 0.72,
    "policy_version": "2026.01",
    "findings": [],
    "decision": None,
    "rationale": None,
})

print(result["decision"])
print(result["rationale"])

4) Add human review and audit capture

For lending workflows, you usually want a refer path that creates an exception case instead of forcing a binary outcome. Keep the audit payload separate from the decision logic so your logs reflect what happened without changing behavior.

def attach_decision(state: LendingState) -> LendingState:
    decision = route_decision(state)
    

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