How to Build a policy Q&A Agent Using CrewAI in TypeScript for retail banking
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.
| Concern | What to do | Why it matters |
|---|---|---|
| Data residency | Host embeddings/indexes in-region | Avoid cross-border data transfer issues |
| Auditability | Persist sources + final answer | Supports complaints handling and reviews |
| Compliance drift | Re-index on policy updates | Prevent stale answers |
| Hallucinations | Force retrieval-first answers | Keeps responses grounded in approved docs |
Common Pitfalls
- •
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.
- •Fix it by making retrieval mandatory before every
- •
Mixing jurisdictions in one prompt
- •Fix it by filtering on
jurisdiction,product, andeffective_date. - •Retail banking policies differ by country and even by state or province.
- •Fix it by filtering on
- •
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.
- •
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
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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