Skip to content
Merged
47 changes: 44 additions & 3 deletions docs-website/docs/concepts/pipelines.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,52 @@ See [Pipeline Loops](pipelines/pipeline-loops.mdx) for a deeper explanation of h

<ClickableImage src="/img/2390eea-Pipeline_Illustrations_1_2.png" alt="Pipeline architecture diagram illustrating a feedback loop where output from later components loops back to earlier components" size="large" />

### Async Pipelines
### Async Execution and Streaming

The AsyncPipeline enables parallel execution of Haystack components when their dependencies allow it. This improves performance in complex pipelines with independent operations. For example, it can run multiple Retrievers or LLM calls simultaneously, execute independent pipeline branches in parallel, and efficiently handle I/O-bound operations that would otherwise cause delays. Through concurrent execution, the AsyncPipeline significantly reduces total processing time compared to sequential execution.
Pipelines execute components in parallel when their dependencies allow it. This improves performance in complex pipelines with independent operations. For example, a pipeline can run multiple Retrievers or LLM calls simultaneously, execute independent pipeline branches in parallel, and efficiently handle I/O-bound operations that would otherwise cause delays. You can cap the number of components running at the same time with the `concurrency_limit` init parameter.

Find out more in our [AsyncPipeline](pipelines/asyncpipeline.mdx) documentation.
Besides the blocking `run` method, every pipeline offers three ways to run asynchronously:

