How to Build a underwriting Agent Using LangGraph in Python for pension funds
An underwriting agent for pension funds evaluates contribution plans, member profiles, plan documents, and risk rules to produce a decision recommendation with a clear audit trail. It matters because pension underwriting is not just scoring risk; it has to respect compliance, data residency, and explainability requirements that survive internal review and regulator scrutiny.
Architecture
- •
Document ingestion layer
- •Pulls plan rules, trustee policies, actuarial assumptions, and member submissions from approved storage.
- •Normalizes PDFs, spreadsheets, and structured forms into a consistent internal schema.
- •
Risk extraction node
- •Uses an LLM to extract underwriting signals like contribution volatility, eligibility exceptions, employer concentration, and benefit design anomalies.
- •Outputs structured JSON, not free text.
- •
Policy validation node
- •Checks extracted facts against pension fund underwriting rules.
- •Flags breaches such as missing disclosures, non-standard vesting terms, or jurisdiction-specific constraints.
- •
Decision synthesis node
- •Produces one of:
approve,reject, orreview. - •Attaches reason codes and evidence references for downstream audit.
- •Produces one of:
- •
Human review gate
- •Routes edge cases to a compliance officer or underwriter.
- •Preserves the model output plus human override in the final record.
- •
Audit and persistence layer
- •Stores every state transition, prompt version, model version, and source document hash.
- •Required for regulator review and internal model governance.
Implementation
1) Define the state and structured output
For pension underwriting, the state should carry raw inputs, extracted facts, policy results, and the final recommendation. Keep the state typed so your graph nodes stay explicit.
from typing import TypedDict, Literal, List, Optional
from langgraph.graph import StateGraph, START, END
from langchain_core.runnables import RunnableLambda
class UnderwritingState(TypedDict):
applicant_id: str
jurisdiction: str
raw_documents: List[str]
extracted_facts: Optional[dict]
policy_result: Optional[dict]
decision: Optional[Literal["approve", "reject", "review"]]
rationale: Optional[str]
def extract_facts(state: UnderwritingState) -> dict:
docs = " ".join(state["raw_documents"])
return {
"contribution_volatility": "high" if "irregular" in docs.lower() else "low",
"missing_disclosures": "yes" if "missing disclosure" in docs.lower() else "no",
"employer_risk": "elevated" if "single employer" in docs.lower() else "normal",
}
def validate_policy(state: UnderwritingState) -> dict:
facts = state["extracted_facts"] or {}
breaches = []
if facts.get("missing_disclosures") == "yes":
breaches.append("DISCLOSURE_GAP")
if facts.get("contribution_volatility") == "high":
breaches.append("VOLATILITY_THRESHOLD")
return {"breaches": breaches}
def decide(state: UnderwritingState) -> dict:
breaches = (state["policy_result"] or {}).get("breaches", [])
if not breaches:
return {"decision": "approve", "rationale": "No policy breaches detected."}
if len(breaches) >= 2:
return {"decision": "reject", "rationale": f"Multiple breaches detected: {breaches}"}
return {"decision": "review", "rationale": f"Single breach detected: {breaches}"}
2) Build the LangGraph workflow
Use StateGraph for deterministic control flow. This is the right shape for underwriting because you want explicit steps instead of a single opaque agent loop.
workflow = StateGraph(UnderwritingState)
workflow.add_node("extract_facts", RunnableLambda(extract_facts))
workflow.add_node("validate_policy", RunnableLambda(validate_policy))
workflow.add_node("decide", RunnableLambda(decide))
workflow.add_edge(START, "extract_facts")
workflow.add_edge("extract_facts", "validate_policy")
workflow.add_edge("validate_policy", "decide")
workflow.add_edge("decide", END)
app = workflow.compile()
3) Run the graph with pension-fund-specific inputs
In production you will source documents from controlled storage and pass only approved content into the graph. Keep jurisdiction explicit because rules differ across regions.
input_state: UnderwritingState = {
"applicant_id": "PF-10291",
"jurisdiction": "ZA",
"raw_documents": [
"Employer contribution schedule shows irregular payments.",
"Missing disclosure on beneficiary nomination.",
"Plan summary references a single employer sponsor."
],
"extracted_facts": None,
"policy_result": None,
"decision": None,
"rationale": None,
}
result = app.invoke(input_state)
print(result["decision"])
print(result["rationale"])
4) Add a human review branch for exceptions
If you need escalation for borderline cases, use conditional edges. This is where LangGraph shines over simple chains.
from typing import Literal
def route_after_decision(state: UnderwritingState) -> Literal["end", "__end__"]:
return "__end__"
# Example pattern: send review cases to an external queue instead of auto-finalizing
def escalate_for_review(state: UnderwritingState) -> dict:
return {
**state,
"rationale": f"{state['rationale']} Escalated to compliance review."
}
workflow2 = StateGraph(UnderwritingState)
workflow2.add_node("extract_facts", RunnableLambda(extract_facts))
workflow2.add_node("validate_policy", RunnableLambda(validate_policy))
workflow2.add_node("decide", RunnableLambda(decide))
workflow2.add_node("escalate_for_review", RunnableLambda(escalate_for_review))
workflow2.add_edge(START, "extract_facts")
workflow2.add_edge("extract_facts", "validate_policy")
workflow2.add_edge("validate_policy", "decide")
def branch(state: UnderwritingState):
return state["decision"]
workflow2.add_conditional_edges(
"decide",
lambda state: state["decision"],
{
"approve": END,
"reject": END,
"review": "__end__", # replace with queue handoff in your app layer
}
)
app2 = workflow2.compile()
Production Considerations
- •
Data residency
- •Keep member data inside approved regional infrastructure.
- •If your pension fund operates across jurisdictions, route workloads by residency policy before they reach the graph.
- •
Auditability
- •Persist input hashes, node outputs, prompt templates, model versions, and timestamps.
- •Regulators care about how a decision was made more than whether it was “AI-assisted.”
- •
Guardrails
- •Enforce structured outputs with Pydantic or strict JSON parsing before any decision node runs.
- •Block unsupported recommendations like investment advice or benefit promises outside underwriting scope.
- •
Monitoring
- •Track reject/review rates by jurisdiction and plan type.
- •Alert on drift when policy breach frequency changes materially after document template updates or regulatory changes.
Common Pitfalls
- •
Using an open-ended agent loop for a deterministic underwriting process
- •Don’t let the model decide its own path repeatedly.
- •Use
StateGraphwith fixed nodes so every step is inspectable and testable.
- •
Mixing raw LLM text with decision logic
- •Free text is fragile in regulated workflows.
- •Convert model output into typed fields before policy evaluation.
- •
Ignoring jurisdiction-specific rule sets
- •Pension underwriting rules are not universal.
- •Parameterize policy checks by country or fund rulebook so one deployment can’t silently apply the wrong standard.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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