Skip to main content
Execute multiple steps concurrently and combine their results. This pattern is ideal for independent tasks that can run simultaneously.
Steps inside this pattern inherit the same max_retries, guardrails, and output_file policies as top-level steps. See Nested workflows → Retry, guardrails, and output_file.
As of PraisonAI #4932, an output_variable set inside a branch is preserved after the parallel block. Earlier releases silently discarded it.
A real agent fanning its output into three branch workers. By default (context="full") each branch receives the full answer the planner produced, not a lossy excerpt.
The user starts one workflow; independent steps run concurrently and merge results.

Quick Start

1

Define parallel workers

2

Run parallel workflow

Output:

API Reference

parallel()

Parameters

Accessing Results

After parallel execution, results are available in ctx.variables:
Index alignment under partial_ok. A failed branch contributes None at the same index as its step, so parallel_outputs[i] is always the result of parallel_step.steps[i]. As of PraisonAI #4203, a downstream reader of parallel_outputs[i] can no longer silently receive a later branch’s result once an earlier branch fails.

Named outputs from branches

Every step inside parallel([...]) may set output_variable. As of PraisonAI #4932, that write is visible to any step after the parallel block, keyed by that exact name — not just as a positional entry in parallel_outputs.
The branch’s own output_variable is stored under that exact name; the ordered parallel_outputs list is still there for aggregators that want positional access. Each branch runs against its own isolated copy of the variables scope, so branches never race on one shared dict and a branch’s in-place mutation of a list or dict is invisible to its siblings. Values that cannot be deep-copied — a threading.Lock, a DB connection, an HTTP or LLM client, an open file, a live Agent, a generator — are shared by reference across branches instead of aborting the run (as of PraisonAI #5717). After the block, only the keys a branch actually wrote merge back into the shared scope — untouched variables keep the parent’s own object.

Two branches wrote the same variable

When two branches write the same output_variable, declaration order wins — the later branch overwrites the earlier, matching what running the same steps sequentially would produce. The result never depends on which thread finishes first.
A WARNING names the key and both branches. Silent last-writer-wins was the exact bug #4932 removes, so a collision is reported rather than hidden.
If two branches want to write the same concept, give them distinct output_variable names (branch_a_score, branch_b_score) and combine them in the aggregator.
The same silent-loss defect existed inside loop() bodies and was fixed by the sibling PR #4947. See Named outputs from a loop body — loop bodies now propagate writes the same way, with the one difference that per-iteration collisions are not warned about (that would emit N-1 warnings for a correct workflow).

Sharing resources across branches

A threading.Lock, DB/HTTP/LLM client, open file, live Agent, or generator can sit in variables even when a branch reads it — the un-copyable leaf is shared by reference, everything around it is still isolated.
Branches share the un-copyable leaf by reference. A Lock/connection/client is designed to be shared; a mutable list or dict that happens to be un-copyable (rare) is also shared — mutations through it will be visible to siblings. Keep ordinary mutable state in a copyable container (plain list / dict) so the isolation guarantee applies to it.

Values that degrade to shared-by-reference

Pass any of these through AgentFlow(variables=...) and sibling branches share one reference instead of crashing.
Cross-variable aliasing inside one branch is preserved. If variables={"records": shared, "state": {"records": shared}} passes the same list under both names, a branch still sees one shared list — a write through records is visible through state["records"] within that branch.

Examples

With Agents

Mixed Steps

Nested Parallel

Performance

Parallel execution uses Python’s ThreadPoolExecutor:
  • Concurrent I/O: Ideal for API calls, file operations
  • Thread-safe: Each step gets its own copy of variables
  • Automatic joining: All results collected before next step

Use Cases

How It Works


Best Practices

Branches must not depend on each other’s output in the same parallel group.
Rate-limited tools may need sequential execution or throttling despite parallel support.
Read the aggregated error list after ParallelExecutionError before retrying.
Large parallel results inflate context — summarise before merging downstream.
Two branches writing the same output_variable collide — the later-declared branch silently overwrites the earlier one (with a warning). That class of silent loss is exactly what #4932 fixes; distinct names (branch_a_score, branch_b_score) avoid it entirely.
A Lock, DB connection, HTTP/LLM client, open file, or live Agent can be injected via AgentFlow(variables=...) and used from branch steps. The un-copyable leaf is shared by reference, so coordination (locking, connection pooling) works as expected; everything around it is still branch-local, so your ordinary mutable state stays isolated. See Sharing resources across branches.
LLM / HTTP / tool calls can hang. parallel(..., timeout=N) is the cheapest hung-branch bound — the run raises WorkflowStepError at the deadline instead of waiting forever on a stuck thread. See Timing out hung branches.
Now that fail_fast actually fails fast (post-#5718), it is the right default for user-facing paths where returning quickly on first error beats collecting every error.

Context pass-through to branches

Every branch sees the exact upstream output by default. Opt in to token savings with context="truncate" or context="summarize" — those drop the middle of large outputs and warn when they do.
Before PraisonAI #5719: large previous_output was always reduced to a head+tail excerpt before fan-out, with no opt-out and no default-level warning — branches silently received partial data. Upgrade to get lossless-by-default; if you relied on the token savings, opt in with context="truncate" or context="summarize".
The two paths differ only in what each branch receives — lossless by default, excerpted (with a WARNING) when you opt in.

Failure Handling

Choose how a parallel block reacts when a branch fails using the on_failure parameter.

Failure Strategies

Before PraisonAI #5718: fail_fast consumed results in submission order and left the thread pool via a with block whose __exit__ called shutdown(wait=True), so the exception was held until every earlier-declared and every still-running branch finished. If you observe a fail_fast block hanging on a slow sibling, upgrade.
Variable merging on failure. Under partial_ok, the variables a failed branch had already written before it failed are preserved — its own output_variable is never set (the failure path returns before that write), so a downstream reader cannot pick up a value the failure invented. Under fail_fast / fail_all, nothing from any branch is merged: the block raises before the merge runs. See Named outputs from branches.

Examples

Error Handling with partial_ok

Stop propagation from nested steps

A branch step with on_error="stop" halts the whole workflow, not just the parallel block — even under on_failure="partial_ok". As of PraisonAI #4203, parallel(...) propagates a nested stop up to the enclosing workflow, matching route() / loop() / repeat() / if_().
The interaction between the parallel block’s on_failure mode and a branch step’s on_error is:

Exception Handling with fail_fast/fail_all

Timing out hung branches

Bound the whole parallel block with timeout so a stuck branch no longer blocks the run.
  • The clock starts when the block begins, including context preparation (the summarisation path can make a blocking litellm.completion call — a deadline started only at submission time would let timeout=N wait far longer than N before its clock even begins).
  • timeout combines with any on_failure mode. fail_fast + timeout is the common “give up on the whole fan-out quickly” pattern.
  • A timed-out branch is cancelled where possible; threads running user code cannot be force-killed in Python, so the branch may keep running in the background. The caller is no longer blocked on it, and its late failure cannot corrupt a later run (see Running the same workflow more than once).
  • timeout=0 and negatives raise ValueError at construction — the only way to mean “no bound” is None.

Running the same workflow more than once

Reusing one Workflow instance across many inputs is safe even when a previous run abandoned a branch — a late failure from an abandoned branch cannot flip a later run’s status.
  • Each run() stamps a monotonic “generation” into a contextvars.ContextVar; branches capture it at submit time.
  • When a branch finally raises after its owning run has moved on, the engine suppresses the shared-status write.
  • This matters when you reuse one Workflow instance across many inputs (common for long-running services) and combine it with fail_fast or timeout.

Workflow Patterns

Overview of routing, parallel, loop, and repeat

Workflow Routing

Decision-based branching

Workflow Loop

Iterate over lists and files

Workflow Repeat

Repeat until a condition is met

Workflow Discussion

N-agent conversation loop