How to Build a KYC verification Agent Using LangGraph in TypeScript for healthcare

By Cyprian AaronsUpdated 2026-04-21
kyc-verificationlanggraphtypescripthealthcare

A KYC verification agent for healthcare collects identity data, checks it against policy and external systems, and decides whether a patient, caregiver, or provider can proceed. In healthcare, this matters because bad identity handling creates fraud risk, breaks compliance, and can expose protected health information to the wrong workflow.

Architecture

  • Input normalization node

    • Cleans up user-submitted identity fields like name, DOB, address, phone, and government ID.
    • Validates required fields before any external calls.
  • Document verification node

    • Checks uploaded IDs, insurance cards, or authorization forms.
    • Extracts metadata and flags missing or inconsistent documents.
  • Policy and compliance node

    • Applies healthcare-specific rules such as minimum required identifiers, consent checks, and residency constraints.
    • Blocks processing if the request violates data handling policy.
  • External verification node

    • Calls third-party identity services or internal master patient index systems.
    • Confirms the identity against trusted records.
  • Risk scoring node

    • Produces an approval, manual review, or rejection decision.
    • Uses deterministic thresholds so audit trails are explainable.
  • Audit sink

    • Stores every state transition, decision input, and output.
    • Keeps a trace for compliance review and incident response.

Implementation

1) Define the graph state and the workflow shape

Use a typed state object so every node reads and writes predictable fields. In healthcare workflows, this is not optional; you need stable auditability around what was collected and why a decision was made.

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

type KycDecision = "approved" | "manual_review" | "rejected";

const KycState = Annotation.Root({
  input: Annotation<{
    fullName: string;
    dob: string;
    address?: string;
    nationalId?: string;
    consentGiven: boolean;
    residencyCountry: string;
  }>(),
  normalized: Annotation<Record<string, unknown>>({
    default: () => ({}),
  }),
  documentChecks: Annotation<{
    valid: boolean;
    issues: string[];
  }>({
    default: () => ({ valid: false, issues: [] }),
  }),
  riskScore: Annotation<number>({
    default: () => 0,
  }),
  decision: Annotation<KycDecision | null>({
    default: () => null,
  }),
});

2) Add nodes for normalization, policy checks, and decisioning

Keep the policy logic explicit. Healthcare teams need to explain why a case was blocked without reverse-engineering model output later.

const normalizeInput = async (state: typeof KycState.State) => {
  const input = state.input;

  return {
    normalized: {
      fullName: input.fullName.trim().toLowerCase(),
      dob: input.dob,
      address: input.address?.trim(),
      nationalId: input.nationalId?.replace(/\s+/g, ""),
      consentGiven: input.consentGiven,
      residencyCountry: input.residencyCountry.toUpperCase(),
    },
  };
};

const applyHealthcarePolicy = async (state: typeof KycState.State) => {
  const n = state.normalized as Record<string, unknown>;
  const issues: string[] = [];

  if (!n.consentGiven) issues.push("Missing patient consent");
  if (!n.fullName || !n.dob) issues.push("Missing core identity fields");
  if ((n.residencyCountry as string) !== "US") {
    issues.push("Data residency requires manual review for non-US records");
  }

  return {
    documentChecks: {
      valid: issues.length === 0,
      issues,
    },
    riskScore: issues.length * 40,
    decision:
      issues.length === 0 ? "manual_review" : "rejected",
  };
};

const finalDecision = async (state: typeof KycState.State) => {
  const score = state.riskScore;
  const docs = state.documentChecks;

   let decision: KycDecision = "manual_review";

   if (docs.valid && score < 20) decision = "approved";
   if (!docs.valid && score >= 40) decision = "rejected";

   return { decision };
};

3) Wire the graph with StateGraph and compile it

This is the actual LangGraph pattern in TypeScript. The graph is small here on purpose; in production you would split document extraction and external verification into separate nodes with retries and timeouts.

const graph = new StateGraph(KycState)
  .addNode("normalizeInput", normalizeInput)
  .addNode("applyHealthcarePolicy", applyHealthcarePolicy)
  .addNode("finalDecision", finalDecision)
  
graph.addEdge(START, "normalizeInput");
graph.addEdge("normalizeInput", "applyHealthcarePolicy");
graph.addEdge("applyHealthcarePolicy", "finalDecision");
graph.addEdge("finalDecision", END);

const app = graph.compile();

4) Invoke the agent with a real payload

Use invoke() for a single verification request. For batch operations or streaming review UIs, you can switch to stream() later without changing the core graph logic.

const result = await app.invoke({
  input: {
    fullName: "Jane Doe",
    dob: "1991-04-12",
    address: "10 Main Street",
    nationalId: "A12345678",
    consentGiven: true,
    residencyCountry: "US",
  },
});

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

Production Considerations

  • Encrypt PHI at rest and in transit

    • Do not log raw identity documents or medical identifiers.
  • Pin data residency by region

    • Keep patient verification data inside approved jurisdictions.
  • Add audit trails for every node transition

    • Persist inputs, outputs, timestamps, and policy reasons for each step.
  • Put human review behind deterministic thresholds

    • Use manual review for ambiguous cases instead of letting an LLM make final compliance decisions.

Common Pitfalls

  1. Letting an LLM decide compliance outcomes

    • Avoid this by keeping approval/rejection rules deterministic in code.
    • Use model output only for extraction or classification support.
  2. Logging sensitive healthcare identifiers

    • Avoid storing raw DOBs, national IDs, or insurance numbers in application logs.
    • Redact before logging and send full records only to approved secure stores.
  3. Skipping residency checks

    • Healthcare workflows often have strict regional storage requirements.
    • Enforce residency before calling external vendors or writing to downstream systems.
  4. Building one giant node

    • Don’t cram normalization, policy checks, vendor calls, and scoring into one function.
    • Split them into nodes so you can test each rule independently and trace failures cleanly.

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