How to Build a underwriting Agent Using CrewAI in TypeScript for wealth management

By Cyprian AaronsUpdated 2026-04-21
underwritingcrewaitypescriptwealth-management

A underwriting agent for wealth management evaluates client profiles, portfolio data, risk tolerance, liquidity needs, and product constraints to decide whether a proposed investment or advisory action fits policy. It matters because bad underwriting in this domain is not just a bad recommendation — it can trigger suitability breaches, compliance issues, and expensive remediation.

Architecture

Build this agent as a small system, not a single prompt.

  • Input normalizer

    • Converts client data from CRM, portfolio systems, and KYC/AML sources into a consistent TypeScript object.
    • Validates required fields like jurisdiction, accreditation status, net worth band, and investment objective.
  • Policy retrieval layer

    • Pulls house rules, product eligibility rules, and jurisdiction-specific constraints.
    • Keeps the agent grounded in current compliance policy instead of relying on model memory.
  • Underwriting crew

    • Uses multiple Agent roles inside a Crew:
      • one for suitability analysis
      • one for compliance review
      • one for final decision synthesis
    • This separation gives you an audit trail per role.
  • Decision formatter

    • Produces structured output: approve, reject, or escalate.
    • Includes reasons, policy citations, and missing evidence.
  • Audit logger

    • Stores inputs, outputs, tool calls, and timestamps.
    • Required for model risk management and post-trade review.

Implementation

1) Install CrewAI for TypeScript and define your data model

You want strict types before the LLM ever sees the request. That keeps the agent from inventing fields and makes downstream auditing easier.

npm install @crewai/crew @crewai/agents @crewai/tasks zod
import { z } from "zod";

export const UnderwritingInputSchema = z.object({
  clientId: z.string(),
  jurisdiction: z.string(),
  netWorthUsd: z.number(),
  liquidAssetsUsd: z.number(),
  riskTolerance: z.enum(["conservative", "moderate", "growth", "aggressive"]),
  investmentObjective: z.string(),
  accreditedInvestor: z.boolean(),
  requestedProduct: z.string(),
});

export type UnderwritingInput = z.infer<typeof UnderwritingInputSchema>;

export type UnderwritingDecision = {
  decision: "approve" | "reject" | "escalate";
  rationale: string;
  policyReferences: string[];
};

2) Create specialized agents with explicit responsibilities

For wealth management, don’t let one agent do everything. Split suitability from compliance so you can inspect where a decision came from.

import { Agent } from "@crewai/agents";

export const suitabilityAgent = new Agent({
  role: "Suitability Analyst",
  goal: "Assess whether the requested product matches the client's financial profile and stated objective.",
  backstory:
    "You evaluate wealth management client suitability using conservative underwriting standards.",
});

export const complianceAgent = new Agent({
  role: "Compliance Reviewer",
  goal: "Check the request against jurisdictional rules, accreditation requirements, and firm policy.",
  backstory:
    "You enforce wealth management compliance controls and flag missing evidence or policy conflicts.",
});

export const decisionAgent = new Agent({
  role: "Underwriting Decision Maker",
  goal: "Synthesize analysis into an auditable approve/reject/escalate decision.",
  backstory:
    "You produce concise decisions with explicit policy references for audit review.",
});

3) Wire the crew with tasks and structured output

This is the actual pattern you want in production: one task per control point. The final task should emit a structured object that your API can persist without parsing free text.

import { Crew } from "@crewai/crew";
import { Task } from "@crewai/tasks";
import { UnderwritingInputSchema } from "./schema";

export async function underwriteRequest(rawInput: unknown) {
  const input = UnderwritingInputSchema.parse(rawInput);

  const suitabilityTask = new Task({
    description: `
      Review this wealth management request for suitability.
      Client profile: ${JSON.stringify(input)}
      Focus on liquidity fit, risk tolerance alignment, and objective consistency.
      Return key findings only.
    `,
    expectedOutput:
      "A short suitability assessment with any mismatches or missing information.",
    agent: suitabilityAgent,
  });

  const complianceTask = new Task({
    description: `
      Review this request for compliance issues.
      Client profile: ${JSON.stringify(input)}
      Check accreditation status, jurisdictional constraints, and product eligibility.
      Return policy concerns and escalation triggers.
    `,
    expectedOutput:
      "A short compliance assessment with explicit concerns or confirmation of pass.",
    agent: complianceAgent,
    context: [suitabilityTask],
  });

  const decisionTask = new Task({
    description: `
      Produce the final underwriting decision using prior analysis.
      Output JSON with keys:
      decision (approve|reject|escalate),
      rationale,
      policyReferences (string array).
      Be conservative when evidence is incomplete.
    `,
    expectedOutput:
      'Valid JSON matching { decision, rationale, policyReferences }',
    agent: decisionAgent,
    context: [suitabilityTask, complianceTask],
  });

  const crew = new Crew({
    agents: [suitabilityAgent, complianceAgent, decisionAgent],
    tasks: [suitabilityTask, complianceTask, decisionTask],
    verbose: true,
  });

  
import { CrewOutput } from "@crewai/crew";

export async function runUnderwriting(rawInput: unknown): Promise<UnderwritingDecision> {
  const result = await underwriteRequest(rawInput);

  
// Depending on your CrewAI TS version, access the final text via result.raw or result.output.
// Keep the contract at your API boundary strict and validate it before persistence.
const parsed = JSON.parse((result as any).raw ?? (result as any).output);
return parsed as UnderwritingDecision;
}

4) Add audit logging around every run

Wealth management teams need traceability. Log both the input snapshot and the final recommendation so reviewers can reconstruct why a case was approved or escalated.

type AuditEvent = {
  clientId: string;
  timestamp: string;
  inputHash?: string;
   decision?: UnderwritingDecision;
};

export async function runAndAudit(input: unknown) {
   const startedAt = new Date().toISOString();
   const decision = await runUnderwriting(input);

   const event: AuditEvent = {
     clientId: (input as any).clientId,
     timestamp: startedAt,
     decision,
   };

   console.log(JSON.stringify(event));
   return decision;
}

Production Considerations

  • Deployment

Keep the agent behind an internal service boundary. Use private networking and region-pinned infrastructure so client data stays within approved data residency zones.

  • Monitoring

Track approval rate, escalation rate, rejection reasons, latency per task, and percentage of cases requiring human override. In wealth management you also want drift monitoring on risk-tolerance mismatches and repeated compliance flags by jurisdiction.

  • Guardrails

Add deterministic checks before calling the crew:

  • accreditation must be present for restricted products
  • jurisdiction must match product distribution permissions
  • missing KYC fields should force escalation

The LLM should never be the first line of defense for these rules.

  • Auditability

Persist prompts, tool outputs if any are used later, model version IDs, timestamps, and final decisions. If a regulator asks why an account was approved or rejected, you need more than a natural-language summary.

Common Pitfalls

  1. Using one general-purpose agent for everything

    This usually produces vague decisions with no clear accountability. Split roles so suitability and compliance can fail independently.

  2. Letting the model infer missing regulated fields

    If jurisdiction, accreditedInvestor, or riskTolerance is absent, do not ask the model to guess. Escalate immediately or block execution until data is complete.

  3. Returning free-text decisions to downstream systems

    Wealth platforms need structured outputs. Validate JSON at the boundary with Zod or a similar schema library before storing or routing the result.

  4. Ignoring residency and retention requirements

    Client data may not be allowed to leave specific regions. Pin compute to approved regions and define retention rules for prompts and outputs based on firm policy.


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