How to Build a underwriting Agent Using LangGraph in Python for wealth management

By Cyprian AaronsUpdated 2026-04-21
underwritinglanggraphpythonwealth-management

An underwriting agent for wealth management takes client inputs, pulls the right policy and portfolio context, evaluates risk against firm rules, and produces a decision path that a human can review. It matters because wealth products are full of compliance constraints, suitability checks, and audit requirements; you need a workflow that is deterministic where it must be, but flexible enough to handle missing documents, exceptions, and escalation.

Architecture

  • Input normalization node

    • Cleans up raw client data: KYC profile, net worth, liquidity, risk tolerance, jurisdiction, product requested.
    • Converts messy payloads into a typed state object.
  • Policy retrieval node

    • Pulls underwriting rules from your internal knowledge base or policy store.
    • Keeps the agent grounded in current compliance rules instead of model memory.
  • Risk evaluation node

    • Scores suitability, concentration risk, AML/KYC flags, and product eligibility.
    • Produces structured outputs, not free-form text.
  • Decision node

    • Chooses approve, reject, or escalate.
    • Applies deterministic thresholds so decisions are auditable.
  • Human review node

    • Routes edge cases to an underwriter or compliance analyst.
    • Captures reviewer notes for the audit trail.
  • Audit logging node

    • Persists inputs, outputs, policy version, model version, and timestamps.
    • Required for regulatory review and internal controls.

Implementation

1. Define the state and decision schema

Use a typed state so every node knows exactly what it can read and write. In wealth management, that keeps the workflow stable when you add new checks later.

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

Decision = Literal["approve", "reject", "escalate"]

class UnderwritingState(TypedDict):
    client_id: str
    jurisdiction: str
    net_worth: float
    liquid_assets: float
    risk_tolerance: str
    product_type: str
    kyc_passed: bool
    policy_text: str
    risk_score: int
    decision: Decision
    rationale: str
    reviewer_note: Optional[str]

2. Build nodes with deterministic logic

Keep the first version simple and explainable. If you later add an LLM for summarization or exception handling, keep it out of the core decision path.

def normalize_input(state: UnderwritingState) -> UnderwritingState:
    state["risk_tolerance"] = state["risk_tolerance"].strip().lower()
    state["product_type"] = state["product_type"].strip().lower()
    return state

def load_policy(state: UnderwritingState) -> UnderwritingState:
    # Replace with vector search / database lookup in production
    if state["jurisdiction"] == "US":
        state["policy_text"] = "US wealth products require KYC pass and minimum liquid assets."
    else:
        state["policy_text"] = "Non-US accounts require enhanced due diligence."
    return state

def evaluate_risk(state: UnderwritingState) -> UnderwritingState:
    score = 0

    if not state["kyc_passed"]:
        score += 80
    if state["liquid_assets"] < 100000:
        score += 30
    if state["risk_tolerance"] == "low" and state["product_type"] in {"options", "leveraged_etf"}:
        score += 40

    state["risk_score"] = score
    return state

def decide(state: UnderwritingState) -> UnderwritingState:
    if not state["kyc_passed"]:
        state["decision"] = "reject"
        state["rationale"] = "KYC failed"
    elif state["risk_score"] >= 70:
        state["decision"] = "escalate"
        state["rationale"] = "High-risk profile requires human review"
    else:
        state["decision"] = "approve"
        state["rationale"] = "Meets policy requirements"
    return state

def human_review(state: UnderwritingState) -> UnderwritingState:
    # In production this would be a task queue / UI callback
    state["reviewer_note"] = f"Reviewed due to {state['rationale']}"
    return state

def audit_log(state: UnderwritingState) -> UnderwritingState:
    print(
        {
            "client_id": state["client_id"],
            "decision": state["decision"],
            "risk_score": state["risk_score"],
            "policy_text": state["policy_text"],
            "rationale": state["rationale"],
            "reviewer_note": state.get("reviewer_note"),
        }
    )
    return state

3. Wire the workflow with LangGraph routing

This is where LangGraph earns its keep. Use StateGraph, add nodes, then branch on the decision outcome before ending the run.

def route_after_decision(state: UnderwritingState) -> str:
    if state["decision"] == "escalate":
        return "human_review"
    return "audit_log"

graph = StateGraph(UnderwritingState)

graph.add_node("normalize_input", normalize_input)
graph.add_node("load_policy", load_policy)
graph.add_node("evaluate_risk", evaluate_risk)
graph.add_node("decide", decide)
graph.add_node("human_review", human_review)
graph.add_node("audit_log", audit_log)

graph.add_edge(START, "normalize_input")
graph.add_edge("normalize_input", "load_policy")
graph.add_edge("load_policy", "evaluate_risk")
graph.add_edge("evaluate_risk", "decide")

graph.add_conditional_edges(
    "decide",
    route_after_decision,
)

graph.add_edge("human_review", "audit_log")
graph.add_edge("audit_log", END)

app = graph.compile()

Run it with a real input payload:

result = app.invoke(
    {
        "client_id": "C-10291",
        "jurisdiction": "US",
        "net_worth": 2500000,
        "liquid_assets": 85000,
        "risk_tolerance": "Low",
        "product_type": "Options",
        "kyc_passed": True,
        "policy_text": "",
        "risk_score": 0,
        "decision": "",
        "rationale": "",
        "reviewer_note": None,
    }
)

print(result["decision"], result["rationale"])

4. Extend for real underwriting controls

For production wealth workflows, this graph usually grows in two directions:

  • Add a document verification node for statements, tax forms, and source-of-funds evidence.
  • Add a jurisdiction gate that blocks unsupported regions before any recommendation is generated.
  • Add persistence with MemorySaver only for short-lived workflow context; keep regulated records in your own immutable store.

Production Considerations

  • Compliance-first logging

    • Store every decision with policy version, rule set hash, reviewer identity, and timestamp.
    • Make logs immutable and searchable for audits.
  • Data residency

    • Keep client PII and account data inside approved regions.
    • If you use hosted LLMs for summaries or explanations, redact sensitive fields before sending anything out of region.
  • Human-in-the-loop escalation

    • Do not auto-approve high-risk or incomplete cases.
    • Route exceptions to licensed staff with clear rationale attached to the case.
  • Monitoring and drift detection

    • Track approval rates by product type, jurisdiction, advisor desk, and client segment.
    • Alert when decision patterns shift after policy updates or model changes.

Common Pitfalls

  1. Letting the LLM make the final underwriting call

    • Use the model for extraction or explanation only.
    • Keep approval logic in deterministic code with explicit thresholds.
  2. Skipping policy versioning

    • If you cannot prove which rule set produced a decision, you do not have an audit trail.
    • Persist policy text or rule IDs alongside each run.
  3. Ignoring incomplete client data

    • Missing source-of-funds docs or unsupported jurisdictions should not fall through to “approve.”
    • Fail closed and escalate when required fields are absent.
  4. Mixing operational memory with regulated records

    • LangGraph checkpointing is useful for workflow recovery.
    • It is not your system of record for wealth management decisions; keep permanent records in compliant storage.

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