How to Build a policy Q&A Agent Using CrewAI in TypeScript for retail banking

By Cyprian AaronsUpdated 2026-04-21
policy-q-acrewaitypescriptretail-bankingpolicy-qanda

A policy Q&A agent for retail banking answers customer-facing and internal policy questions from approved sources like product terms, fee schedules, lending rules, and compliance playbooks. It matters because frontline teams need fast, consistent answers without exposing sensitive data or letting an LLM invent policy.

Architecture

  • User interface
    • Web chat, CRM sidebar, or internal ops tool where staff ask policy questions.
  • Policy knowledge source
    • Versioned documents: deposit account terms, card dispute policies, KYC/AML procedures, complaints handling guides.
  • Retrieval layer
    • Search over approved policy content only, with document chunking and metadata like jurisdiction, product line, and effective date.
  • CrewAI agent
    • A single policy specialist agent that answers only from retrieved context and refuses unsupported claims.
  • Guardrails
    • PII redaction, prompt-injection checks, jurisdiction filtering, and refusal rules for regulated advice.
  • Audit logging
    • Store question, retrieved sources, answer, model version, and timestamp for compliance review.

Implementation

1) Install the TypeScript stack

Use the TypeScript SDK for CrewAI plus a retrieval store. In a banking setup, keep the vector index in-region and store document metadata for audit and residency controls.

npm install @crewai/crewai openai zod
npm install @pinecone-database/pinecone

Set environment variables for the model provider and your vector store:

export OPENAI_API_KEY="..."
export PINECONE_API_KEY="..."
export PINECONE_INDEX="retail-banking-policies"

2) Define the policy agent with strict instructions

The agent should answer only from retrieved policy context. If the context is missing or ambiguous, it should say so instead of guessing.

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

export const policyAgent = new Agent({
  name: "Retail Banking Policy Assistant",
  role: "Answer retail banking policy questions using approved internal documents",
  goal:
    "Provide accurate, compliant answers grounded only in supplied policy context",
  backstory:
    "You support frontline banking teams. You never invent policy, never provide legal advice, and always cite the relevant source excerpt when possible.",
  verbose: true,
  allowDelegation: false,
  maxIter: 2,
});

3) Retrieve approved policy context before calling CrewAI

CrewAI works best when you pass a clean task context. In banking, retrieval should filter by product and jurisdiction so a UK overdraft rule does not leak into a US answer.

import { Pinecone } from "@pinecone-database/pinecone";

const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
const index = pinecone.index(process.env.PINECONE_INDEX!);

type PolicyChunk = {
  id: string;
  text: string;
  source: string;
  jurisdiction: string;
  product: string;
};

async function retrievePolicyContext(
  question: string,
  jurisdiction: string,
  product: string
): Promise<PolicyChunk[]> {
  const queryResponse = await index.query({
    topK: 5,
    includeMetadata: true,
    vector: await embedQuestion(question),
    filter: {
      jurisdiction: { $eq: jurisdiction },
      product: { $eq: product },
      status: { $eq: "approved" },
    },
  });

  return (queryResponse.matches ?? []).map((match) => ({
    id: match.id,
    text: String(match.metadata?.text ?? ""),
    source: String(match.metadata?.source ?? ""),
    jurisdiction: String(match.metadata?.jurisdiction ?? ""),
    product: String(match.metadata?.product ?? ""),
  }));
}

// Replace with your embedding provider call.
async function embedQuestion(_question: string): Promise<number[]> {
  return new Array(1536).fill(0);
}

4) Create the task and run the crew

This is the core pattern. The task includes the user question plus retrieved excerpts. The agent then produces an answer constrained to that context.

import { Crew } from "@crewai/crewai";
import { z } from "zod";

const AnswerSchema = z.object({
  answer: z.string(),
  citations: z.array(z.string()),
});

export async function answerPolicyQuestion(input: {
  question: string;
  jurisdiction: string;
  product: string;
}) {
  const chunks = await retrievePolicyContext(
    input.question,
    input.jurisdiction,
    input.product
  );

  const contextBlock = chunks
    .map(
      (c) =>
        `SOURCE=${c.source}\nJURISDICTION=${c.jurisdiction}\nPRODUCT=${c.product}\nTEXT=${c.text}`
    )
    .join("\n\n---\n\n");

  const crew = new Crew({
    agents: [policyAgent],
    tasks: [
      {
        description:
          `Answer this retail banking policy question using only the provided context.\n\nQuestion:\n${input.question}\n\nContext:\n${contextBlock}`,
        expectedOutput:
          "A concise answer with citations to the provided sources. If insufficient context exists, say what is missing.",
        agentIdOrName: "Retail Banking Policy Assistant",
      },
    ],
    verboseOutputFormattersEnabled: false,
  });

  const result = await crew.kickoff();

  return AnswerSchema.parse({
    answer:
      typeof result === "string" ? result : JSON.stringify(result),
    citations: chunks.map((c) => c.source),
  });
}

Production Considerations

  • Deployment
    • Keep retrieval and inference in the same region as your banking data to satisfy residency requirements.
    • Separate environments for dev, UAT, and production; never point production prompts at non-approved content.
  • Monitoring
    • Log every question with retrieved document IDs, model version, latency, refusal rate, and escalation rate.
    • Track “no-answer” responses; in banking that usually means your knowledge base is stale or too narrow.
  • Guardrails
    • Block prompts that ask for legal interpretation beyond published policy.
    • Redact account numbers, SSNs/NINs, PANs, and other PII before sending text to the model.
  • Compliance controls
    • Keep an immutable audit trail of source excerpts used in each response.
    • Version policies by effective date so you can reproduce historical answers during disputes.
ConcernWhat to doWhy it matters
Data residencyHost embeddings/indexes in-regionAvoid cross-border data transfer issues
AuditabilityPersist sources + final answerSupports complaints handling and reviews
Compliance driftRe-index on policy updatesPrevent stale answers
HallucinationsForce retrieval-first answersKeeps responses grounded in approved docs

Common Pitfalls

  1. Letting the agent answer without retrieval

    • Fix it by making retrieval mandatory before every kickoff().
    • If no approved chunks are found, return a refusal or escalation path.
  2. Mixing jurisdictions in one prompt

    • Fix it by filtering on jurisdiction, product, and effective_date.
    • Retail banking policies differ by country and even by state or province.
  3. Treating policy text as plain chat history

    • Fix it by separating user input from trusted context blocks.
    • Mark retrieved excerpts clearly so prompt injection inside uploaded documents does not override instructions.
  4. Skipping audit logs

    • Fix it by storing question ID, source IDs, response hash, timestamp, and model version.
    • In retail banking you need to explain how an answer was produced after the fact.

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