How to Build a claims processing Agent Using LangGraph in Python for payments
A claims processing agent for payments takes an incoming claim, validates the payload, checks policy and payment rules, routes exceptions, and either approves, rejects, or escalates for human review. It matters because claims are where money moves, so every decision needs traceability, deterministic controls, and a clean audit trail.
Architecture
- •
Ingress validator
- •Normalizes claim input from API, queue, or webhook.
- •Rejects malformed requests before any model call or payment lookup.
- •
State object
- •Holds claim metadata, extracted entities, validation results, decision status, and audit events.
- •In LangGraph, this is usually a
TypedDictor Pydantic model passed between nodes.
- •
Rule engine node
- •Applies deterministic checks first: policy active, coverage limits, duplicate claim detection, sanctions flags, payout thresholds.
- •This should run before any LLM-based reasoning.
- •
LLM extraction / triage node
- •Uses an LLM only for unstructured text extraction or classification.
- •Keeps the model out of final approval logic.
- •
Human review branch
- •Routes edge cases to ops or compliance when confidence is low or rules fail.
- •Necessary for regulated payment workflows.
- •
Decision/audit sink
- •Persists the final decision plus reasons, timestamps, and node outputs.
- •Supports compliance review, dispute handling, and replay.
Implementation
1) Define state and deterministic helpers
Start with a small state shape. Keep it explicit so every node writes predictable fields and your audit trail stays readable.
from typing import TypedDict, Literal, Optional
from langgraph.graph import StateGraph, START, END
class ClaimState(TypedDict):
claim_id: str
customer_id: str
amount: float
currency: str
description: str
risk_flag: bool
validated: bool
decision: Optional[Literal["approve", "reject", "review"]]
reason: str
def validate_claim(state: ClaimState) -> ClaimState:
if state["amount"] <= 0:
return {**state,
"validated": False,
"decision": "reject",
"reason": "Invalid claim amount"}
if state["currency"] not in {"USD", "EUR", "GBP"}:
return {**state,
"validated": False,
"decision": "reject",
"reason": "Unsupported settlement currency"}
return {**state,
"validated": True,
"reason": "Basic validation passed"}
def risk_check(state: ClaimState) -> ClaimState:
high_value = state["amount"] > 10000
suspicious_text = any(word in state["description"].lower() for word in ["urgent", "cash", "refund"])
return {**state,
"risk_flag": high_value or suspicious_text}
This is the pattern you want in payments systems: deterministic prechecks first. If validation fails, do not continue into model-driven branches.
2) Add an LLM triage node only where it helps
Use the model to classify ambiguous descriptions or extract missing context. Don’t ask it to approve payments directly; let it produce structured signal that downstream rules can use.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def llm_triage(state: ClaimState) -> ClaimState:
prompt = (
f"Classify this payment claim as low_risk or needs_review.\n"
f"Claim ID: {state['claim_id']}\n"
f"Amount: {state['amount']} {state['currency']}\n"
f"Description: {state['description']}"
)
response = llm.invoke(prompt)
text = response.content.lower()
needs_review = "needs_review" in text or state["risk_flag"]
return {**state,
"decision": "review" if needs_review else "approve",
"reason": f"LLM triage result: {response.content}"}
The key point is that the LLM is advisory. Final payout logic still belongs to deterministic code and policy checks.
3) Build the graph with conditional routing
This is where LangGraph fits well. You define nodes with StateGraph, then route based on state using add_conditional_edges.
def route_after_validation(state: ClaimState):
if not state["validated"]:
return END
if state["risk_flag"]:
return "llm_triage"
return END
graph = StateGraph(ClaimState)
graph.add_node("validate_claim", validate_claim)
graph.add_node("risk_check", risk_check)
graph.add_node("llm_triage", llm_triage)
graph.add_edge(START, "validate_claim")
graph.add_edge("validate_claim", "risk_check")
graph.add_conditional_edges("risk_check", route_after_validation)
app = graph.compile()
initial_state = {
"claim_id": "CLM-1001",
"customer_id": "CUST-42",
"amount": 12500.0,
"currency": "USD",
"description": "Urgent refund request for damaged shipment",
"risk_flag": False,
"validated": False,
"decision": None,
"reason": ""
}
result = app.invoke(initial_state)
print(result)
That graph gives you a simple but production-shaped flow:
| Condition | Path |
|---|---|
| Invalid payload | Reject at validation |
| Low-risk claim | End after rule checks |
| High-risk / ambiguous claim | Send to LLM triage |
4) Add human review as a hard stop for regulated cases
For payment claims above thresholds or flagged by compliance rules, route to review instead of auto-settlement. In practice this means your graph can emit a review decision and persist the case for an operator queue.
def finalize_decision(state: ClaimState) -> ClaimState:
if state["decision"] == "approve":
reason = f"Approved for settlement. {state['reason']}"
elif state["decision"] == "review":
reason = f"Sent to human review. {state['reason']}"
else:
reason = state["reason"]
return {**state, "reason": reason}
graph2 = StateGraph(ClaimState)
graph2.add_node("validate_claim", validate_claim)
graph2.add_node("risk_check", risk_check)
graph2.add_node("llm_triage", llm_triage)
graph2.add_node("finalize_decision", finalize_decision)
graph2.add_edge(START, "validate_claim")
graph2.add_edge("validate_claim", "risk_check")
graph2.add_conditional_edges(
"risk_check",
lambda s: END if not s["validated"] else ("llm_triage" if s["risk_flag"] else END)
)
graph2.add_edge("llm_triage", "finalize_decision")
graph2.add_edge("finalize_decision", END)
app2 = graph2.compile()
Production Considerations
- •
Persist every node output
- •Store input state, node transitions, final decision, and model responses.
- •Payments teams need auditability for disputes, regulator requests, and internal control testing.
- •
Keep PII and payment data scoped
- •Redact account numbers, addresses, and sensitive identifiers before sending anything to an LLM.
- •Enforce data residency by running inference in-region when claims contain regulated customer data.
- •
Use strict approval thresholds
- •Auto-settle only under tightly bounded conditions.
- •Anything involving high value amounts, sanctions risk, unusual geographies, or duplicate detection should go to review.
- •
Add observability around branch rates
- •Track reject rate, review rate, average time-to-decision, and manual override rate.
- •A sudden spike in reviews usually means a rule drift problem or upstream data quality issue.
Common Pitfalls
- •
Letting the LLM make the payment decision
- •Bad idea. Use the model for extraction or triage only.
- •Avoid this by keeping approval/rejection logic in deterministic nodes with explicit thresholds.
- •
Skipping audit fields in state
- •If you don’t carry reasons through the graph, you lose explainability fast.
- •Fix this by writing
reason, timestamps, and branch decisions into the shared state on every step.
- •
Ignoring jurisdictional constraints
- •Claims may contain personal data that cannot leave a specific region.
- •Solve this by pinning deployment regions per market and routing sensitive workflows to compliant infrastructure only.
- •
No fallback path for low-confidence cases
- •If your graph has no human-review branch, edge cases will either fail open or fail closed incorrectly.
- •Add an explicit
reviewoutcome and integrate it with your ops queue from day one.
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