skip to content

Components & Generators

You will learn what makes a Haystack component — a class with @component, declared output types, and a run method — plus the built-ins you assemble most: prompt builders and chat generators. Interviewers commonly ask you to write a custom component, so know the contract exactly.

part ofAI agent & RAG frameworksoverview, primer and where to startread it →
on this pageshow

questions

6

In Haystack, what does ChatPromptBuilder do inside a RAG pipeline?

level: juniorimportance: must knowfreq 62%

answer

  1. Renders, never calls the model
  2. Jinja2 over a message list
  3. Placeholders become run-time inputs
  4. Output socket named prompt, list of ChatMessage
  5. required_variables defaults to all

basics

~20 s

ChatPromptBuilder renders a Jinja2 template into a list of ChatMessage objects. Every placeholder in the template becomes an input it expects at run time, and the rendered messages leave on its prompt output, ready for a chat generator.

solid answer

~40 s

`ChatPromptBuilder` is the templating step that sits between retrieval and generation. You give it a `template` — a list of `ChatMessage` objects, typically `ChatMessage.from_system(...)` plus `ChatMessage.from_user(...)` — whose bodies are Jinja2. Every `{{ placeholder }}` it finds becomes an input the component expects, so a template mentioning `{{ documents }}` and `{{ question }}` makes both required at run time; you loop over documents in Jinja and inline each `doc.content`. It emits a single output named `prompt`, typed as a list of `ChatMessage`, which is exactly what a chat generator consumes. In Haystack 3.0 detected variables are required by default (`required_variables` defaults to `"*"`), so a forgotten one raises instead of silently rendering an empty string — you opt out per variable if you genuinely want it optional.

code

python · 16 lines
python
from haystack.components.builders import ChatPromptBuilder
from haystack.dataclasses import ChatMessage

template = [
    ChatMessage.from_system("Answer only from the context. If it is missing, say so."),
    ChatMessage.from_user(
        "Context:\n"
        "{% for doc in documents %}{{ doc.content }}\n{% endfor %}\n"
        "Question: {{ question }}"
    ),
]

builder = ChatPromptBuilder(template=template)
rendered = builder.run(documents=[], question="What is a Haystack component?")
for message in rendered["prompt"]:
    print(message.role, "->", message.text)

go deeper

for a junior

Be able to say plainly that it fills a Jinja2 template and hands a list of chat messages to the generator, and that the placeholders in the template are the inputs the component needs.

for a middle

Explain that variables are inferred from the template source and become inputs, that the single output is prompt typed as a list of ChatMessage, and what required_variables changes when a variable is absent.

for a senior

Show the production angle: how you format retrieved documents inside the template, why strict required variables catch a class of silent quality regressions, and how you unit-test the builder without touching a model.

for a principal

Own the question of where prompts live. Argue when templates belong in code under review versus loaded per tenant at run time, and what you lose in wiring-time validation when the template only exists at run time.

