How to Build a underwriting Agent Using LangGraph in Python for healthcare

By Cyprian AaronsUpdated 2026-04-21
underwritinglanggraphpythonhealthcare

An underwriting agent for healthcare takes member or provider data, checks eligibility and policy rules, scores risk against plan constraints, and produces a decision with an audit trail. That matters because healthcare underwriting is not just classification; it affects compliance, pricing, coverage eligibility, and downstream claims risk.

Architecture

  • Input normalization node

    • Cleans up raw application data from EHR exports, PDFs, CRM records, or API payloads.
    • Converts everything into a strict schema before any reasoning happens.
  • Policy retrieval node

    • Pulls the current underwriting rules, plan exclusions, state-specific regulations, and internal SOPs.
    • Keeps the agent grounded in approved policy instead of free-form generation.
  • Risk assessment node

    • Scores the case using deterministic logic plus model-assisted reasoning where allowed.
    • Produces structured outputs like approve, review, or deny.
  • Compliance guardrail node

    • Checks HIPAA-sensitive fields, minimum necessary access, and prohibited decisions.
    • Forces escalation when the input is incomplete or outside policy scope.
  • Decision synthesis node

    • Converts the assessment into a final underwriting recommendation.
    • Generates a concise explanation that can be audited by compliance teams.
  • Audit logging node

    • Stores inputs, retrieved policy references, intermediate decisions, and final output.
    • Supports traceability for regulators and internal review.

Implementation

1. Define the state and decision schema

Use a typed state object so every node reads and writes predictable fields. In healthcare underwriting, this is non-negotiable because you need reproducibility and auditability.

from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
import operator

class UnderwritingState(TypedDict):
    applicant: dict
    policy_context: str
    risk_factors: list[str]
    decision: Literal["approve", "manual_review", "deny"]
    rationale: str
    audit_log: Annotated[list[str], operator.add]

def normalize_input(state: UnderwritingState) -> UnderwritingState:
    applicant = state["applicant"]
    normalized = {
        "age": int(applicant["age"]),
        "state": applicant["state"].upper(),
        "diagnosis_codes": [c.strip().upper() for c in applicant.get("diagnosis_codes", [])],
        "coverage_requested": applicant["coverage_requested"],
    }
    return {
        **state,
        "applicant": normalized,
        "audit_log": ["normalized input"],
    }

2. Add policy retrieval and risk scoring nodes

Keep policy text outside the prompt. In production you would replace the static string with a vector store or document service that returns only approved plan language.

def retrieve_policy(state: UnderwritingState) -> UnderwritingState:
    state["policy_context"] = (
        "Plan A excludes experimental treatments. "
        "Applicants with uncontrolled chronic conditions require manual review. "
        "State-specific rules apply for CA and NY."
    )
    return {**state, "audit_log": ["retrieved policy context"]}

def assess_risk(state: UnderwritingState) -> UnderwritingState:
    applicant = state["applicant"]
    factors = []

    if applicant["age"] > 65:
        factors.append("age_over_65")
    if any(code.startswith("E11") for code in applicant["diagnosis_codes"]):
        factors.append("diabetes_related_condition")
    if applicant["state"] in {"CA", "NY"}:
        factors.append("state_regulated_market")

    return {
        **state,
        "risk_factors": factors,
        "audit_log": [f"risk factors identified: {', '.join(factors) or 'none'}"],
    }

3. Add compliance checks and decision logic

This is where LangGraph helps. You can branch deterministically based on state instead of forcing everything through an LLM.

def compliance_check(state: UnderwritingState) -> UnderwritingState:
    applicant = state["applicant"]

    if not applicant.get("coverage_requested"):
        return {
            **state,
            "decision": "manual_review",
            "rationale": "Missing coverage request; cannot underwrite.",
            "audit_log": ["compliance failed: missing coverage_requested"],
        }

    if len(applicant.get("diagnosis_codes", [])) == 0:
        return {
            **state,
            "decision": "manual_review",
            "rationale": "No diagnosis codes provided; manual review required.",
            "audit_log": ["compliance failed: missing diagnosis codes"],
        }

    return {**state, "audit_log": ["compliance passed"]}

4. Build the LangGraph workflow

Use StateGraph, add nodes with add_node, connect them with add_edge, then compile. This pattern gives you a real graph with explicit control flow.

def decide(state: UnderwritingState) -> UnderwritingState:
    if state.get("decision") in {"manual_review", "deny"}:
        return state

    factors = set(state.get("risk_factors", []))
    if {"age_over_65", "diabetes_related_condition"} <= factors:
        decision = "manual_review"
        rationale = (
            f"Multiple elevated risk indicators found: {', '.join(sorted(factors))}. "
            f"Policy context: {state['policy_context']}"
        )
    else:
        decision = "approve"
        rationale = (
            f"Case within acceptable risk thresholds. "
            f"Policy context: {state['policy_context']}"
        )

    return {
        **state,
        "decision": decision,
        "rationale": rationale,
        "audit_log": [f"final decision: {decision}"],
    }

graph = StateGraph(UnderwritingState)
graph.add_node("normalize_input", normalize_input)
graph.add_node("retrieve_policy", retrieve_policy)
graph.add_node("assess_risk", assess_risk)
graph.add_node("compliance_check", compliance_check)
graph.add_node("decide", decide)

graph.add_edge(START, "normalize_input")
graph.add_edge("normalize_input", "retrieve_policy")
graph.add_edge("retrieve_policy", "assess_risk")
graph.add_edge("assess_risk", "compliance_check")
graph.add_edge("compliance_check", "decide")
graph.add_edge("decide", END)

app = graph.compile()

result = app.invoke({
    "applicant": {
        "age": 71,
        "state": "ca",
        "diagnosis_codes": ["E11.9"],
        "coverage_requested": True,
    },
    "policy_context": "",
    "risk_factors": [],
    "decision": None,
    "rationale": "",
    # Use operator.add reducer semantics by providing an initial list
    # so every node can append audit entries.
    # In production this should go to immutable storage.
    # The graph will merge updates across nodes.
})
print(result["decision"])
print(result["rationale"])
print(result["audit_log"])

Production Considerations

  • HIPAA controls

    • Do not send PHI to external model endpoints unless you have a signed BAA and approved data handling terms.
    • Minimize payloads before they enter the graph; strip identifiers that are not needed for underwriting.
  • Auditability

    • Persist every state transition with timestamps, node names, retrieved policy versions, and final decisions.
    • Store immutable logs in WORM-capable storage or an equivalent tamper-evident system.
  • Data residency

    • Keep processing inside the required jurisdiction when dealing with regional health data restrictions.
    • If your deployment spans multiple regions, pin workloads to approved clusters and avoid cross-region PHI replication.
  • Guardrails

    • Hard-fail to manual review when inputs are incomplete or conflict with policy.
    • Add deterministic rules before any model call so the agent cannot override mandatory compliance checks.

Common Pitfalls

  • Letting the LLM make the final underwriting call

    • Don’t do that for regulated healthcare workflows.
    • Use the model for summarization or extraction only; keep approval/denial logic deterministic and reviewable.
  • Skipping schema validation

    • Free-form JSON from upstream systems will break your workflow sooner or later.
    • Validate at the boundary with typed state or Pydantic models before any graph execution.
  • Ignoring versioned policy context

    • If you don’t store which policy document was used, you cannot explain why a case was approved six months later.
    • Always attach policy version IDs to the graph run and persist them with the audit record.

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