Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/openai/openai-cookbook/llms.txt

Use this file to discover all available pages before exploring further.

Sequential agent execution is simple to reason about, but it leaves performance on the table whenever tasks are independent of each other. If you need a sentiment analysis, a feature extraction, and a pros-and-cons summary from the same product review, there is no reason to wait for one to finish before starting the next. Running those agents in parallel — fanning out, then fanning in — can cut total latency dramatically and reduce the blast radius of a single failure. This guide shows you how to implement that pattern with the OpenAI Agents SDK and Python’s asyncio.

When parallel agents help

Parallel execution is the right tool when your workflow contains tasks that:
  • Do not depend on each other’s output. Each agent gets the same input and produces an independent result.
  • Are slow enough to matter. If each step takes 1–3 seconds, sequential execution adds up fast. Parallel execution brings the total latency closer to the slowest single step.
  • Can fail independently. If one analysis fails, the others can still complete. You decide how to handle partial results.
Common scenarios that fit this pattern: content moderation pipelines (toxicity, spam, and legal checks running in parallel), research assistants that query multiple sources simultaneously, and document analysis workflows that extract different entity types at once.
Do not parallelize tasks that depend on each other’s output. If agent B needs agent A’s result as its input, they must run sequentially. Parallelism is only safe for truly independent branches.

Basic parallel execution with asyncio

The Agents SDK’s Runner.run method is an async coroutine. Use asyncio.gather to await multiple runs at the same time:
import asyncio
from agents import Agent, Runner

async def run_parallel_agents():
    agent_a = Agent(name="Researcher", instructions="Research the topic thoroughly.")
    agent_b = Agent(name="Analyst", instructions="Analyze data and find patterns.")

    results = await asyncio.gather(
        Runner.run(agent_a, "Research quantum computing trends"),
        Runner.run(agent_b, "Analyze the provided dataset")
    )
    return results

results = asyncio.run(run_parallel_agents())
asyncio.gather schedules both coroutines onto the event loop and returns when all of them complete. The result is a list in the same order as the input coroutines, so results[0] corresponds to agent_a and results[1] to agent_b.

A real-world example: parallel review analysis

The following example analyzes a product review from four angles simultaneously — features, pros/cons, sentiment, and recommendation — then feeds all four outputs to a meta-agent that writes a final summary.

Step 1: Define specialist agents

from agents import Agent

features_agent = Agent(
    name="FeaturesAgent",
    instructions="Extract the key product features mentioned in the review."
)

pros_cons_agent = Agent(
    name="ProsConsAgent",
    instructions="List the pros and cons mentioned in the review."
)

sentiment_agent = Agent(
    name="SentimentAgent",
    instructions="Summarize the overall user sentiment from the review."
)

recommendation_agent = Agent(
    name="RecommendationAgent",
    instructions="State whether the reviewer recommends the product and why."
)

Step 2: Run all agents in parallel

import asyncio
from agents import Runner

REVIEW = """
I've been using this wireless keyboard for three months. The battery life is
exceptional — I charge it once every six weeks. Typing feel is crisp and the
low-profile keys are great for long sessions. My only complaint is that
Bluetooth pairing is occasionally finicky on first connect. Overall I'd
recommend it to anyone who types a lot.
"""

async def analyze_review(review: str):
    results = await asyncio.gather(
        Runner.run(features_agent, review),
        Runner.run(pros_cons_agent, review),
        Runner.run(sentiment_agent, review),
        Runner.run(recommendation_agent, review),
    )
    return {
        "features": results[0].final_output,
        "pros_cons": results[1].final_output,
        "sentiment": results[2].final_output,
        "recommendation": results[3].final_output,
    }

analysis = asyncio.run(analyze_review(REVIEW))

Step 3: Synthesize with a meta-agent

Pass all four outputs to a final agent that writes the user-facing summary:
meta_agent = Agent(
    name="SummaryAgent",
    instructions=(
        "You receive structured analysis of a product review. "
        "Write a concise, helpful summary a shopper can use to make a purchase decision."
    )
)

async def synthesize(analysis: dict) -> str:
    combined_input = "\n\n".join(
        f"**{key.upper()}**\n{value}" for key, value in analysis.items()
    )
    result = await Runner.run(meta_agent, combined_input)
    return result.final_output

summary = asyncio.run(synthesize(analysis))
print(summary)
This fan-out / fan-in structure is the core of the parallel agent pattern: scatter independent tasks across specialist agents, then gather their outputs into a single cohesive result.

Agents as tools: SDK-native parallelism

The Agents SDK also supports a built-in parallel execution model where specialist agents are exposed as tools on a parent agent. The parent agent calls whichever tools it deems useful — potentially multiple at once — and the SDK handles concurrency automatically:
from agents import Agent

features_agent = Agent(
    name="FeaturesAgent",
    instructions="Extract product features from the text.",
    as_tool=True,
)

sentiment_agent = Agent(
    name="SentimentAgent",
    instructions="Analyze overall sentiment from the text.",
    as_tool=True,
)

orchestrator = Agent(
    name="Orchestrator",
    instructions=(
        "Analyze the product review by calling both the features and sentiment tools. "
        "Then write a summary combining their outputs."
    ),
    tools=[features_agent.as_tool(), sentiment_agent.as_tool()]
)
The asyncio.gather approach gives you direct control over which agents run and when. The agent-as-tool approach lets the orchestrator decide dynamically at runtime — useful when the set of analyses depends on the input content.

Handling partial failures

asyncio.gather raises the first exception it encounters and cancels the rest by default. If you want to collect results even when some agents fail, use return_exceptions=True:
results = await asyncio.gather(
    Runner.run(features_agent, review),
    Runner.run(pros_cons_agent, review),
    Runner.run(sentiment_agent, review),
    return_exceptions=True,
)

for i, result in enumerate(results):
    if isinstance(result, Exception):
        print(f"Agent {i} failed: {result}")
    else:
        print(f"Agent {i} succeeded: {result.final_output}")
With this pattern you can pass partial results to the synthesis agent and have it note which analyses were unavailable, rather than failing the entire workflow.

Measuring the latency benefit

One way to confirm parallelism is working is to compare wall-clock time against the sum of individual run times. If the parallel run takes roughly as long as your slowest single agent (rather than the total of all agents), you are getting the benefit:
import asyncio
import time
from agents import Agent, Runner

async def timed_parallel():
    agents = [
        Agent(name=f"Agent{i}", instructions="Respond with a short analysis.")
        for i in range(4)
    ]
    message = "Analyze the impact of renewable energy on global markets."

    start = time.perf_counter()
    await asyncio.gather(*[Runner.run(a, message) for a in agents])
    elapsed = time.perf_counter() - start

    print(f"Parallel: {elapsed:.2f}s for {len(agents)} agents")

asyncio.run(timed_parallel())

Coordination patterns at a glance

Fan-out / fan-in

Run N specialist agents in parallel, then pass all outputs to one synthesis agent. Best for fixed analysis pipelines.

Agents as tools

Expose specialists as callable tools on an orchestrator. The orchestrator selects which to call at runtime.

Partial failure tolerance

Use return_exceptions=True to collect whatever completed and handle failures gracefully in the synthesis step.

Sequential fallback

When task B needs task A’s output, keep them sequential. Only parallelize genuinely independent branches.

Further reading

Build docs developers (and LLMs) love