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

By Cyprian AaronsUpdated 2026-04-21
underwritinglanggraphpythoninsurance

An underwriting agent automates the first pass of risk assessment for an insurance submission. It reads the application, extracts key attributes, checks rules and policy appetite, then routes the case to approve, refer, or decline with an audit trail.

Architecture

  • Input ingestion layer

    • Accepts submission data from CRM, policy admin, email parsing, or API payloads.
    • Normalizes fields like applicant type, coverage requested, loss history, location, and industry.
  • Document extraction layer

    • Pulls structured data from ACORD forms, PDFs, and supporting documents.
    • Keeps extracted values tied to source references for auditability.
  • Underwriting rules engine

    • Applies deterministic checks such as appetite rules, limits, exclusions, and mandatory referral thresholds.
    • This should stay separate from the LLM so you can explain decisions.
  • LLM reasoning node

    • Summarizes risk factors, identifies missing information, and drafts underwriting notes.
    • Used for classification and synthesis, not final authority.
  • Decision router

    • Produces one of a small set of outcomes: approve, refer, decline, or request_more_info.
    • Routes based on structured state rather than free-form text.
  • Audit and persistence layer

    • Stores every state transition, model output, rule hit, and final decision.
    • Needed for compliance review, dispute handling, and model governance.

Implementation

1. Define the underwriting state

Use a typed state object so every node reads and writes predictable fields. In insurance workflows, this is what keeps your graph auditable and reduces accidental prompt-driven behavior.

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from langchain_core.runnables import RunnableLambda

Decision = Literal["approve", "refer", "decline", "request_more_info"]

class UnderwritingState(TypedDict):
    submission: dict
    extracted: dict
    risk_score: int
    decision: Decision
    rationale: str
    missing_fields: list[str]

2. Add deterministic underwriting checks

This node should encode obvious business rules before any LLM call. If a submission violates appetite or lacks required fields, you want a predictable outcome that can be defended in an audit.

def extract_submission(state: UnderwritingState) -> dict:
    sub = state["submission"]
    missing = []

    required = ["applicant_name", "industry", "state", "coverage_limit"]
    for field in required:
        if not sub.get(field):
            missing.append(field)

    extracted = {
        "applicant_name": sub.get("applicant_name"),
        "industry": sub.get("industry"),
        "state": sub.get("state"),
        "coverage_limit": sub.get("coverage_limit"),
        "prior_losses": sub.get("prior_losses", []),
        "years_in_business": sub.get("years_in_business", 0),
    }

    risk_score = 0
    if extracted["years_in_business"] < 2:
        risk_score += 30
    if len(extracted["prior_losses"]) > 2:
        risk_score += 40
    if extracted["state"] in {"AK", "LA"}:
        risk_score += 10

    return {
        **state,
        "extracted": extracted,
        "missing_fields": missing,
        "risk_score": risk_score,
    }

def route_decision(state: UnderwritingState) -> str:
    if state["missing_fields"]:
        return "request_more_info"
    if state["risk_score"] >= 60:
        return "decline"
    if state["risk_score"] >= 30:
        return "refer"
    return "approve"

3. Use an LLM node for underwriting notes only

The LLM should summarize why the case was routed a certain way and flag gaps for an underwriter. Keep it out of the final decision path unless you have a controlled approval workflow.

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

def write_underwriting_note(state: UnderwritingState) -> dict:
    prompt = f"""
You are assisting a commercial insurance underwriter.
Summarize the key risk factors in one short paragraph.
Do not invent facts.

Submission: {state['extracted']}
Risk score: {state['risk_score']}
Missing fields: {state['missing_fields']}
"""
    response = llm.invoke(prompt)
    return {**state, "rationale": response.content}

4. Build the LangGraph workflow

This is the actual pattern you want in production: deterministic intake first, conditional routing second, then optional LLM summarization before ending the run.

workflow = StateGraph(UnderwritingState)

workflow.add_node("extract_submission", RunnableLambda(extract_submission))
workflow.add_node("write_underwriting_note", RunnableLambda(write_underwriting_note))

workflow.set_entry_point("extract_submission")
workflow.add_conditional_edges(
    "extract_submission",
    route_decision,
    {
        "request_more_info": END,
        "decline": END,
        "refer": "write_underwriting_note",
        "approve": "write_underwriting_note",
    },
)
workflow.add_edge("write_underwriting_note", END)

graph = workflow.compile()

result = graph.invoke({
    "submission": {
        "applicant_name": "Acme Manufacturing",
        "industry": "metal fabrication",
        "state": "TX",
        "coverage_limit": 5000000,
        "prior_losses": [{"year": 2022, "amount": 120000}],
        "years_in_business": 5,
    },
    "extracted": {},
    "risk_score": 0,
    "decision": "",
    "rationale": "",
    "missing_fields": [],
})

print(result)

Production Considerations

  • Keep PII inside your controlled boundary

    • Mask or tokenize sensitive fields before sending data to an external model provider.
    • For insurance submissions this includes SSNs, DOBs, VINs, claim notes, and medical details where applicable.
  • Log every decision path

    • Persist node inputs/outputs, rule hits, model version, prompt version, and final outcome.
    • That gives you traceability for compliance reviews and internal model governance.
  • Respect data residency

    • If your carrier operates across regions, pin storage and inference to approved jurisdictions.
    • Do not route regulated customer data to a region that violates local requirements.
  • Add human-in-the-loop thresholds

    • Any high-limit policy request, adverse loss history pattern, or exception to appetite should go to referral.
    • The graph should make escalation explicit instead of trying to “reason through” exceptions automatically.

Common Pitfalls

  • Letting the LLM make the final underwriting decision

    • Don’t do this. Use deterministic rules for approve/decline boundaries and reserve the model for summarization or classification support.
  • Not separating extracted facts from generated text

    • Store raw submission data and extracted fields separately from rationale.
    • If you mix them together, audits become messy and hallucinations become hard to detect.
  • Ignoring jurisdictional constraints

    • Insurance workflows often cross state or country lines with different filing rules and retention requirements.
    • Encode jurisdiction checks into the graph early so you can block unsupported cases before any downstream processing.

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