- `run_async`: Executes the pipeline in a single non-blocking call, ideal for integrating a pipeline into a larger async application or service.
- `run_async_generator`: Yields partial outputs as components complete their tasks, which is useful for monitoring progress, debugging, and handling outputs incrementally.
- `stream`: Runs the pipeline and returns a handle that streams [`StreamingChunk`](/reference/data-classes-api#streamingchunk) objects as they are produced — a convenient way to stream LLM output from an async application, such as an API endpoint. Iterate the handle with `async for` to consume the chunks; after iteration ends, `handle.result` holds the final pipeline output (the same dictionary returned by `run_async`).

```python
import asyncio

from haystack import Pipeline
from haystack.components.builders import ChatPromptBuilder
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage

pipe = Pipeline()
pipe.add_component(
"prompt_builder",
ChatPromptBuilder(template=[ChatMessage.from_user("Tell me about {{topic}}")]),
)
pipe.add_component("llm", OpenAIChatGenerator())
pipe.connect("prompt_builder.prompt", "llm.messages")


async def main():
handle = pipe.stream(data={"prompt_builder": {"topic": "Italy"}})
async for chunk in handle:
print(chunk.content, end="", flush=True)
return handle.result


result = asyncio.run(main())
```

By default, chunks from every streaming-capable component are forwarded; pass `streaming_components` with a list of component names to stream only specific components. If the consumer abandons iteration, the underlying pipeline run is cancelled automatically; pass `cancel_on_abandon=False` to let it run to completion instead.

If a `streaming_callback` is set on a component (at init or at runtime through `data`), it is still invoked for each chunk in addition to the chunks being pushed to the handle. When streaming, components accept a sync `streaming_callback` in `run_async` too — see the [Choosing the Right Generator guide](../pipeline-components/generators/guides-to-generators/choosing-the-right-generator.mdx#sync-and-async-callbacks) for details.

#### Error Handling and Task Cancellation

If a component raises an error while sibling components are still running concurrently, the pipeline cancels and drains those in-flight tasks before re-raising the original error, so no tasks keep running in the background. The same cleanup applies when you stop iterating `run_async_generator` early (for example, by breaking out of the loop and closing the generator) or when the run itself is cancelled.

Note that cancellation only interrupts components that run natively async. Sync components are offloaded to a worker thread, which cannot be interrupted and runs to completion in the background. Their outputs are discarded, so the pipeline state stays consistent, but the component's side effects still complete.

## SuperComponents

Expand Down
163 changes: 0 additions & 163 deletions docs-website/docs/concepts/pipelines/asyncpipeline.mdx

This file was deleted.

2 changes: 1 addition & 1 deletion docs-website/docs/concepts/pipelines/pipeline-loops.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ There are two main ways a loop ends:
The pipeline finishes when the work queue is empty and no component can run again (for example, the router stops feeding inputs back into the loop).

2. **Reaching the maximum run count**
Every pipeline has a per-component run limit, controlled by the `max_runs_per_component` parameter of the `Pipeline` (or `AsyncPipeline`) constructor, which is `100` by default. If any component exceeds this limit, Haystack raises a `PipelineMaxComponentRuns` error.
Every pipeline has a per-component run limit, controlled by the `max_runs_per_component` parameter of the `Pipeline` constructor, which is `100` by default. If any component exceeds this limit, Haystack raises a `PipelineMaxComponentRuns` error.

You can set this limit to a lower value:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ This template format allows you to define `ChatMessage` sequences using Jinja2 s

Compared to using a list of `ChatMessage`s, this format is more flexible and allows including structured parts like images in the templatized `ChatMessage`; to better understand this use case, check out the [multimodal example](#multimodal) in the Usage section below.

#### The `insert` Tag

String templates also support an `{% insert %}` tag. It is a placeholder that evaluates an expression to a `ChatMessage` or a list of `ChatMessage` objects and expands it into the prompt, so messages provided at runtime can be interleaved with literal `{% message %}` blocks. For example, you can wrap the runtime messages with a system message above and a templated user message below, then pass the messages (and any template variables) at run time:

```python
from haystack.components.builders import ChatPromptBuilder
from haystack.dataclasses import ChatMessage

template = """
{% message role="system" %}You are a helpful assistant.{% endmessage %}
{% insert messages %}
{% message role="user" %}{{ query }}{% endmessage %}
"""

builder = ChatPromptBuilder(template=template)
result = builder.run(
messages=[ChatMessage.from_user("Hi"), ChatMessage.from_assistant("Hello!")],
query="What's the weather?",
)
# result["prompt"] -> [system, user "Hi", assistant "Hello!", user "What's the weather?"]
```

All content types (tool calls, tool call results, images, reasoning, `name`, and `meta`) round trip without loss. A missing or empty value expands to nothing.

The expression can be a plain variable (`{% insert messages %}`), a slice or index (`{% insert messages[-1:] %}`, `{% insert messages[-1] %}`), or a combination of variables (`{% insert previous + current %}`). Multiple `{% insert %}` tags can be used in a single template, so the runtime messages can be split, reordered, or repeated across different positions.

### Jinja2 Time Extension

`PromptBuilder` supports the Jinja2 TimeExtension, which allows you to work with datetime formats.
Expand Down
4 changes: 2 additions & 2 deletions docs-website/docs/tools/pipelinetool.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ It replaces the older workflow of first wrapping a pipeline in a `SuperComponent
`ComponentTool`.

`PipelineTool` builds the tool parameter schema from the pipeline’s input sockets and uses the underlying components’ docstrings for input descriptions. You can choose which pipeline inputs and outputs to expose with
`input_mapping` and `output_mapping`. It works with both `Pipeline` and `AsyncPipeline` and can be used in a pipeline with `ToolInvoker` or directly with the `Agent` component.
`input_mapping` and `output_mapping`. It can be used in a pipeline with `ToolInvoker` or directly with the `Agent` component.

### Parameters

- `pipeline` is mandatory and must be a `Pipeline` or `AsyncPipeline` instance.
- `pipeline` is mandatory and must be a `Pipeline` instance.
- `name` is mandatory and specifies the tool name.
- `description` is mandatory and explains what the tool does.
- `input_mapping` is optional. It maps tool input names to pipeline input socket paths. If omitted, a default mapping is created from all pipeline inputs.
Expand Down
8 changes: 4 additions & 4 deletions docs-website/docs/tools/searchabletoolset.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ toolset = SearchableToolset(

### Reusing the toolset across multiple agent runs

When reusing the same `SearchableToolset` instance across multiple agent runs, you can call `clear()` to reset any tools discovered in the previous run:
You can safely reuse the same `SearchableToolset` instance across multiple agent runs, including concurrent ones. Each `Agent` run operates on an isolated, run-scoped copy of the toolset (created with [`spawn()`](toolset.mdx#run-scoped-copies-and-tool-selection)), so tools discovered in one run do not persist into, or collide with, other runs — every run starts fresh from the catalog:

```python
agent = Agent(
Expand All @@ -120,8 +120,8 @@ agent = Agent(

result1 = agent.run(messages=[ChatMessage.from_user("What's the weather in Milan?")])

# Reset discovered tools before the next run
toolset.clear()

# The next run starts fresh: tools discovered in the previous run are not carried over
result2 = agent.run(messages=[ChatMessage.from_user("Search for news about AI.")])
```

If you drive the toolset directly (outside an `Agent`), you can call `clear()` to reset the discovered tools yourself.
11 changes: 11 additions & 0 deletions docs-website/docs/tools/toolset.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ math_toolset.add(multiply_numbers)
math_toolset.add(another_toolset)
```

### Run-Scoped Copies and Tool Selection

A `Toolset` is never mutated in place during an [`Agent`](../pipeline-components/agents-1/agent.mdx) run. Each run operates on an isolated, run-scoped copy of the configured `Toolset`, created with the `spawn()` method. This makes concurrent runs that share the same `Toolset` instance safe: per-run state, such as an active tool-name selection or a [`SearchableToolset`](searchabletoolset.mdx)'s discovered tools, cannot leak or collide across runs.

You can also restrict an `Agent` to a subset of tools at runtime by passing tool names, for example `agent.run(tools=["tool_a", "tool_b"])`. When a `Toolset` is configured, the selection is applied to the live (run-scoped) `Toolset` rather than flattening it into a static list, so dynamic behavior like a `SearchableToolset`'s search and lazy loading keeps working over the selected subset.

Two methods support this and can be overridden when subclassing:

- `get_selectable_tools()`: Returns every tool available for name-based selection, ignoring any active selection restriction. Override it if your subclass's iteration does not surface every selectable tool.
- `spawn()`: Returns an isolated, run-scoped copy of the `Toolset`. Override it if your subclass holds additional run-scoped state.

## Usage

You can use `Toolset` wherever you can use Tools in Haystack.
Expand Down
4 changes: 4 additions & 0 deletions docs-website/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
from: '/docs/pipeline',
to: '/docs/pipelines',
},
{
from: '/docs/asyncpipeline',
to: '/docs/pipelines',
},
{
from: '/docs/prompt_node',
to: '/docs/promptbuilder',
Expand Down
1 change: 0 additions & 1 deletion docs-website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export default {
'concepts/pipelines/debugging-pipelines',
'concepts/pipelines/pipeline-breakpoints',
'concepts/pipelines/pipeline-loops',
'concepts/pipelines/asyncpipeline',
'concepts/pipelines/smart-pipeline-connections',
],
},
Expand Down