Haystack Tutorial (TypeScript): implementing guardrails for advanced developers

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

This tutorial shows you how to add guardrails around a Haystack TypeScript pipeline so your agent only answers within policy, refuses unsafe requests, and validates outputs before they reach users. You need this when your LLM is good enough to be useful but not reliable enough to be trusted raw, especially in banking, insurance, or any workflow with compliance constraints.

What You'll Need

  • Node.js 18+
  • TypeScript 5+
  • A Haystack project with the TypeScript SDK installed
  • An OpenAI API key
  • Basic familiarity with Pipeline, PromptBuilder, and OpenAIChatGenerator
  • A local .env file or shell environment variable for OPENAI_API_KEY

Install the packages:

npm install @haystack-ai/core @haystack-ai/integrations dotenv zod
npm install -D typescript tsx @types/node

Step-by-Step

  1. Start by defining the policy surface area. For guardrails, keep it explicit: what topics are allowed, what must be refused, and what format the output must follow.
import { z } from "zod";

export const PolicySchema = z.object({
  topic: z.enum(["claims", "billing", "policy", "general"]),
  allowAnswer: z.boolean(),
  refusalReason: z.string().optional(),
});

export const AnswerSchema = z.object({
  answer: z.string().min(1),
  confidence: z.number().min(0).max(1),
});
  1. Build a classifier step that decides whether the user request is allowed. In production, this can be another model call; here we use a deterministic rule so the guardrail is executable and testable as-is.
import { PolicySchema } from "./policy";

export function classifyRequest(input: string) {
  const lowered = input.toLowerCase();

  if (lowered.includes("password") || lowered.includes("ssn") || lowered.includes("credit card")) {
    return PolicySchema.parse({
      topic: "general",
      allowAnswer: false,
      refusalReason: "Request contains sensitive data handling.",
    });
  }

  if (lowered.includes("claim")) {
    return PolicySchema.parse({ topic: "claims", allowAnswer: true });
  }

  if (lowered.includes("invoice") || lowered.includes("premium")) {
    return PolicySchema.parse({ topic: "billing", allowAnswer: true });
  }

  return PolicySchema.parse({ topic: "general", allowAnswer: true });
}
  1. Create a prompt builder that forces bounded behavior. The important part is not “being helpful”; it is constraining the model to answer only inside policy and to return a structured response you can validate.
import { PromptBuilder } from "@haystack-ai/core";
import { AnswerSchema } from "./policy";

export const promptBuilder = new PromptBuilder({
  template:
    `You are a regulated assistant.
Allowed topic: {{topic}}
User request: {{question}}

Rules:
- If the request is outside the allowed topic, refuse.
- Never invent policy details.
- Return JSON with keys answer and confidence only.
- Keep answer concise.`,
  variables: ["topic", "question"],
});

export function validateAnswer(rawText: string) {
  const parsed = JSON.parse(rawText);
  return AnswerSchema.parse(parsed);
}
  1. Wire the guardrail into a Haystack pipeline. The flow is simple: classify first, refuse early if needed, then generate, then validate output before returning it.
import "dotenv/config";
import { Pipeline } from "@haystack-ai/core";
import { OpenAIChatGenerator } from "@haystack-ai/integrations";
import { classifyRequest } from "./classifier";
import { promptBuilder, validateAnswer } from "./prompt";

const generator = new OpenAIChatGenerator({
  model: "gpt-4o-mini",
  apiKey: process.env.OPENAI_API_KEY!,
});

const pipeline = new Pipeline();

pipeline.addComponent("promptBuilder", promptBuilder);
pipeline.addComponent("llm", generator);

pipeline.connect("promptBuilder.prompt", "llm.messages");

export async function guardedAnswer(question: string) {
  const policy = classifyRequest(question);

  if (!policy.allowAnswer) {
    return {
      answer: `Refused: ${policy.refusalReason}`,
      confidence: 1,
    };
  }

  const result = await pipeline.run({
    promptBuilder: {
      topic: policy.topic,
      question,
    },
  });

  const raw = result.llm.replies[0].content;
  return validateAnswer(raw);
}
  1. Add a small executable entrypoint so you can test both allowed and blocked requests. This is where you verify that the guardrail blocks sensitive prompts before they ever hit the model.
import { guardedAnswer } from "./pipeline";

async function main() {
  const safe = await guardedAnswer("How do I check claim status?");
  console.log("SAFE:", safe);

  const blocked = await guardedAnswer("What is my SSN and password?");
  console.log("BLOCKED:", blocked);
}

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

Testing It

Run the script with npx tsx src/main.ts. You should see one structured JSON response for the allowed request and one refusal object for the blocked request.

Then try prompts that are close to policy boundaries, like “Explain premium adjustments” or “Help me recover my account password.” The first should pass through; the second should be refused before generation.

For deeper validation, intentionally break the model output by changing the prompt to ask for plain text instead of JSON. Your validateAnswer() step should fail fast, which is exactly what you want in production.

Next Steps

  • Replace the deterministic classifier with a lightweight moderation model or rules engine backed by your compliance taxonomy.
  • Add a second validation layer for PII detection on both input and output.
  • Persist guardrail decisions as audit logs so reviewers can trace why a request was accepted or refused.

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