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

By Cyprian AaronsUpdated 2026-04-21
claims-processinglanggraphtypescripthealthcare

A claims processing agent in healthcare takes a claim, checks completeness, validates policy and coding rules, routes exceptions, and prepares a decision package for downstream systems or human review. It matters because claims are high-volume, regulated, and expensive to process manually; the agent reduces turnaround time while keeping auditability, compliance, and patient data handling intact.

Architecture

  • Claim intake node

    • Normalizes inbound claim payloads from EHR, clearinghouse, or payer APIs.
    • Validates required fields like member ID, CPT/HCPCS codes, diagnosis codes, provider ID, dates of service.
  • Policy and coverage checker

    • Verifies eligibility, benefits, prior authorization requirements, and plan-specific exclusions.
    • Calls internal policy services or payer rule engines.
  • Coding and medical necessity validator

    • Checks for ICD/CPT mismatches, modifier issues, duplicate billing patterns, and missing documentation flags.
    • Produces structured reasons for denial or manual review.
  • Exception routing node

    • Sends ambiguous claims to human adjudication.
    • Escalates cases with missing clinical evidence or suspected fraud/waste/abuse signals.
  • Decision composer

    • Builds an auditable decision object: approve, pend, deny, or route to review.
    • Includes rationale, rule references, timestamps, and source system IDs.
  • Audit and persistence layer

    • Stores every state transition for compliance review.
    • Supports retention policies and data residency constraints by region.

Implementation

1) Define the graph state and typed outputs

In LangGraph for TypeScript, keep the state small and explicit. For healthcare claims processing, that means separating raw claim input from derived decision fields so you can audit each step cleanly.

import { Annotation } from "@langchain/langgraph";

export type Claim = {
  claimId: string;
  memberId: string;
  providerId: string;
  cptCodes: string[];
  icd10Codes: string[];
  dateOfService: string;
  amount: number;
};

export type ClaimDecision = {
  status: "approve" | "pend" | "deny" | "manual_review";
  reasons: string[];
};

export const ClaimState = Annotation.Root({
  claim: Annotation<Claim>(),
  eligibilityOk: Annotation<boolean>({ default: () => false }),
  codingOk: Annotation<boolean>({ default: () => false }),
  decision: Annotation<ClaimDecision | null>({ default: () => null }),
});

This pattern keeps your graph deterministic. You can log the full state after each node without mixing in transient LLM chatter.

2) Build validation nodes with StateGraph

Use StateGraph when your workflow is mostly deterministic with conditional branching. That fits claims processing better than a free-form agent loop.

import { StateGraph, START, END } from "@langchain/langgraph";

const validateEligibility = async (state: typeof ClaimState.State) => {
  const eligible = state.claim.memberId.startsWith("M") && state.claim.amount < 10000;

  return {
    eligibilityOk: eligible,
    decision: eligible
      ? null
      : { status: "deny", reasons: ["Member not eligible or claim exceeds threshold"] },
  };
};

const validateCoding = async (state: typeof ClaimState.State) => {
  const hasRequiredCodes = state.claim.cptCodes.length > 0 && state.claim.icd10Codes.length > 0;

  return {
    codingOk: hasRequiredCodes,
    decision: hasRequiredCodes
      ? null
      : { status: "manual_review", reasons: ["Missing CPT or ICD-10 codes"] },
  };
};

const composeDecision = async (state: typeof ClaimState.State) => {
  if (state.decision) return {};

  if (state.eligibilityOk && state.codingOk) {
    return {
      decision: {
        status: "approve",
        reasons: ["Eligibility verified", "Coding validated"],
      },
    };
  }

  return {
    decision: {
      status: "manual_review",
      reasons: ["One or more checks failed"],
    },
  };
};

const graph = new StateGraph(ClaimState)
  .addNode("validateEligibility", validateEligibility)
  .addNode("validateCoding", validateCoding)

Continue the graph wiring with conditional routing:

type Route = "validateCoding" | "composeDecision" | "__end__";

const routeAfterEligibility = (state: typeof ClaimState.State): Route => {
  if (state.decision?.status === "deny") return "__end__";
  return "validateCoding";
};

graph
  .addNode("composeDecision", composeDecision)
  .addEdge(START, "validateEligibility")
  .addConditionalEdges("validateEligibility", routeAfterEligibility)

Finish it by linking the remaining edge and compiling:

graph
  .addEdge("validateCoding", "composeDecision")
  .addEdge("composeDecision", END);

export const claimsApp = graph.compile();

3) Invoke the workflow with a real claim payload

At runtime you pass a typed state object into invoke(). The result contains the final state after all transitions.

const result = await claimsApp.invoke({
    claim:
      {
        claimId:
          "CLM-10001",
        memberId:
          "M123456",
        providerId:
          "P9001",
        cptCodes:
          ["99213"],
        icd10Codes:
          ["E11.9"],
        dateOfService:
          "2026-04-01",
        amount:
         300,
      },
});

console.log(result.decision);

For healthcare deployments, this is where you’d attach request metadata such as tenant ID, region, and trace ID outside the PHI payload. Keep those values in your transport layer or execution context so they don’t pollute clinical data records.

Production Considerations

  • Audit every node transition

    • Persist input state, output patch, timestamp, and rule version for each node.
    • Healthcare audits need explainability down to the exact policy check that triggered a denial or manual review.
  • Enforce PHI boundaries

    • Redact unnecessary identifiers before sending data to model-backed nodes.
    • If you use any hosted LLM component for summarization or exception triage, make sure your BAA covers it and that PHI stays within approved regions.
  • Add hard guardrails before model calls

    • Validate schema first with Zod or equivalent.
    • Never let an LLM decide final payment outcomes without deterministic rules around eligibility, coding validity, and fraud thresholds.
  • Design for residency and tenant isolation

    • Pin workloads to approved cloud regions.
    • Separate queues and storage by payer or health system tenant when data residency requirements differ across contracts.

Common Pitfalls

  1. Treating the agent like a chatbot

    • Claims processing is workflow automation with strict outputs.
    • Avoid open-ended prompts for core adjudication; use typed state and deterministic nodes first.
  2. Mixing PHI into logs and traces

    • Developers often dump full request bodies into observability tools.
    • Strip member names, diagnosis text notes, and attachment content before logging; keep only IDs and hashes where possible.
  3. Skipping exception paths

    • If your graph only handles approve/deny flows, real-world edge cases will break it.
    • Add a manual review branch for missing documentation, conflicting codes, prior auth gaps, and policy ambiguity.
  4. Ignoring versioning of rules

    • Claims decisions must be reproducible months later.
    • Store policy version IDs alongside every decision so you can explain why a claim was processed under a specific rule set.

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