LlamaIndex Tutorial (TypeScript): implementing guardrails for advanced developers

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

This tutorial shows how to add guardrails around a LlamaIndex TypeScript agent so it only answers within policy, refuses risky prompts, and validates tool outputs before they reach the user. You need this when your agent is going into a bank or insurance workflow where “best effort” is not acceptable and you need deterministic failure modes.

What You'll Need

  • Node.js 18+
  • A TypeScript project
  • llamaindex installed
  • zod for schema validation
  • An OpenAI API key in OPENAI_API_KEY
  • Optional: a local policy file if you want to externalize rules later

Install the packages:

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

Set your environment variable:

export OPENAI_API_KEY="your-key-here"

Step-by-Step

  1. Start with a strict policy object.
    The guardrail should be explicit, not buried in prompt text. This gives you one place to enforce allowed topics, refusal behavior, and output shape.
import { z } from "zod";

export const GuardrailDecisionSchema = z.object({
  allowed: z.boolean(),
  reason: z.string(),
});

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

export const POLICY = {
  blockedTopics: ["legal advice", "medical advice", "investment advice"],
  allowedTopics: ["claims status", "policy details", "coverage explanation"],
};
  1. Add a pre-check that classifies the user request before it reaches the agent.
    In production, this is where you stop obvious policy violations early and cheaply. Keep it deterministic and easy to audit.
import { OpenAI } from "llamaindex";
import { GuardrailDecisionSchema, POLICY } from "./policy.js";

const llm = new OpenAI({ model: "gpt-4o-mini" });

export async function preCheck(userInput: string) {
  const prompt = `
You are a policy classifier.
Allowed topics: ${POLICY.allowedTopics.join(", ")}.
Blocked topics: ${POLICY.blockedTopics.join(", ")}.

Return JSON with keys: allowed (boolean), reason (string).
User input: ${userInput}
`;

  const raw = await llm.complete(prompt);
  const parsed = GuardrailDecisionSchema.parse(JSON.parse(raw.text));
  return parsed;
}
  1. Build the main answer path with structured output validation.
    This keeps the model from returning malformed responses or unstructured text when your downstream system expects JSON. If parsing fails, you can fail closed instead of guessing.
import { OpenAI } from "llamaindex";
import { AnswerSchema } from "./policy.js";

const llm = new OpenAI({ model: "gpt-4o-mini" });

export async function generateAnswer(userInput: string) {
  const prompt = `
Answer only using the provided context of a customer support assistant.
Return JSON with keys: answer (string), citations (string[]).

User question: ${userInput}
`;

  const raw = await llm.complete(prompt);
  const data = AnswerSchema.parse(JSON.parse(raw.text));
  return data;
}
  1. Wrap both checks into one guarded entry point.
    This is the part your application calls. It blocks unsafe requests before generation and rejects malformed outputs after generation.
import { preCheck } from "./precheck.js";
import { generateAnswer } from "./answer.js";

export async function guardedChat(userInput: string) {
  const decision = await preCheck(userInput);

  if (!decision.allowed) {
    return {
      status: "blocked",
      message: `Request rejected by guardrail: ${decision.reason}`,
    };
  }

  const result = await generateAnswer(userInput);

  return {
    status: "ok",
    ...result,
  };
}
  1. Add a runnable CLI so you can test the guardrail locally.
    This gives you a fast feedback loop before wiring it into an API route or agent workflow.
import { guardedChat } from "./guarded-chat.js";

async function main() {
  const input = process.argv.slice(2).join(" ");
  if (!input) {
    console.log("Usage: tsx src/index.ts \"your question\"");
    process.exit(1);
  }

  const result = await guardedChat(input);
  console.log(JSON.stringify(result, null, 2));
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Testing It

Run the CLI with a safe prompt like What does my coverage include?. You should get back a JSON object with status: "ok" and a validated answer field.

Then try a blocked prompt like Should I sue my insurer?. The pre-check should reject it before any answer is generated.

Also test malformed output handling by temporarily changing the prompt so the model returns plain text instead of JSON. Your parser should throw, which is what you want in a fail-closed setup.

For integration tests, mock llm.complete() and verify three cases:

  • Allowed input returns structured output
  • Blocked input never reaches generation
  • Invalid JSON causes hard failure

Next Steps

  • Move policy rules into versioned config and load them per tenant or product line
  • Add retrieval-time guardrails so documents are filtered before they reach the model
  • Replace prompt-based classification with a dedicated moderation model for stricter control

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