How to Build a underwriting Agent Using LlamaIndex in TypeScript for banking

By Cyprian AaronsUpdated 2026-04-21
underwritingllamaindextypescriptbanking

An underwriting agent in banking takes a loan or credit application, pulls the right internal and external documents, evaluates them against policy, and produces a decision packet with reasons. It matters because underwriters spend most of their time gathering evidence and checking policy fit; the agent handles that repeatable work while keeping humans in control of the final decision.

Architecture

  • Document ingestion layer

    • Pulls application forms, bank statements, KYC files, income proofs, and credit policy docs.
    • Normalizes PDFs, text files, and structured metadata into LlamaIndex Document objects.
  • Indexing layer

    • Builds a vector index over policy manuals, historical underwriting guidelines, and product eligibility rules.
    • Uses VectorStoreIndex so the agent can retrieve relevant clauses fast.
  • Retrieval + reasoning layer

    • Uses a query engine to answer questions like “Does this applicant meet debt-to-income limits?”
    • Combines retrieved policy text with application facts to generate a grounded recommendation.
  • Decision orchestration layer

    • Runs deterministic checks first: missing docs, hard policy thresholds, residency restrictions.
    • Sends only the ambiguous cases to the LLM-backed reasoning path.
  • Audit and trace layer

    • Stores retrieved sources, prompts, outputs, and timestamps.
    • Required for model governance, compliance review, and adverse action explainability.

Implementation

1) Install dependencies and load your banking documents

Use LlamaIndex’s TypeScript package plus a filesystem reader for local documents. In production, swap the file loader for your DMS or object storage connector.

npm install llamaindex
import {
  Document,
  VectorStoreIndex,
  Settings,
} from "llamaindex";
import { SimpleDirectoryReader } from "llamaindex/readers/SimpleDirectoryReader";

async function loadDocuments() {
  const reader = new SimpleDirectoryReader();
  const docs = await reader.loadData({
    directoryPath: "./banking-data",
  });

  return docs.map(
    (d) =>
      new Document({
        text: d.text,
        metadata: {
          source: d.metadata?.fileName ?? "unknown",
          type: d.metadata?.fileType ?? "unknown",
          tenant: "retail-credit",
        },
      }),
  );
}

This is where you keep policy docs separate from customer data. In regulated environments, that separation matters for access control and data residency.

2) Build a policy index for underwriting rules

The underwriting agent should retrieve from policy documents first. That gives you grounded answers instead of free-form model guesses.

async function buildPolicyIndex() {
  const docs = await loadDocuments();

  Settings.chunkSize = 1024;
  Settings.chunkOverlap = 150;

  const index = await VectorStoreIndex.fromDocuments(docs);
  return index;
}

For banking use cases, keep the indexed corpus narrow:

  • credit policy manuals
  • product eligibility rules
  • exceptions matrix
  • adverse action templates

Do not mix raw customer PII into the same long-lived index unless your controls are tight enough for audit and retention requirements.

3) Create a query engine that explains decisions with sources

The key pattern is retrieval-first generation. The query engine answers using retrieved policy passages so every recommendation can be traced back to source text.

import { QueryEngineTool } from "llamaindex";

async function buildUnderwritingEngine() {
  const index = await buildPolicyIndex();
  const queryEngine = index.asQueryEngine({
    similarityTopK: 4,
    responseMode: "compact",
  });

  const tool = QueryEngineTool.fromDefaults({
    queryEngine,
    name: "underwriting_policy_lookup",
    description:
      "Use this tool to retrieve underwriting policy clauses, eligibility criteria, and exception rules.",
  });

  return { queryEngine, tool };
}

async function runCheck() {
  const { queryEngine } = await buildUnderwritingEngine();

  const result = await queryEngine.query({
    query: [
      "Assess whether an applicant with DTI of 41%, stable employment of 26 months,",
      "and one recent late payment meets standard unsecured personal loan criteria.",
      "Return decision rationale with cited policy references.",
    ].join(" "),
  });

  console.log(String(result));
}

runCheck();

That query() call is the core pattern. In a real underwriting flow, you’d pass structured applicant facts into the prompt template or pre-check them before querying.

4) Add deterministic guardrails before any model call

Banking agents should not let the LLM decide everything. Hard rules like minimum age, residency restrictions, or required documentation should be enforced in code before retrieval.

type Applicant = {
  age: number;
  countryOfResidence: string;
  monthlyIncome: number;
  monthlyDebtPayments: number;
};

function validateApplicant(applicant: Applicant) {
  if (applicant.age < 18) throw new Error("Applicant below legal age");
  
});

function debtToIncome(applicant: Applicant) {
  
}

async function underwrite(applicant: Applicant) {
  
}

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