## What this component is for A chat model does not take "a question plus some documents"; it takes a list of role-tagged messages. `ChatPromptBuilder` is the piece of a Haystack pipeline that turns structured data — retrieved `Document` objects, a user question, a tenant name, a date — into exactly that list of messages. It performs no model call and holds no state; it is a pure rendering step, which is why it is cheap to unit-test on its own. ## The template is a list of messages, not a string The `template` init parameter takes a list of `ChatMessage` objects. You build them with the class factories: `ChatMessage.from_system(...)` for the standing instruction, `ChatMessage.from_user(...)` for the turn that carries the retrieved context and the question, and `ChatMessage.from_assistant(...)` if you are seeding a few-shot exchange. The *text* of each message is a Jinja2 template, so roles are fixed at design time while content is filled per request. This is the difference from the older string-shaped `PromptBuilder`, which renders one flat string. `PromptBuilder` still exists in Haystack 3.0, but with chat generators being the only generator line, `ChatPromptBuilder` is what you reach for when you want a real system message rather than a preamble glued to the top of a user turn. ## Variables become inputs Haystack inspects the Jinja2 source and collects every `{{ name }}` it finds. Those names become the component's run-time inputs, which is what makes the pipeline's wiring check meaningful: if your template mentions `{{ documents }}`, the pipeline expects something to supply `documents`, and you can connect a retriever's output straight into it. There is no separate schema to declare in the common case. Because it is real Jinja2 and not string interpolation, you get loops and conditionals. The canonical RAG body is a `{% for doc in documents %}{{ doc.content }}{% endfor %}` loop, and it is normal to also inline metadata — `{{ doc.meta.source }}` — so the model can cite. If the template only becomes known at run time, you can declare the expected inputs up front with the `variables` init parameter instead. ## The single output The component emits one output, `prompt`, holding the rendered list of `ChatMessage`. That is the type a chat generator's `messages` input accepts, so the two connect directly. Nothing else comes out — no metadata, no token count. ## Required versus optional variables `required_variables` decides what happens when a variable in the template is not supplied. In Haystack 3.0 it defaults to `"*"`, meaning every detected variable is required and a missing one raises, halting the run. This is a deliberate change of posture: under the older permissive behaviour a typo in a variable name produced a prompt with a silent hole in it, the model answered from whatever was left, and the failure surfaced as "the answers got worse" rather than as an exception. You can still make variables optional — pass a shorter `required_variables` list, or an empty one — and unprovided optional variables render as an empty string. Haystack logs a warning when you turn the requirement off entirely, precisely because silent empty substitution is hard to debug. ## Changing the template per request `run()` accepts a `template` argument that overrides whatever was set at init, for that call only. That is how you do per-tenant prompts, A/B tests between two phrasings, or prompts loaded from a database, without rebuilding the pipeline. The trade-off is that a template supplied at run time cannot be inspected at wiring time, so declare its inputs with `variables` if you want the pipeline's socket check to still mean something. ## Where people go wrong The two recurring mistakes are treating the template as an f-string — Jinja2 syntax is `{{ }}` and `{% %}`, and Python formatting does not apply — and assuming the component does something intelligent with `Document` objects. It does not: if you pass a list of documents and never loop over them, Jinja renders their repr, and you have handed the model a wall of Python objects. Formatting the context is your job, inside the template, and it is a real quality lever: order, separators, and whether you include metadata all change the answer.

  • What happens if the template references a variable you never pass at run time?
    In Haystack 3.0 every detected variable is required by default, so the component raises and the run stops. You can relax that by listing a shorter `required_variables`; unprovided optional variables then render as an empty string. Haystack warns when you disable the requirement entirely, because a silently truncated prompt degrades answers without ever throwing.
  • How do you swap the template per request instead of pinning it at init?
    `run()` accepts a `template` argument that overrides the init template for that call, which is how per-tenant prompts or A/B tests are done without rebuilding the pipeline. Because a run-time template cannot be inspected when the pipeline is wired, declare the inputs it needs through the `variables` init parameter so the socket check still applies.
  • Why loop over documents in the template rather than passing a pre-joined string?
    Keeping the loop in the template keeps the retriever's output connectable directly and keeps formatting decisions — separators, ordering, whether to inline `doc.meta` for citations — visible in one reviewable artifact rather than hidden in glue code. It also lets you unit-test the builder alone by feeding it a fixed document list and asserting on the rendered messages.

saying these in an interview costs you the question

  • Says ChatPromptBuilder calls the model itself
  • Thinks the template is a Python f-string rather than Jinja2
  • Expects a missing variable to render empty by default in 3.0
  • Passes Document objects and expects automatic formatting
  • Believes it outputs one plain string for any generator

context

open as a page

In Haystack 3.0, what replaced OpenAIGenerator and what must you change?

level: middleimportance: must knowfreq 55%

basics

~20 s

Haystack 3.0 removed the legacy string-in generators, OpenAIGenerator among them. OpenAIChatGenerator replaces it: it accepts ChatMessage objects, or a plain string, and returns replies as ChatMessage objects, so downstream code must read reply.text instead of a raw string.

open as a page

In Haystack, what does the @component decorator require of your class?

level: middleimportance: must knowfreq 78%

basics

~20 s

A Haystack component is a class decorated with @component that exposes a run() method. The typed parameters of run() become its inputs, and run() must return a dict whose keys match the names declared in @component.output_types on that method.

open as a page

Why does a Haystack component load its model in warm_up(), not __init__?

level: middleimportance: should knowfreq 42%

basics

~20 s

warm_up() separates cheap construction from expensive resource loading. A component can be built, wired, type-checked, serialized and drawn without downloading weights or claiming GPU memory; the model loads only when the component is about to actually run.

open as a page

Why does Haystack take an API key as Secret.from_env_var, not a string?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Secret.from_env_var stores only the variable's name, so serializing a component or exporting a pipeline to YAML writes a reference rather than the key. Secret.from_token holds the literal value and refuses to serialize at all, which is deliberate.

open as a page

When must a custom Haystack component implement to_dict and from_dict?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Only when default serialization cannot round-trip your constructor arguments. Haystack's default maps each init parameter to a same-named attribute and needs JSON-friendly values, so sets, callables, custom objects and secrets require explicit conversion in to_dict and reconstruction in from_dict.

open as a page