How to Build a claims processing Agent Using LangGraph in TypeScript for pension funds

By Cyprian AaronsUpdated 2026-04-21
claims-processinglanggraphtypescriptpension-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.
  • 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

  1. 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.

  2. 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.

  3. 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

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