How to Build a fraud detection Agent Using LangGraph in TypeScript for healthcare

By Cyprian AaronsUpdated 2026-04-21
fraud-detectionlanggraphtypescripthealthcare

A healthcare fraud detection agent flags suspicious claims, referrals, and billing patterns before they turn into financial leakage or compliance incidents. It matters because healthcare data is sensitive, regulated, and expensive to investigate manually; the agent needs to triage risk fast, preserve an audit trail, and route borderline cases to humans with enough context to act.

Architecture

  • Claim intake node

    • Accepts claim metadata, provider details, CPT/ICD codes, amount, location, and timestamps.
    • Normalizes input into a typed state object.
  • Policy and compliance rules node

    • Applies deterministic checks for things like duplicate claims, impossible service dates, out-of-network anomalies, and missing authorization.
    • Keeps hard rules separate from LLM reasoning.
  • Risk scoring node

    • Combines rule outputs with model-generated signals.
    • Produces a risk score and structured explanation.
  • Investigation routing node

    • Sends low-risk claims to auto-approve or queue.
    • Sends high-risk claims to manual review with supporting evidence.
  • Audit logging node

    • Writes every decision, score, and reason code to an immutable log.
    • Supports HIPAA-style traceability and internal audit requirements.
  • Human-in-the-loop review path

    • Allows investigators to override decisions.
    • Captures reviewer notes for future tuning.

Implementation

1) Define the state and build deterministic checks first

For healthcare fraud detection, start with explicit state. Don’t let the graph pass around untyped blobs; you want every node to know exactly what it can read and write.

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

export type Claim = {
  claimId: string;
  patientId: string;
  providerId: string;
  cptCode: string;
  icd10Code: string;
  amount: number;
  serviceDate: string;
  submissionDate: string;
  locationState: string;
};

export const FraudState = Annotation.Root({
  claim: Annotation<Claim>(),
  ruleFlags: Annotation<string[]>({ default: () => [] }),
  riskScore: Annotation<number>({ default: () => 0 }),
  decision: Annotation<"approve" | "review" | "deny">(),
  rationale: Annotation<string>({ default: () => "" }),
});

Now add a pure rule function. This keeps obvious fraud signals out of the model path.

function applyHealthcareRules(claim: Claim): string[] {
  const flags: string[] = [];
  const serviceDate = new Date(claim.serviceDate);
  const submissionDate = new Date(claim.submissionDate);

  if (submissionDate < serviceDate) flags.push("submission_before_service");
  if (claim.amount > 10000) flags.push("high_amount");
  if (claim.cptCode === "99285" && claim.amount < 50) flags.push("billing_anomaly");
  if (!claim.locationState) flags.push("missing_location_state");

  return flags;
}

2) Add LangGraph nodes for scoring and routing

Use StateGraph for orchestration. In TypeScript, the graph should stay simple: one node enriches state, one scores it, one routes it.

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

const enrichNode = async (state: typeof FraudState.State) => {
  const ruleFlags = applyHealthcareRules(state.claim);
  return { ruleFlags };
};

const scoreNode = async (state: typeof FraudState.State) => {
  let score = state.ruleFlags.length * 20;

  if (state.ruleFlags.includes("high_amount")) score += 25;
  if (state.ruleFlags.includes("billing_anomaly")) score += 30;

   // Keep thresholds deterministic for production
   const decision =
    score >= 60 ? "deny" :
    score >= 30 ? "review" : "approve";

   const rationale = `Flags=${state.ruleFlags.join(",") || "none"}; score=${score}`;

   return { riskScore: score, decision, rationale };
};

const routeNode = async (state: typeof FraudState.State) => {
  return {
    rationale:
      `${state.rationale}; routed=${state.decision}; claimId=${state.claim.claimId}`,
   };
};

Build the graph:

const graph = new StateGraph(FraudState)
 .addNode("enrich", enrichNode)
 .addNode("score", scoreNode)
 .addNode("route", routeNode)
 .addEdge(START, "enrich")
 .addEdge("enrich", "score")
 .addEdge("score", "route")
 .addEdge("route", END);

export const fraudApp = graph.compile();

3) Run the agent on a claim

This is the pattern you want in your API layer or worker. Keep patient identifiers minimized in memory and never log raw PHI unless your controls allow it.

async function main() {
 const result = await fraudApp.invoke({
   claim: {
     claimId: "CLM-10001",
     patientId: "PAT-9001",
     providerId: "PRV-42",
     cptCode: "99285",
     icd10Code: "R07.9",
     amount: 25,
     serviceDate: "2026-04-01",
     submissionDate: "2026-04-02",
     locationState: "CA",
   },
 });

 console.log({
   decision: result.decision,
   riskScore: result.riskScore,
   rationale: result.rationale,
 });
}

main();

Why this pattern works

ConcernBad approachBetter approach
Fraud logicLLM decides everythingDeterministic rules first
AuditabilityFree-form text onlyStructured state + rationale
ComplianceRaw PHI everywhereMinimized fields in graph state
OperationsOne big promptSmall nodes with clear ownership

Production Considerations

  • Deploy in-region

Use a region that matches your data residency obligations. If claims data must stay in-country or inside a specific cloud region, keep both LangGraph execution and any model calls inside that boundary.

  • Log decisions, not raw PHI

Store claimId, riskScore, decision, rule flags, reviewer ID, and timestamps. If you need sample payloads for debugging, redact patient identifiers and diagnosis details before they hit logs.

  • Add human review thresholds

Auto-deny only when the evidence is strong and policy-approved. For medium-risk claims, route to investigators with reason codes like duplicate billing or impossible timing.

  • Version rules like code

Treat fraud rules as deployable artifacts with semantic versions. That gives compliance teams a way to answer “what logic was active on this date?”

Common Pitfalls

  1. Letting the model make final decisions without guardrails

    • In healthcare, that creates explainability and compliance problems.
    • Avoid it by using deterministic policy checks before any model-based reasoning.
  2. Logging sensitive claim payloads verbatim

    • Teams do this during debugging and then ship it to production.
    • Avoid it by redacting PHI at the edge and logging only audit-safe fields.
  3. Mixing investigation logic with transport logic

    • If your API handler contains business rules plus retry logic plus persistence code, you won’t be able to test or certify it cleanly.
    • Keep LangGraph nodes focused on fraud logic; handle retries, auth, persistence, and queueing outside the graph.
  4. Ignoring false positives

    • A healthcare fraud agent that over-flags legitimate claims will get turned off quickly.
    • Track precision by provider segment, specialty, geography, and claim type so thresholds are tuned per workflow instead of globally.

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