How to Build a loan approval Agent Using LangGraph in TypeScript for investment banking

By Cyprian AaronsUpdated 2026-04-21
loan-approvallanggraphtypescriptinvestment-banking

A loan approval agent in investment banking automates the first-pass decisioning flow: it ingests borrower data, validates policy rules, pulls risk signals, and routes borderline cases to a human credit officer. It matters because the bank needs faster turnaround without losing control over compliance, auditability, and capital risk.

Architecture

  • Input normalization layer

    • Converts raw application payloads into a typed internal shape.
    • Handles missing fields, inconsistent units, and jurisdiction-specific formats.
  • Policy and eligibility node

    • Checks hard rules like minimum DSCR, leverage caps, sector exclusions, KYC status, and sanctioned-country restrictions.
    • Produces a deterministic approve/reject/needs-review outcome.
  • Risk enrichment node

    • Pulls bureau scores, exposure history, covenant breaches, and internal ratings.
    • Keeps model outputs separate from policy decisions.
  • Decision graph

    • Uses LangGraph to route between auto-approval, manual review, or rejection.
    • Preserves state across nodes for traceability.
  • Audit logging layer

    • Stores every input, rule result, and final decision with timestamps and reviewer identity.
    • Required for model governance and regulatory review.
  • Human escalation path

    • Sends exceptions to a credit analyst when rules conflict or confidence is low.
    • Prevents fully automated decisions on high-risk or incomplete files.

Implementation

1) Define the graph state and decision types

Start with a strict state object. In banking workflows, weak typing becomes an audit problem fast because you cannot explain why a decision was made if the state is ambiguous.

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

type LoanDecision = "approve" | "reject" | "manual_review";

type LoanState = {
  applicantId: string;
  requestedAmount: number;
  annualRevenue: number;
  existingDebt: number;
  dscr?: number;
  kycPassed?: boolean;
  sanctionHit?: boolean;
  riskGrade?: "A" | "B" | "C" | "D";
  decision?: LoanDecision;
  reasons?: string[];
};

const LoanStateSchema = Annotation.Root({
  applicantId: Annotation<string>(),
  requestedAmount: Annotation<number>(),
  annualRevenue: Annotation<number>(),
  existingDebt: Annotation<number>(),
  dscr: Annotation<number | undefined>(),
  kycPassed: Annotation<boolean | undefined>(),
  sanctionHit: Annotation<boolean | undefined>(),
  riskGrade: Annotation<"A" | "B" | "C" | "D" | undefined>(),
  decision: Annotation<LoanDecision | undefined>(),
  reasons: Annotation<string[] | undefined>(),
});

2) Add deterministic nodes for policy checks and enrichment

Keep hard policy logic outside the LLM path. For investment banking use cases, the first pass should be explainable and reproducible.

const calculateDscr = (state: LoanState): Partial<LoanState> => {
  const debtService = Math.max(state.existingDebt / 12, 1);
  const dscr = state.annualRevenue / debtService;

  return {
    dscr,
    reasons: [...(state.reasons ?? []), `Calculated DSCR=${dscr.toFixed(2)}`],
    kycPassed: true,
    sanctionHit: false,
    riskGrade: dscr >= 2.0 ? "A" : dscr >= 1.5 ? "B" : dscr >= 1.2 ? "C" : "D",
  };
};

const applyPolicy = (state: LoanState): Partial<LoanState> => {
    const reasons = [...(state.reasons ?? [])];

    if (state.sanctionHit) {
      return { decision: "reject", reasons: [...reasons, "Sanctions hit detected"] };
    }

    if (!state.kycPassed) {
      return { decision: "manual_review", reasons: [...reasons, "KYC not completed"] };
    }

    if ((state.dscr ?? 0) < 1.25) {
      return { decision: "reject", reasons: [...reasons, `DSCR below threshold (${state.dscr?.toFixed(2)})`] };
    }

    if ((state.riskGrade === "C") || state.riskGrade === "D") {
      return { decision: "manual_review", reasons: [...reasons, `Risk grade ${state.riskGrade} requires analyst review`] };
    }

    return { decision: "approve", reasons: [...reasons, "Meets automated approval criteria"] };
};

3) Build routing with StateGraph and compile it

This is where LangGraph fits well. You get explicit transitions instead of hidden orchestration logic spread across services.

const graph = new StateGraph(LoanStateSchema)
  .addNode("enrich", calculateDscr)
  .addNode("policy", applyPolicy)
  
import { START } from "@langchain/langgraph";

const workflow = new StateGraph(LoanStateSchema)
  .addNode("enrich", calculateDscr)
  

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