LlamaIndex Tutorial (TypeScript): implementing guardrails for intermediate developers
This tutorial shows how to add guardrails to a LlamaIndex TypeScript agent so it only answers within a defined scope, rejects unsafe requests, and avoids hallucinating when the context is weak. You need this when your agent is going into a bank, insurance, or any workflow where “best effort” is not acceptable.
What You'll Need
- •Node.js 18+
- •A TypeScript project
- •An OpenAI API key
- •These packages:
- •
llamaindex - •
zod - •
dotenv - •
typescript - •
tsxorts-nodefor running TypeScript directly
- •
- •A
.envfile with:- •
OPENAI_API_KEY=...
- •
Install everything:
npm install llamaindex zod dotenv
npm install -D typescript tsx @types/node
Step-by-Step
- •Start by creating a small policy layer for your agent. The point is to classify user input before you send it to the model, so you can block obviously unsafe or out-of-scope requests early.
import "dotenv/config";
import { z } from "zod";
const GuardrailDecisionSchema = z.object({
allowed: z.boolean(),
reason: z.string(),
});
type GuardrailDecision = z.infer<typeof GuardrailDecisionSchema>;
export function localGuardrail(input: string): GuardrailDecision {
const blockedPatterns = [
/password/i,
/secret/i,
/credit card/i,
/ssn/i,
/social security/i,
];
if (blockedPatterns.some((pattern) => pattern.test(input))) {
return {
allowed: false,
reason: "Request contains sensitive data handling keywords.",
};
}
return {
allowed: true,
reason: "Input passed local policy checks.",
};
}
- •Next, build a small knowledge base and query engine. This example uses in-memory documents so you can focus on guardrails instead of wiring up storage.
import { Document, VectorStoreIndex } from "llamaindex";
const docs = [
new Document({
text: "Claims processing requires a policy number, incident date, and supporting evidence.",
metadata: { source: "claims-handbook" },
}),
new Document({
text: "Customer support can explain policy coverage but cannot approve exceptions.",
metadata: { source: "support-playbook" },
}),
];
export async function buildIndex() {
return await VectorStoreIndex.fromDocuments(docs);
}
- •Add an answer guardrail that checks whether the model response is grounded enough. In production, this is where you stop the agent from confidently inventing policy details.
import { OpenAI } from "llamaindex";
const llm = new OpenAI({ model: "gpt-4o-mini" });
export async function responseGuardrail(question: string, answer: string) {
const prompt = `
You are validating whether an assistant answer is safe to return.
Question:
${question}
Answer:
${answer}
Return JSON only:
{"allowed":true|false,"reason":"..."}
`;
const raw = await llm.complete(prompt);
const parsed = JSON.parse(raw.text.trim());
return GuardrailDecisionSchema.parse(parsed);
}
- •Now wire the pieces together into one request flow. The order matters: local policy first, retrieval second, response validation last.
import { QueryEngineTool } from "llamaindex";
import { buildIndex, localGuardrail, responseGuardrail } from "./guardrails";
async function main() {
const question = process.argv.slice(2).join(" ");
if (!question) throw new Error("Pass a question as CLI args.");
const preCheck = localGuardrail(question);
if (!preCheck.allowed) {
console.log(`Blocked: ${preCheck.reason}`);
return;
}
const index = await buildIndex();
const queryEngine = index.asQueryEngine();
const tool = new QueryEngineTool({
queryEngine,
metadata: {
name: "policy_knowledge_base",
description: "Internal claims and support policy reference",
},
});
const result = await tool.call({ input: question });
const answer = String(result);
const postCheck = await responseGuardrail(question, answer);
if (!postCheck.allowed) {
console.log(`Rejected answer: ${postCheck.reason}`);
return;
}
console.log(answer);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
- •Tighten the system by adding a fallback behavior instead of returning raw failures. For regulated workflows, “I don’t know” is better than a wrong answer.
export function safeFallback(reason: string) {
return [
"I can’t safely answer that from the available policy context.",
`Reason: ${reason}`,
"Please route this to a human reviewer or provide more specific internal documentation.",
].join("\n");
}
- •Replace hard failures with controlled responses when either guardrail fails. This keeps the UX predictable and gives downstream systems a stable contract.
import { safeFallback } from "./fallback";
if (!preCheck.allowed) {
console.log(safeFallback(preCheck.reason));
} else if (!postCheck.allowed) {
console.log(safeFallback(postCheck.reason));
} else {
console.log(answer);
}
Testing It
Run the script with an in-scope question like “What do I need to file a claim?” You should get an answer grounded in the two sample documents.
Then test an out-of-scope prompt like “What’s my customer’s SSN?” The local guardrail should block it before any LLM call is made.
Finally, try a vague question such as “Can I approve this exception?” If the retrieved context is weak or the model starts inventing details, the post-response guardrail should reject it and return your fallback message.
Next Steps
- •Add structured output validation with Zod for every tool call.
- •Replace the simple keyword filter with an LLM-based moderation step.
- •Store guardrail decisions in logs so compliance teams can audit rejections later.
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