How to Build a compliance checking Agent Using LangGraph in Python for banking
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, orreject. - •Produces a confidence score and reason codes.
- •Assigns outcomes like
- •
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
- •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