How to Build a loan approval Agent Using LangGraph in Python for healthcare
A loan approval agent for healthcare automates the decisioning flow for financing patient care, equipment purchases, or provider working capital. It matters because healthcare lending has tighter compliance, stronger audit requirements, and more sensitive data handling than a generic consumer loan workflow.
Architecture
- •
Input validation layer
- •Normalizes applicant data: entity type, revenue, debt service coverage, credit score, procedure type, and requested amount.
- •Rejects incomplete or malformed requests before they hit decision logic.
- •
Policy engine
- •Encodes lending rules for healthcare-specific cases.
- •Example rules: minimum cash flow thresholds for clinics, stricter limits for new practices, and exclusions for restricted jurisdictions.
- •
Risk scoring node
- •Produces a structured risk assessment from financial and operational signals.
- •Keeps the model output constrained to
approve,reject, ormanual_review.
- •
Compliance check node
- •Checks HIPAA-adjacent handling rules, KYC/AML flags, and audit logging requirements.
- •Ensures no protected health information is used unless it is explicitly allowed and minimized.
- •
Decision router
- •Routes cases to auto-approval, rejection, or human underwriting review.
- •Uses LangGraph conditional edges to keep the flow deterministic.
- •
Audit trail store
- •Persists every state transition, rule decision, and model output.
- •Needed for regulator review, internal QA, and dispute resolution.
Implementation
1. Define the graph state and decision functions
Use a typed state so every node reads and writes the same contract. In healthcare lending, that contract should separate financial attributes from any clinical or patient-related fields so you can enforce data minimization.
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
Decision = Literal["approve", "reject", "manual_review"]
class LoanState(TypedDict):
applicant_id: str
entity_type: str
annual_revenue: float
requested_amount: float
credit_score: int
dscr: float
jurisdiction: str
compliance_flag: bool
risk_score: float
decision: Decision
reason: str
def validate_input(state: LoanState) -> LoanState:
required = ["applicant_id", "entity_type", "annual_revenue", "requested_amount", "credit_score", "dscr"]
for field in required:
if field not in state:
raise ValueError(f"Missing required field: {field}")
return state
def score_risk(state: LoanState) -> LoanState:
score = 0.0
score += 0.4 if state["credit_score"] < 650 else 0.1
score += 0.4 if state["dscr"] < 1.2 else 0.1
score += 0.2 if state["requested_amount"] > state["annual_revenue"] * 0.5 else 0.05
state["risk_score"] = min(score, 1.0)
return state
def compliance_check(state: LoanState) -> LoanState:
restricted_jurisdictions = {"XK", "IR", "KP"}
state["compliance_flag"] = state["jurisdiction"] in restricted_jurisdictions
return state
def decide(state: LoanState) -> LoanState:
if state["compliance_flag"]:
state["decision"] = "manual_review"
state["reason"] = "Jurisdiction requires compliance review"
elif state["risk_score"] >= 0.7:
state["decision"] = "reject"
state["reason"] = "Risk above threshold"
elif state["risk_score"] <= 0.3:
state["decision"] = "approve"
state["reason"] = "Meets underwriting thresholds"
else:
state["decision"] = "manual_review"
state["reason"] = "Borderline risk profile"
return state
2. Build the LangGraph workflow with conditional routing
StateGraph is the right fit here because loan approval is a finite-state process with explicit transitions. The key pattern is to keep each node small and route based on the post-node state.
workflow = StateGraph(LoanState)
workflow.add_node("validate_input", validate_input)
workflow.add_node("score_risk", score_risk)
workflow.add_node("compliance_check", compliance_check)
workflow.add_node("decide", decide)
workflow.add_edge(START, "validate_input")
workflow.add_edge("validate_input", "score_risk")
workflow.add_edge("score_risk", "compliance_check")
workflow.add_edge("compliance_check", "decide")
def route_decision(state: LoanState) -> str:
return state["decision"]
workflow.add_conditional_edges(
"decide",
route_decision,
{
"approve": END,
"reject": END,
"manual_review": END,
},
)
app = workflow.compile()
3. Run the agent with an underwriting payload
This is where you keep the input strictly financial and operational. If you need patient-related context for medical necessity financing, strip it down to approved features before this graph sees it.
payload: LoanState = {
"applicant_id": "clinic-1029",
"entity_type": "outpatient_clinic",
"annual_revenue": 2400000.0,
"requested_amount": 300000.0,
"credit_score": 684,
"dscr": 1.35,
"jurisdiction": "US",
}
result = app.invoke(payload)
print(result["decision"])
print(result["reason"])
print(result["risk_score"])
4. Add audit logging around execution
For production healthcare workflows, every invocation should be traceable. At minimum log the input hash, output decision, timestamp, model/version metadata, and reviewer override if one exists.
import json
import hashlib
from datetime import datetime
def audit_record(state_in: dict, result: dict) -> dict:
raw = json.dumps(state_in, sort_keys=True).encode()
record = {
"timestamp": datetime.utcnow().isoformat(),
"request_hash": hashlib.sha256(raw).hexdigest(),
"applicant_id": result.get("applicant_id"),
"decision": result.get("decision"),
"reason": result.get("reason"),
"risk_score": result.get("risk_score"),
"system": {
"graph_version": "loan-approval-v1",
"policy_version": "2026-04",
},
}
return record
Production Considerations
- •
Data residency
- •Keep application data in-region if your healthcare customers operate under country-specific hosting rules.
- •Don’t move PHI-adjacent records across regions unless your legal basis and contracts allow it.
- •
Auditability
- •Persist every graph run with node-level outputs.
- •Underwriting teams need to explain why a request was approved, rejected, or escalated.
- •
Guardrails
- •Block direct use of protected health information in scoring unless explicitly approved by policy.
- •Use schema validation plus allowlisted features only; do not pass raw clinical notes into the agent.
- •
Monitoring
- •Track approval rates by provider type, geography, loan size, and reviewer override rate.
- •Watch for drift in DSCR distributions and rejection spikes after policy changes.
Common Pitfalls
- •
Mixing financial data with clinical data
- •Don’t feed diagnosis codes or treatment notes into underwriting unless counsel has signed off on that use case.
- •Keep a hard boundary between lending features and healthcare operations data.
- •
Using an LLM as the final decision maker
- •The LLM can summarize or classify edge cases, but final decisions should come from explicit policy logic.
- •In regulated lending flows, deterministic rules are easier to audit than free-form generation.
- •
Skipping manual review paths
- •Borderline cases need escalation instead of forced auto-decisions.
- •Use
add_conditional_edges()so uncertain cases go to underwriters instead of being silently approved or rejected.
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