How to Build a claims processing Agent Using LangGraph in TypeScript for pension funds
A claims processing agent for pension funds takes a member’s claim, validates the documents, checks policy and eligibility rules, routes exceptions, and prepares a decision package for human review or downstream systems. It matters because pension claims are high-trust workflows: mistakes affect retirement income, compliance exposure, and auditability.
Architecture
- •
Ingress API
- •Receives claim payloads from portals, case management systems, or batch jobs.
- •Normalizes member identity, claim type, jurisdiction, and attached documents.
- •
Document extraction layer
- •Pulls structured fields from PDFs, scanned forms, death certificates, ID docs, and bank letters.
- •Keeps extracted values separate from source evidence for audit.
- •
Rules and eligibility engine
- •Checks pension-specific constraints:
- •membership status
- •vesting rules
- •beneficiary hierarchy
- •age thresholds
- •residency/jurisdiction constraints
- •Produces deterministic decisions where possible.
- •Checks pension-specific constraints:
- •
Exception triage node
- •Detects missing documents, conflicting dates, duplicate claims, or suspicious patterns.
- •Routes cases to human ops when confidence is low.
- •
Audit trail store
- •Persists every state transition, rule result, document reference, and model output.
- •Supports regulator reviews and internal controls.
- •
Decision publisher
- •Sends approved claims to the payment or case management system.
- •Emits rejection or escalation packets with reasons attached.
Implementation
1. Define the claim state and validation helpers
Use a typed graph state so every node reads and writes predictable fields. For pension workflows, keep raw evidence references alongside derived decisions so you can reconstruct the case later.
import { Annotation } from "@langchain/langgraph";
export type ClaimDoc = {
id: string;
type: "application" | "id" | "death_certificate" | "bank_proof" | "other";
uri: string;
};
export type ClaimState = typeof ClaimState.State;
export const ClaimState = Annotation.Root({
claimId: Annotation<string>(),
memberId: Annotation<string>(),
jurisdiction: Annotation<string>(),
docs: Annotation<ClaimDoc[]>(),
extracted: Annotation<Record<string, unknown>>(),
eligibility: Annotation<{
passed: boolean;
reasons: string[];
confidence: number;
}>(),
decision: Annotation<"approve" | "reject" | "review">(),
auditTrail: Annotation<string[]>(),
});
2. Add deterministic nodes for extraction, eligibility, and routing
Keep the first pass deterministic. In pension funds, you want rules to decide as much as possible before any model-driven step is involved.
import { StateGraph, START, END } from "@langchain/langgraph";
const extractNode = async (state: ClaimState) => {
const extracted = {
hasDeathCertificate: state.docs.some((d) => d.type === "death_certificate"),
hasBankProof: state.docs.some((d) => d.type === "bank_proof"),
docCount: state.docs.length,
};
return {
extracted,
auditTrail: [
...(state.auditTrail ?? []),
`Extracted ${JSON.stringify(extracted)}`,
],
};
};
const eligibilityNode = async (state: ClaimState) => {
const reasons: string[] = [];
if (!state.extracted?.["hasDeathCertificate"]) reasons.push("Missing death certificate");
if (!state.extracted?.["hasBankProof"]) reasons.push("Missing bank proof");
if (state.jurisdiction !== "ZA") reasons.push("Jurisdiction not supported");
const passed = reasons.length === 0;
return {
eligibility: {
passed,
reasons,
confidence: passed ? 0.98 : 0.72,
},
decision: passed ? "approve" : reasons.length > 1 ? "review" : "reject",
auditTrail: [
...(state.auditTrail ?? []),
`Eligibility checked with result=${passed}`,
],
};
};
const routeNode = async (state: ClaimState) => {
if (state.decision === "approve") return { auditTrail: [...state.auditTrail!, "Routed to payment"] };
if (state.decision === "review") return { auditTrail: [...state.auditTrail!, "Routed to human review"] };
return { auditTrail: [...state.auditTrail!, "Rejected with reasons attached"] };
};
3. Wire the graph with conditional routing
This pattern gives you a clear control flow and keeps the graph explainable. That matters when compliance asks why a pension claim was rejected.
const graph = new StateGraph(ClaimState)
.addNode("extract", extractNode)
.addNode("eligibility", eligibilityNode)
.addNode("route", routeNode)
.addEdge(START, "extract")
.addEdge("extract", "eligibility")
.addConditionalEdges("eligibility", (state) => {
if (state.decision === "approve") return "route";
if (state.decision === "review") return "route";
return END;
})
.addEdge("route", END);
export const compiledGraph = graph.compile();
4. Invoke the workflow from your service layer
In production you’ll call this from an API handler or queue consumer. Persist inputs and outputs before and after execution so you have a full trace for audits.
const result = await compiledGraph.invoke({
claimId: "CLM-100245",
memberId: "M-88321",
jurisdiction: "ZA",
docs: [
{ id: "doc-1", type: "application", uri: "/vault/applications/doc-1.pdf" },
{ id: "doc-2", type: "death_certificate", uri: "/vault/certs/doc-2.pdf" },
{ id: "doc-3", type: "bank_proof", uri: "/vault/bank/doc-3.pdf" },
],
extracted:
{},
eligibility:
undefined as any,
decision:
undefined as any,
auditTrail:
[],
});
console.log(result.decision);
console.log(result.auditTrail);
Production Considerations
- •Deploy in-region
Put execution close to your data residency boundary. Pension data often cannot leave a specific country or cloud region without legal review.
- •Store immutable audit logs
Write every input document hash, node output, decision reason, and operator override to WORM-capable storage. Regulators care about traceability more than model sophistication.
- •Add guardrails before any model call
If you introduce LLM-based extraction or summarization later, constrain it behind rules:
- •
never invent missing dates
- •
never infer beneficiary identity without source evidence
- •
always surface low-confidence cases to humans
- •
Monitor exception rates by claim type
Track rejection rate, manual review rate, average handling time, and override frequency by jurisdiction. A spike in one segment usually means a rule regression or upstream document quality issue.
Common Pitfalls
- •
Letting the model make final eligibility decisions
Don’t do this for pension claims. Use deterministic rules for entitlement checks and reserve models for extraction or summarization only.
- •
Dropping source evidence after extraction
If you only store normalized fields, you lose defensibility during audits. Keep document URIs, hashes, and extracted field provenance attached to the case.
- •
Ignoring jurisdiction-specific rules
Pension processing changes across countries and even fund schemes within the same country. Encode jurisdiction into the graph state early so routing and validation stay explicit.
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