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.context="full") each branch receives the full answer the planner produced, not a lossy excerpt.
Quick Start
1
Define parallel workers
2
Run parallel workflow
API Reference
parallel()
Parameters
Accessing Results
After parallel execution, results are available inctx.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 insideparallel([...]) 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.
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 sameoutput_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.
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.
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
Athreading.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 throughAgentFlow(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’sThreadPoolExecutor:
- 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
Parallelise only independent steps
Parallelise only independent steps
Branches must not depend on each other’s output in the same parallel group.
Cap concurrency for external APIs
Cap concurrency for external APIs
Rate-limited tools may need sequential execution or throttling despite parallel support.
Collect and inspect branch errors
Collect and inspect branch errors
Read the aggregated error list after
ParallelExecutionError before retrying.Keep branch outputs small
Keep branch outputs small
Large parallel results inflate context — summarise before merging downstream.
Give sibling branches distinct output_variable names
Give sibling branches distinct output_variable names
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.Bound external calls with timeout
Bound external calls with timeout
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.Prefer fail_fast for latency-critical fan-outs
Prefer fail_fast for latency-critical fan-outs
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 withcontext="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".WARNING) when you opt in.
Failure Handling
Choose how a parallel block reacts when a branch fails using theon_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 withon_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_().
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 withtimeout 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.completioncall — a deadline started only at submission time would lettimeout=Nwait far longer thanNbefore its clock even begins). timeoutcombines with anyon_failuremode.fail_fast + timeoutis 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=0and negatives raiseValueErrorat construction — the only way to mean “no bound” isNone.
Running the same workflow more than once
Reusing oneWorkflow 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 acontextvars.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
Workflowinstance across many inputs (common for long-running services) and combine it withfail_fastortimeout.
Related
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

