How to Build a loan approval Agent Using LangGraph in TypeScript for pension funds

By Cyprian AaronsUpdated 2026-04-21
loan-approvallanggraphtypescriptpension-funds

A loan approval agent for pension funds takes a structured loan application, checks it against policy and risk rules, calls the right internal systems, and returns a decision with an audit trail. It matters because pension capital has stricter fiduciary, compliance, and data residency constraints than a normal consumer lending flow, so the agent has to be deterministic, explainable, and easy to review.

Architecture

  • State model

    • Holds the application payload, extracted borrower facts, policy checks, risk signals, decision, and audit notes.
    • Keep it serializable so every step can be replayed.
  • Policy retrieval layer

    • Pulls pension-fund-specific lending rules: exposure limits, approved asset classes, jurisdiction restrictions, and delegated authority thresholds.
    • This should be versioned by fund and region.
  • Decision graph

    • Uses LangGraph to route between eligibility checks, risk scoring, manual review, and final approval/rejection.
    • The graph should make branching explicit.
  • Tooling layer

    • Connects to credit bureau APIs, KYC/AML services, document parsers, and internal portfolio systems.
    • Every tool call should be logged with request IDs.
  • Audit and explanation layer

    • Produces a human-readable rationale for compliance teams.
    • Stores inputs, outputs, policy version, and timestamps for later review.
  • Deployment controls

    • Enforces regional hosting for data residency.
    • Adds approval thresholds and escalation paths for human sign-off.

Implementation

  1. Define the state shape and build a typed graph

Use StateGraph from LangGraph and keep the state explicit. For loan approval workflows in pension funds, I prefer a state object that carries both machine-readable decisions and an audit record.

import { StateGraph, START, END } from "@langchain/langgraph";

type LoanApplication = {
  applicantId: string;
  amount: number;
  termMonths: number;
  jurisdiction: string;
  purpose: string;
};

type LoanState = {
  application: LoanApplication;
  policyVersion?: string;
  eligibility?: "pass" | "fail";
  riskScore?: number;
  decision?: "approve" | "reject" | "manual_review";
  rationale?: string[];
};

const graph = new StateGraph<LoanState>({
  channels: {
    application: null as any,
    policyVersion: null as any,
    eligibility: null as any,
    riskScore: null as any,
    decision: null as any,
    rationale: null as any,
  },
});
  1. Add nodes for policy checks, risk scoring, and final decision

Keep these nodes small. In production you want each node to do one thing so you can test it independently and trace failures cleanly.

const checkPolicy = async (state: LoanState): Promise<Partial<LoanState>> => {
  const { amount, jurisdiction } = state.application;

  if (jurisdiction !== "ZA") {
    return {
      policyVersion: "pension-fund-lending-v3",
      eligibility: "fail",
      rationale: ["Jurisdiction not approved for this pension fund mandate"],
      decision: "reject",
    };
  }

  if (amount > 5000000) {
    return {
      policyVersion: "pension-fund-lending-v3",
      eligibility: "fail",
      rationale: ["Requested amount exceeds delegated authority"],
      decision: "manual_review",
    };
  }

  return {
    policyVersion: "pension-fund-lending-v3",
    eligibility: "pass",
    rationale: ["Policy checks passed"],
  };
};

const scoreRisk = async (state: LoanState): Promise<Partial<LoanState>> => {
  const baseScore =
    state.application.termMonths > 60 ? 72 : 35;

  return {
    riskScore: baseScore,
    rationale: [...(state.rationale ?? []), `Computed risk score ${baseScore}`],
  };
};

const decide = async (state: LoanState): Promise<Partial<LoanState>> => {
  if (state.eligibility === "fail" && state.decision === "reject") return {};

  if ((state.riskScore ?? 100) > 65) {
    return {
      decision: "manual_review",
      rationale: [...(state.rationale ?? []), "Risk score above automated approval threshold"],
    };
  }

   return {
    decision: "approve",
    rationale: [...(state.rationale ?? []), "Meets automated approval threshold"],
   };
};
  1. Wire the graph with conditional routing

This is the part that makes LangGraph useful. Use addNode, addEdge, and addConditionalEdges so the workflow is visible in code instead of hidden in callbacks.

const workflow = new StateGraph<LoanState>({
  channels: {
    application: null as any,
    policyVersion: null as any,
    eligibility: null as any,
    riskScore: null as any,
    decision: null as any,
    rationale: null as any,
  },
});

workflow.addNode("policy", checkPolicy);
workflow.addNode("risk", scoreRisk);
workflow.addNode("decision", decide);

workflow.addEdge(START, "policy");

workflow.addConditionalEdges("policy", (state) => {
  if (state.decision === "reject") return END;
  if (state.eligibility === "fail") return END;
  return "risk";
});

workflow.addEdge("risk", "decision");
workflow.addEdge("decision", END);

const app = workflow.compile();

const result = await app.invoke({
  application: {
    applicantId: "app-123",
    amount: 2500000,
    termMonths: 48,
    jurisdiction: "ZA",
    purpose: "Bridge financing",
  },
});
console.log(result);
  1. Add audit logging around execution

Pension funds need an evidentiary trail. Store the input state, output state, policy version, and node-level timestamps in your own persistence layer; don’t rely on console logs.

  • Persist application.applicantId
  • Persist policyVersion
  • Persist decision
  • Persist all rationale entries
  • Store request metadata like region, tenant ID, and operator ID

Production Considerations

  • Data residency
    • Keep the graph runtime and its storage in-region if the fund mandates local processing.
  • Compliance controls

Use hard thresholds for auto-approval and route borderline cases to manual review.

  • Monitoring

Track node latency, rejection rates by reason code, manual review volume, and tool failure rates.

  • Guardrails

Validate input schemas before invoking the graph. Reject missing KYC fields or unsupported jurisdictions early.

Common Pitfalls

  • Mixing policy logic with model logic

Don’t let an LLM invent lending rules. Keep policy checks deterministic and versioned outside the model.

  • No audit trail

If you can’t explain why a loan was approved or rejected six months later, the system is not ready for a pension fund.

  • Over-automating edge cases

Large amounts, unusual jurisdictions, or thin-file applicants should default to manual review instead of auto-decisioning.


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