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

By Cyprian AaronsUpdated 2026-04-21
loan-approvalllamaindextypescriptbanking

A loan approval agent automates the first pass of credit decisioning: it gathers applicant data, checks policy and regulatory rules, retrieves supporting documents, and produces a structured recommendation for a human underwriter. For banking, this matters because it reduces manual review time, enforces consistent policy application, and creates an audit trail for every decision path.

Architecture

  • Applicant intake layer

    • Accepts structured inputs like income, employment status, debt-to-income ratio, and requested amount.
    • Validates schema before any model call.
  • Policy retrieval layer

    • Uses LlamaIndex to fetch bank policy documents, underwriting rules, product constraints, and regional compliance notes.
    • Keeps the agent grounded in current internal policy.
  • Decision engine

    • Combines deterministic checks with LLM reasoning.
    • Produces one of three outputs: approve, refer to manual review, or decline.
  • Audit logger

    • Stores input payloads, retrieved policy snippets, model output, and final recommendation.
    • Required for model governance and regulatory review.
  • Guardrail layer

    • Blocks unsupported recommendations.
    • Prevents the agent from using sensitive attributes like race, religion, or health data.
  • Human-in-the-loop handoff

    • Routes borderline cases to an underwriter with a concise explanation and evidence bundle.

Implementation

1) Install LlamaIndex TypeScript packages

Use the TypeScript SDK and a chat model provider. For this example, I’m using OpenAI-compatible APIs plus local document indexing.

npm install llamaindex zod dotenv

Set your environment variables:

export OPENAI_API_KEY="your-key"

2) Load banking policy documents into an index

The agent needs retrieval over underwriting rules. In practice, these are PDFs or text exports from your policy repository. The key pattern is: load docs once, build an index, then query that index at decision time.

import "dotenv/config";
import {
  SimpleDirectoryReader,
  VectorStoreIndex,
  Settings,
} from "llamaindex";

async function buildPolicyIndex() {
  const reader = new SimpleDirectoryReader();
  const documents = await reader.loadData("./bank-policy");

  const index = await VectorStoreIndex.fromDocuments(documents);

  return index;
}

async function main() {
  Settings.chunkSize = 1024;
  const index = await buildPolicyIndex();
  console.log("Policy index ready:", !!index);
}

main();

This gives you retrieval over policy text without hardcoding rules into prompts. For banking teams, that matters because policy changes frequently and must be versioned.

3) Create a structured loan decision workflow

Do not let the model freewheel. Wrap the request in a schema and enforce deterministic checks before the LLM sees anything. Then retrieve supporting policy context and ask for a structured recommendation.

import "dotenv/config";
import { z } from "zod";
import {
  OpenAI,
  VectorStoreIndex,
  QueryEngineTool,
} from "llamaindex";

const LoanApplicationSchema = z.object({
  applicantId: z.string(),
  annualIncome: z.number().positive(),
  monthlyDebtPayments: z.number().nonnegative(),
  requestedAmount: z.number().positive(),
  employmentYears: z.number().nonnegative(),
});

type LoanApplication = z.infer<typeof LoanApplicationSchema>;

function calculateDTI(app: LoanApplication) {
  return app.monthlyDebtPayments / (app.annualIncome / 12);
}

async function getDecision(index: VectorStoreIndex, app: LoanApplication) {
  const dti = calculateDTI(app);

  if (dti > 0.45) {
    return {
      recommendation: "refer",
      reason: `DTI ${dti.toFixed(2)} exceeds policy threshold`,
    };
    }

    const queryEngine = index.asQueryEngine();
    const tool = QueryEngineTool.fromDefaults({
      queryEngine,
      name: "bank_policy_search",
      description: "Searches underwriting policies for loan approval criteria",
    });

    const llm = new OpenAI({ model: "gpt-4o-mini" });

    const prompt = `
You are a banking underwriting assistant.
Use only the provided policy context.
Return JSON with keys:
recommendation (approve|refer|decline),
reason,
policyReferences (array of strings).

Applicant:
${JSON.stringify(app)}

DTI:
${dti.toFixed(2)}
`;

    const response = await llm.complete({
      prompt,
      tools: [tool],
    });

    return response.text;
}

The pattern here is important:

  • validate inputs with zod
  • compute hard rules outside the model
  • use retrieval for policy grounding
  • force a machine-readable response

That keeps the agent auditable and easier to test against lending standards.

4) Add audit logging and human review routing

Every decision needs traceability. Store the raw input, computed risk metrics, retrieved context IDs, model output, and final action in your audit store.

type AuditRecord = {
  applicantId: string;
  input: LoanApplication;
  dti: number;
  decisionText: string;
  timestamp: string;
};

async function logAudit(record: AuditRecord) {
  console.log(JSON.stringify(record));
}

async function processApplication(index: VectorStoreIndex, rawInput: unknown) {
  const app = LoanApplicationSchema.parse(rawInput);
  const dti = calculateDTI(app);
  
   const decisionText = await getDecision(index, app);

   await logAudit({
     applicantId: app.applicantId,
     input: app,
     dti,
     decisionText:
       typeof decisionText === "string" ? decisionText : JSON.stringify(decisionText),
     timestamp: new Date().toISOString(),
   });

   return decisionText;
}

In production you would replace console.log with immutable storage in your SIEM or audit database. For regulated lending workflows, you need replayable evidence for every recommendation.

Production Considerations

  • Data residency

    • Keep applicant PII and policy indexes in-region.
    • If your bank operates across jurisdictions, isolate indexes per region to avoid cross-border data movement violations.
  • Compliance controls

    • Block protected attributes at ingestion.
    • Maintain a clear separation between eligibility factors and prohibited attributes under fair lending rules.
  • Monitoring

    • Track approval rate drift by segment, refer rate spikes, retrieval failures, and latency.
    • Alert when the agent starts over-relying on “refer” or when policy retrieval returns low-confidence matches.
  • Human override path

    • Every decline above a threshold amount should route to an underwriter.
    • Store reviewer overrides so you can measure model disagreement patterns over time.

Common Pitfalls

  1. Letting the LLM make final decisions without deterministic checks

    • Avoid this by calculating hard thresholds like DTI, income minimums, or loan-to-value outside the model.
    • Use the model for explanation and synthesis, not core eligibility logic.
  2. Retrieving stale or unversioned policy content

    • Avoid this by versioning document sources and attaching policy revision IDs to every audit record.
    • If underwriting changes weekly, rebuild or reindex on release boundaries.
  3. Ignoring explainability requirements

    • Avoid free-form outputs that cannot be traced back to evidence.
    • Force JSON output with explicit policyReferences, then store those references alongside the application record.

If you build it this way, the agent stays useful to underwriters without becoming a compliance liability. The rule is simple: deterministic where possible, retrieval-grounded where necessary, and always auditable.


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