Haystack Tutorial (TypeScript): building conditional routing for advanced developers

By Cyprian AaronsUpdated 2026-04-21
haystackbuilding-conditional-routing-for-advanced-developerstypescript

This tutorial shows how to build a Haystack pipeline in TypeScript that routes queries conditionally based on simple business rules. You need this when one prompt should not go through the same path every time, for example when a compliance question should hit a policy retriever while a general question should go to a standard RAG chain.

What You'll Need

  • Node.js 18+
  • TypeScript 5+
  • npm or pnpm
  • Haystack TypeScript package:
    • @haystack-ai/core
  • An OpenAI API key if you want to run the example with an LLM:
    • OPENAI_API_KEY
  • Optional but useful:
    • ts-node for quick execution
    • dotenv for loading environment variables

Step-by-Step

  1. Start by installing the package and setting up your environment. Keep this minimal; the point is to get a clean TypeScript project that can run Haystack components without extra framework code.
npm init -y
npm install @haystack-ai/core
npm install -D typescript ts-node @types/node
npx tsc --init
  1. Create a small router component that decides which branch to take. In production, this logic usually sits in front of two different pipelines: one for normal retrieval and one for high-risk or policy-sensitive requests.
import { Component } from "@haystack-ai/core";

type Route = "general" | "policy";

export class QueryRouter extends Component {
  declare inputTypes: { query: string };
  declare outputTypes: { route: Route };

  async run(inputs: { query: string }) {
    const q = inputs.query.toLowerCase();

    const policyTerms = ["compliance", "policy", "risk", "audit", "gdpr"];
    const route: Route = policyTerms.some((term) => q.includes(term))
      ? "policy"
      : "general";

    return { route };
  }
}
  1. Build two downstream components that represent the two branches. For this tutorial, they are simple deterministic responders, but in a real system these would be retrievers, prompt builders, or full LLM chains.
import { Component } from "@haystack-ai/core";

export class GeneralResponder extends Component {
  declare inputTypes: { query: string };
  declare outputTypes: { answer: string };

  async run(inputs: { query: string }) {
    return { answer: `General path selected for: ${inputs.query}` };
  }
}

export class PolicyResponder extends Component {
  declare inputTypes: { query: string };
  declare outputTypes: { answer: string };

  async run(inputs: { query: string }) {
    return { answer: `Policy path selected for: ${inputs.query}` };
  }
}
  1. Wire the components together with conditional routing in a pipeline. The important part is that the router output determines which branch receives the original query, so you avoid duplicating preprocessing logic.
import { Pipeline } from "@haystack-ai/core";
import { QueryRouter } from "./QueryRouter";
import { GeneralResponder } from "./GeneralResponder";
import { PolicyResponder } from "./PolicyResponder";

const pipeline = new Pipeline();

pipeline.addComponent("router", new QueryRouter());
pipeline.addComponent("general", new GeneralResponder());
pipeline.addComponent("policy", new PolicyResponder());

pipeline.connect("router.route", "general.__condition__");
pipeline.connect("router.route", "policy.__condition__");
pipeline.connect("router.query", "general.query");
pipeline.connect("router.query", "policy.query");

const result = await pipeline.run({ router: { query: "What is our compliance policy?" } });
console.log(result);
  1. Add a small wrapper so you can test both branches quickly from the command line. This is where you verify routing behavior before replacing the stub responders with real retrieval and generation components.
async function main() {
  const queries = [
    "How do I reset my password?",
    "Do we have GDPR compliance requirements?",
  ];

  for (const query of queries) {
    const result = await pipeline.run({ router: { query } });
    console.log(JSON.stringify(result, null, 2));
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
  1. Replace the stub responders with real business logic once routing is stable. The routing layer should stay thin; keep domain-specific retrieval and prompting inside the branch components so each path can evolve independently.
// Example shape only — replace responder internals with your own retrieval/LLM logic.
// The routing contract stays the same:
//
// router.route -> __condition__
// router.query -> branch.query
//
// That lets you swap branch implementations without changing upstream code.

Testing It

Run the script and confirm that queries containing terms like compliance, policy, or risk are sent to the policy branch. Then test ordinary user questions and confirm they stay on the general branch.

If you want stronger validation, add unit tests around QueryRouter.run() first. That gives you deterministic coverage for routing rules before you connect any external model or retriever.

For production debugging, log both the selected route and the original query. Routing bugs are usually not in Haystack itself; they come from ambiguous rules, missing normalization, or branch inputs wired incorrectly.

Next Steps

  • Replace keyword routing with a classifier component backed by an LLM or embedding-based scorer.
  • Add a third branch for fallback handling when confidence is low or no rule matches.
  • Wrap each branch in its own evaluation suite so you can measure route accuracy separately from answer quality.

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