LlamaIndex Tutorial (TypeScript): implementing guardrails for intermediate developers

By Cyprian AaronsUpdated 2026-04-21
llamaindeximplementing-guardrails-for-intermediate-developerstypescript

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
    • tsx or ts-node for running TypeScript directly
  • A .env file with:
    • OPENAI_API_KEY=...

Install everything:

npm install llamaindex zod dotenv
npm install -D typescript tsx @types/node

Step-by-Step

  1. 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.",
  };
}
  1. 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);
}
  1. 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);
}
  1. 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);
});
  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");
}
  1. 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

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