skip to content

How do you embed a Haystack Agent inside a Pipeline, and what sockets does it expose?

level: middleimportance: must knowfreq 50%

answer

  1. it is a component like any other
  2. one input socket carrying chat messages
  3. transcript and final turn come out
  4. state keys become extra output sockets
  5. iterations stay inside a single run

basics

~20 s

Agent implements the component interface, so you add it with Pipeline.add_component and connect into its messages input and out of its messages or last_message outputs. Its tool loop runs entirely inside one component execution — the pipeline never sees the iterations.

solid answer

~40 s

An `Agent` is a component like any other: `pipe.add_component("agent", agent)` and then `pipe.connect("prompt.prompt", "agent.messages")`, since the agent consumes a `list[ChatMessage]`. Its outputs are `messages` (the whole transcript) and `last_message` (the final `ChatMessage`), plus one output per key declared in `state_schema` — that is how structured data such as retrieved documents leaves the agent for a downstream component. The pipeline calls `warm_up()`, which warms the chat generator and any component-backed tools. The crucial property is that the agent's generate-invoke loop happens **within a single component run**: from the pipeline's point of view the node executes once, so you do not model the loop with edges, and the pipeline's own iteration controls have nothing to do with `max_agent_steps`. That containment is the design bet — bounded model-driven control flow inside an otherwise explicit, inspectable graph.

code

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

template = [ChatMessage.from_user("Research this and answer: {{query}}")]

pipe = Pipeline()
pipe.add_component("prompt", ChatPromptBuilder(template=template))
pipe.add_component("agent", agent)
pipe.connect("prompt.prompt", "agent.messages")

result = pipe.run({"prompt": {"query": "our refund policy"}})
print(result["agent"]["last_message"].text)

go deeper

for a junior

Be able to say that an Agent is added with add_component and connected like any other component, taking messages in and giving messages and last_message out.

for a middle

Explain the socket names, that state_schema keys become extra outputs, and that the agent's loop runs inside one component execution rather than as pipeline iterations.

for a senior

Discuss operational consequences: the transcript is your trace, warm_up propagates to tools, and serialization of a pipeline holding an agent depends on every tool being serializable.

for a principal

Argue the architecture: confine model-driven control flow to one node and keep consequential actions in the explicit graph where they can be gated, logged and retried. Be ready to say when that containment stops fitting.

## Agent is a component, not an alternative to pipelines Many agent frameworks force a choice: either you write a graph or you write an agent. Haystack does not. `Agent` implements the same component contract as `ChatPromptBuilder` or a retriever — it has typed inputs, typed outputs, a `run()`/`run_async()`, `warm_up()`, and `to_dict`/`from_dict`. So the natural production shape is a mostly deterministic pipeline with one agent node where the control flow genuinely has to be decided by a model. ## Wiring it up ``` pipe = Pipeline() pipe.add_component("prompt", ChatPromptBuilder(template=template)) pipe.add_component("agent", agent) pipe.connect("prompt.prompt", "agent.messages") ``` The agent's input socket is `messages: list[ChatMessage]`. Anything producing chat messages can feed it — a prompt builder rendering a template, or your own component assembling history from a store. Because Haystack validates socket types at `connect()` time, wiring a `str` output into `agent.messages` fails when you build the pipeline, not at run time. On `run()`, the agent's input is supplied like any other component's: `pipe.run({"prompt": {"query": "..."}})`, with the messages arriving over the connection. ## What comes out - `messages: list[ChatMessage]` — the full transcript, including every assistant turn with its tool calls and every tool result. This is your trace. - `last_message: ChatMessage` — the final turn, normally what you show a user. - One socket per `state_schema` key. Declaring `state_schema={"documents": {"type": list[Document]}}` gives the agent a `documents` output, so a downstream ranker or a citation formatter can consume the actual `Document` objects the agent's retrieval tool produced — no re-parsing of prose. That last point is what makes the agent composable rather than a black box that returns a paragraph. ## The loop does not leak into the graph A pipeline that contains a cycle needs its own loop controls. An agent inside a pipeline needs none of that: the entire generate-invoke-repeat sequence happens inside one invocation of `agent.run()`. The pipeline executes the node once and moves on. Practically this means: - You do **not** draw an edge from the agent back to itself. - Pipeline-level run limits do not bound the agent; `max_agent_steps` does. - Pipeline visualization shows one node, not the agent's internal iterations — for step-level detail you read the returned `messages` or attach a streaming callback. ## Warm-up and lifecycle `Pipeline.run()` warms components that need it, and `Agent.warm_up()` propagates to the chat generator and to component-backed tools that define `warm_up`. So a `ComponentTool` wrapping an embedder is warmed once when the pipeline warms, not on each tool call. ## Serialization Because the agent supports `to_dict`/`from_dict`, a pipeline containing one can be serialized to YAML and rebuilt — as long as its chat generator and every tool are serializable. Tools built by wrapping serializable components round-trip; tools closing over live objects do not. This is a real constraint for teams who ship pipeline definitions as configuration. ## Agents as tools of other agents Since an agent is a component, `ComponentTool(component=agent, ...)` makes one agent callable by another — the nested agent's loop runs inside the outer agent's single tool invocation. It works, and it composes, but be deliberate: each nesting level multiplies steps and tokens, and the transcript of the inner run does not appear in the outer agent's messages unless you render it into the tool result. ## When the containment is the wrong shape If a human needs to approve an action mid-loop, or a step must be durably resumed after a crash, a self-contained `run()` is an awkward unit — the loop is one atomic execution from the outside. In those cases decompose: put the model-driven decision in the agent, but keep the consequential action as a separate pipeline component the agent merely recommends, so the graph — where you can gate, log and retry — owns it.

  • How does a downstream component get the documents an agent's retrieval tool found?
    Declare them in the agent's `state_schema` and have the tool write there via `outputs_to_state`. The agent then exposes a matching output socket, so you connect `agent.documents` to the next component and receive real `Document` objects. Without state, that data exists only as stringified text inside the transcript and would have to be re-parsed.
  • Do you need to add an edge back into the agent to make its loop run?
    No. The loop is internal to a single `agent.run()` call; the pipeline executes the node once. Adding a self-edge would model something else entirely — repeated whole-agent runs — and pipeline-level run limits would then apply where `max_agent_steps` is the correct control.
  • What stops a pipeline containing an Agent from serializing to YAML?
    A non-serializable part: a chat generator or tool that cannot express itself through `to_dict`, typically one closing over a live client, a lambda, or an unpicklable handler. Tools built from serializable components round-trip cleanly, which is one practical reason to prefer `ComponentTool` over ad-hoc closures when configuration-as-code matters.
  • Can an Agent be used as a tool for another Agent?
    Yes — wrap it in a `ComponentTool`, since an agent is a component. The inner loop then runs inside one tool invocation of the outer agent. Use it sparingly: steps and tokens multiply per nesting level, and the inner transcript is invisible to the outer agent unless you deliberately render it into the tool result.

saying these in an interview costs you the question

  • Treating Agent as a replacement for Pipeline rather than a component in one
  • Drawing a cycle back into the agent to make its loop run
  • Expecting pipeline run limits to bound the agent's internal steps
  • Assuming only text comes out, so re-parsing documents from prose
  • Forgetting that state_schema keys become additional output sockets

context