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

By Cyprian AaronsUpdated 2026-04-21
loan-approvalautogentypescriptbanking

A loan approval agent automates the first-pass decisioning flow for banking applications: it gathers applicant data, checks policy rules, calls risk and affordability tools, and produces a recommendation with an audit trail. It matters because banks need faster turnaround without losing control over compliance, explainability, and human override.

Architecture

  • Applicant intake layer

    • Accepts structured loan applications from a web app, CRM, or core banking system.
    • Normalizes fields like income, employment type, requested amount, term, and jurisdiction.
  • Policy and compliance agent

    • Applies bank-specific lending rules.
    • Checks hard constraints such as minimum income thresholds, prohibited geographies, KYC/AML flags, and product eligibility.
  • Tool layer

    • Exposes deterministic functions for credit score lookup, debt-to-income calculation, affordability checks, and sanctions screening.
    • Keeps the LLM away from making numeric decisions it should not own.
  • Decision orchestrator

    • Uses AutoGen agents to coordinate the workflow.
    • Produces one of three outcomes: approve, reject, or escalate to manual review.
  • Audit and logging layer

    • Stores every input, tool call, intermediate reasoning artifact you choose to retain, and final recommendation.
    • Supports model governance and regulator review.
  • Human review queue

    • Captures borderline cases.
    • Lets underwriters override the agent with a reason code.

Implementation

1) Install AutoGen for TypeScript and define your domain types

Use the TypeScript AutoGen package and keep the application state explicit. Banking workflows break when you let message payloads become loosely typed blobs.

npm install @autogen/core zod
import { z } from "zod";

export const LoanApplicationSchema = z.object({
  applicantId: z.string(),
  country: z.string(),
  requestedAmount: z.number().positive(),
  annualIncome: z.number().nonnegative(),
  monthlyDebtPayments: z.number().nonnegative(),
  creditScore: z.number().int().min(300).max(850),
  kycPassed: z.boolean(),
});

export type LoanApplication = z.infer<typeof LoanApplicationSchema>;

export type LoanDecision =
  | { decision: "approve"; reason: string; aprBand: string }
  | { decision: "reject"; reason: string }
  | { decision: "manual_review"; reason: string };

2) Register deterministic banking tools

Keep calculations outside the model. The model should choose when to call a tool; the tool should return the truth.

export function calculateDti(app: LoanApplication): number {
  const monthlyIncome = app.annualIncome / 12;
  if (monthlyIncome === 0) return Infinity;
  return app.monthlyDebtPayments / monthlyIncome;
}

export function affordabilityCheck(app: LoanApplication): boolean {
  const dti = calculateDti(app);
  return dti <= 0.35 && app.requestedAmount <= app.annualIncome * 0.4;
}

export function hardPolicyCheck(app: LoanApplication): string[] {
  const violations: string[] = [];
  if (!app.kycPassed) violations.push("KYC_FAILED");
  if (app.creditScore < 580) violations.push("LOW_CREDIT_SCORE");
  if (app.country !== "US") violations.push("OUT_OF_RESIDENCY_SCOPE");
  return violations;
}

3) Build an AutoGen agent that orchestrates the decision

The key pattern is a single assistant agent that can call your tools and then emit a structured recommendation. In AutoGen TS, you create an agent with AssistantAgent, register tools with registerTool, then run a chat via run.

import { AssistantAgent } from "@autogen/core";

const loanOfficer = new AssistantAgent({
  name: "loan_officer_agent",
});

loanOfficer.registerTool({
  name: "hardPolicyCheck",
  description: "Checks mandatory lending policy constraints.",
  parameters: LoanApplicationSchema,
  execute: async (app: LoanApplication) => hardPolicyCheck(app),
});

loanOfficer.registerTool({
  name: "calculateDti",
      description: "Calculates debt-to-income ratio.",
      parameters: LoanApplicationSchema,
      execute: async (app: LoanApplication) => calculateDti(app),
});

loanOfficer.registerTool({
      name: "affordabilityCheck",
      description: "Checks whether the applicant passes affordability rules.",
      parameters: LoanApplicationSchema,
      execute: async (app: LoanApplication) => affordabilityCheck(app),
});

export async function assessLoan(appInput: unknown): Promise<LoanDecision> {
    const app = LoanApplicationSchema.parse(appInput);

    const result = await loanOfficer.run([
      {
        role: "user",
        content:
          `Assess this loan application using policy first, then affordability. ` +
          `Return only one of approve/reject/manual_review with a short reason.`,
      },
      {
        role: "user",
        content: JSON.stringify(app),
      },
    ]);

    const text = result.messages.at(-1)?.content ?? "";

    if (text.includes("approve")) {
      return { decision: "approve", reason: text.slice(0, 120), aprBand: "prime" };
    }
    if (text.includes("reject")) {
      return { decision: "reject", reason: text.slice(0, 120) };
    }
    return { decision: "manual_review", reason: text.slice(0,120) };
}

4) Add a second agent for audit-safe explanations

For banking, you want a separate explanation path that summarizes what happened without exposing chain-of-thought or sensitive internal prompts. Use another AssistantAgent to generate concise customer-facing or auditor-facing summaries from structured facts only.

const explainer = new AssistantAgent({ name: "decision_explainer" });

export async function buildAuditSummary(
   app : LoanApplication,
   decision : LoanDecision
): Promise<string> {
   const dti = calculateDti(app);
   const violations = hardPolicyCheck(app);

   const summary = await explainer.run([
     {
       role : "user",
       content : JSON.stringify({
         applicantId : app.applicantId,
         creditScore : app.creditScore,
         dti,
         violations,
         decision,
       }),
     },
   ]);

   return summary.messages.at(-1)?.content ?? "";
}

Production Considerations

  • Deploy in-region

    • Keep application data in approved banking regions to satisfy data residency requirements.
    • If you operate across jurisdictions, route requests by country or business unit.
  • Log everything needed for audit

    • Persist input payload hashes, tool outputs, model version, prompt version, and final decision.
    • Store enough to reconstruct why a case was approved or escalated during model risk review.
  • Add hard guardrails before model output is accepted


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