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

By Cyprian AaronsUpdated 2026-04-21
compliance-checkinglanggraphpythonbanking

A compliance checking agent for banking takes a request, inspects it against policy rules, flags violations, and produces an auditable decision trail. It matters because banks need consistent enforcement of AML, KYC, sanctions, disclosures, and internal policy without letting a model make free-form decisions that can’t be explained later.

Architecture

Build this agent as a small graph, not a monolith:

  • Input normalizer

    • Cleans and structures the user request or case payload.
    • Extracts entities like customer name, transaction amount, country, product type, and requested action.
  • Policy retrieval node

    • Pulls the relevant compliance rules from an internal policy store.
    • Keeps jurisdiction-specific logic separate from application code.
  • Rule evaluation node

    • Applies deterministic checks first.
    • Uses LLM reasoning only where policy text needs interpretation or summarization.
  • Risk classification node

    • Assigns outcomes like approve, review, or reject.
    • Produces a confidence score and reason codes.
  • Audit trail node

    • Writes every decision input/output to an immutable log.
    • Captures model version, policy version, timestamp, and reviewer overrides.
  • Human escalation path

    • Routes ambiguous cases to a compliance analyst.
    • Prevents auto-approval when thresholds are exceeded.

Implementation

1) Define the graph state and helper functions

Use a typed state object so every node gets the same contract. In banking, this is where you keep the data shape explicit for auditability.

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

class ComplianceState(TypedDict):
    request: dict
    policy_text: str
    findings: list[str]
    decision: Literal["approve", "review", "reject"]
    rationale: str
    audit_log: list[dict]

def load_policy(request: dict) -> str:
    country = request.get("country", "unknown")
    product = request.get("product", "general")
    return f"Policy for {country} / {product}: reject sanctioned jurisdictions; review high-value transfers; require KYC for new accounts."

def evaluate_rules(state: ComplianceState) -> dict:
    req = state["request"]
    findings = []

    if req.get("country") in {"IR", "KP", "SY"}:
        findings.append("Sanctions risk: restricted jurisdiction")
    if req.get("amount", 0) >= 100000:
        findings.append("High-value transfer requires enhanced review")
    if req.get("kyc_status") != "verified":
        findings.append("KYC incomplete")

    decision = "approve" if not findings else ("reject" if any("Sanctions risk" in f for f in findings) else "review")
    return {"findings": findings, "decision": decision}

2) Add nodes with StateGraph and wire the flow

This is the actual LangGraph pattern you want in production. The graph stays deterministic at the edges and only uses model logic where needed.

from langchain_core.runnables import RunnableLambda

def normalize_input(state: ComplianceState) -> dict:
    req = state["request"]
    normalized = {
        **req,
        "amount": float(req.get("amount", 0)),
        "country": str(req.get("country", "")).upper(),
        "product": str(req.get("product", "")).lower(),
        "kyc_status": str(req.get("kyc_status", "")).lower(),
    }
    return {"request": normalized}

def attach_policy(state: ComplianceState) -> dict:
    return {"policy_text": load_policy(state["request"])}

def write_audit(state: ComplianceState) -> dict:
    entry = {
        "customer_id": state["request"].get("customer_id"),
        "decision": state["decision"],
        "findings": state["findings"],
        "policy_version": "2026.04",
    }
    return {"audit_log": [entry]}

graph = StateGraph(ComplianceState)

graph.add_node("normalize_input", normalize_input)
graph.add_node("attach_policy", attach_policy)
graph.add_node("evaluate_rules", evaluate_rules)
graph.add_node("write_audit", write_audit)

graph.add_edge(START, "normalize_input")
graph.add_edge("normalize_input", "attach_policy")
graph.add_edge("attach_policy", "evaluate_rules")
graph.add_edge("evaluate_rules", "write_audit")
graph.add_edge("write_audit", END)

app = graph.compile()

3) Run the agent on a real case payload

Keep the output structured. Banking teams need reason codes they can store in case management systems and expose to auditors.

case = {
    "customer_id": "CUST-10021",
    "amount": 250000,
    "country": "GB",
    "product": "wire_transfer",
    "kyc_status": "verified",
}

result = app.invoke({
    "request": case,
    "policy_text": "",
    "findings": [],
    "decision": "review",
    "rationale": "",
    "audit_log": [],
})

print(result["decision"])
print(result["findings"])
print(result["audit_log"])

4) Add conditional escalation for ambiguous cases

This is where LangGraph earns its keep. Use add_conditional_edges when you need routing based on outcome rather than linear flow.

def route_case(state: ComplianceState) -> str:
    if state["decision"] == "reject":
        return END
    if state["decision"] == "review":
        return END
    return END

# Example only: replace END with human-review node in real deployments.

In a production setup, route review cases to a human approval queue or a secondary analysis node that summarizes why the case needs attention.

Production Considerations

  • Deploy with strict data residency boundaries

    • Keep customer data in-region.
    • If you call an LLM API, ensure the provider supports your regulatory region and retention terms.
  • Store full audit context

    • Persist input payload hash, policy version, graph version, model version, and final decision.
    • Make logs immutable or append-only for regulatory review.
  • Use deterministic guardrails before any model call

    • Sanctions lists, threshold checks, PEP flags, and KYC status should be rule-based.
    • Never let the model override hard rejects.
  • Monitor drift by outcome class

    • Track approve/review/reject rates by product line and geography.
    • Spikes often mean policy changes or bad upstream data.

Common Pitfalls

  • Letting the LLM make final compliance decisions

    • Avoid this by making rule evaluation deterministic first.
    • Use the model for explanation or summarization, not authoritative approval.
  • Skipping policy versioning

    • If you don’t store which policy text was used, you can’t defend decisions later.
    • Version policies exactly like code releases.
  • Mixing personal data into prompts without controls

    • Mask account numbers, IDs, and free-text fields before sending anything to a model.
    • Apply field-level redaction and keep sensitive records inside your bank boundary.

A good compliance agent is boring by design. It should be traceable, predictable, and easy to defend when audit asks why a transaction was blocked or escalated.


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