Building a Multi-Agent Research Pipeline with OpenAI Agents SDK: Complete Guide
Modern research workflows rarely fit into a single prompt-response cycle. Whether you're investigating market trends, summarizing scientific literature, or producing competitive analysis reports, the work demands multiple specialized steps: gathering sources, extracting key insights, cross-referencing claims, and synthesizing a final deliverable. The OpenAI Agents SDK is purpose-built for this kind of orchestrated, multi-step reasoning. In this guide, we'll build a complete multi-agent research pipeline from scratch, covering architecture, implementation, and production best practices.
What Is the OpenAI Agents SDK?
The OpenAI Agents SDK is a lightweight Python framework for building agentic applications where one or more LLM-powered agents collaborate to accomplish complex tasks. Unlike a single chat completion call, agents can use tools, hand off work to other agents, maintain guardrails, and execute multi-step plans autonomously. The SDK abstracts away much of the orchestration plumbing — tool calling loops, context propagation, tracing, and handoffs — so you can focus on defining what each agent should do.
At its core, the SDK revolves around four primitives:
- Agents — LLMs configured with instructions, tools, and guardrails.
- Handoffs — Mechanisms that let one agent delegate control to another.
- Guardrails — Input and output validators that run in parallel with the agent.
- Tracing — Built-in observability for every step, tool call, and handoff.
Why Multi-Agent Pipelines Matter
A single monolithic agent asked to "research a topic and write a report" tends to produce shallow, unfocused output. It tries to do everything at once and ends up doing nothing particularly well. Multi-agent pipelines solve this by applying the principle of separation of concerns to LLM workflows. Each agent has a narrow, well-defined role, a tailored system prompt, and a curated set of tools. This leads to several concrete benefits:
- Higher quality output — Specialized agents produce deeper, more focused work than a generalist.
- Better token economics — You only load relevant tools and context into each agent's context window.
- Parallelism — Independent research subtasks can run concurrently.
- Testability — Each agent can be evaluated and improved in isolation.
- Extensibility — Adding a new capability means adding a new agent, not rewriting a mega-prompt.
Prerequisites and Installation
Before we start building, make sure you have Python 3.9 or higher and an OpenAI API key. Install the SDK and a few supporting libraries:
pip install openai-agents python-dotenv httpx
Create a .env file in your project root:
OPENAI_API_KEY=sk-your-key-here
Designing the Research Pipeline Architecture
Our pipeline will consist of four agents working in sequence with one parallel branch:
- Planner Agent — Decomposes the research question into sub-questions and a search strategy.
- Searcher Agent — Executes web searches for each sub-question and collects raw sources.
- Analyst Agent — Reads the collected sources, extracts key findings, and flags contradictions.
- Writer Agent — Synthesizes everything into a structured final report with citations.
The Planner hands off to the Searcher, the Searcher hands off to the Analyst, and the Analyst hands off to the Writer. Context — the research question, sub-questions, and accumulated findings — flows through a shared data structure.
Defining the Shared Context
First, let's define a Pydantic model that will carry state between agents. The SDK supports passing a typed context object through the entire run.
from pydantic import BaseModel, Field
from typing import Optional
class ResearchContext(BaseModel):
"""Shared state passed between agents in the research pipeline."""
original_question: str
sub_questions: list[str] = Field(default_factory=list)
raw_sources: list[dict] = Field(default_factory=list)
findings: list[dict] = Field(default_factory=list)
final_report: Optional[str] = None
Each agent reads from and writes to this context. Keeping the schema explicit prevents the kind of silent data drift that plagues ad-hoc dictionary-based approaches.
Building the Tools
Agents need tools to interact with the outside world. Our Searcher needs a web search tool, and our Analyst needs a tool to fetch and parse full page content. For this tutorial we'll use a mock search function that you can later replace with a real API like Tavily, SerpAPI, or Brave Search.
import httpx
import json
async def web_search(query: str, max_results: int = 5) -> list[dict]:
"""Search the web and return a list of source dicts.
Replace this stub with a real search API call in production.
"""
# Stub: return mock results. In production, call Tavily/SerpAPI/etc.
return [
{
"title": f"Source result for: {query}",
"url": f"https://example.com/source-{i}",
"snippet": f"This is a mock snippet relevant to '{query}'."
}
for i in range(max_results)
]
async def fetch_page(url: str) -> str:
"""Fetch and return the text content of a web page."""
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(url, follow_redirects=True)
resp.raise_for_status()
# In production, use BeautifulSoup or trafilatura to extract text
return resp.text[:8000]
except Exception as e:
return f"Error fetching {url}: {e}"
Now let's wrap these functions as SDK tools using the @function_tool decorator:
from agents import function_tool
@function_tool
async def search_web(query: str) -> str:
"""Search the web for information on a given query.
Args:
query: The search query string.
Returns:
JSON string of search results with title, url, and snippet.
"""
results = await web_search(query)
return json.dumps(results, indent=2)
@function_tool
async def read_page(url: str) -> str:
"""Fetch and read the full text content of a web page.
Args:
url: The URL of the page to read.
Returns:
The text content of the page (truncated).
"""
content = await fetch_page(url)
return content
Creating the Agents
Now we define each agent with tailored instructions. The key to good agent design is writing system prompts that are specific, constrained, and action-oriented.
from agents import Agent, Runner
PLANNER_INSTRUCTIONS = """
You are a research planner. Given a research question, your job is to:
1. Break the question into 3-5 specific sub-questions that together
cover the full scope of the original question.
2. Identify what types of sources would be most valuable.
3. Output the sub-questions as a clear numbered list.
Do not attempt to answer the question yourself. Your only job is planning.
Be precise — vague sub-questions lead to vague research.
"""
SEARCHER_INSTRUCTIONS = """
You are a research searcher. You receive a set of sub-questions.
For each sub-question:
1. Use the search_web tool to find relevant sources.
2. Use the read_page tool to read the most promising 2-3 sources per
sub-question.
3. Collect the raw source content and URLs.
Prioritize authoritative sources. Skip paywalled or low-quality content.
Return a structured summary of what you found for each sub-question,
including source URLs.
"""
ANALYST_INSTRUCTIONS = """
You are a research analyst. You receive raw search results and page
content. Your job is to:
1. Extract the key findings relevant to each sub-question.
2. Note any contradictions between sources.
3. Assess source credibility.
4. Identify gaps where more research might be needed.
Output a structured analysis with findings grouped by sub-question.
Each finding should include a citation to its source URL.
Do not fabricate information. If sources are insufficient, say so.
"""
WRITER_INSTRUCTIONS = """
You are a research report writer. You receive a structured analysis
with findings and citations. Your job is to synthesize this into a
polished, well-structured report.
The report should include:
- An executive summary (2-3 paragraphs)
- Key findings organized by theme
- Contradictions and areas of uncertainty
- A conclusion with actionable takeaways
- A sources section listing all URLs cited
Write in a clear, professional tone. Use markdown formatting.
Every factual claim must be traceable to a cited source.
"""
Now let's instantiate the agents and wire up the handoffs:
planner_agent = Agent(
name="Research Planner",
instructions=PLANNER_INSTRUCTIONS,
model="gpt-4o",
)
searcher_agent = Agent(
name="Research Searcher",
instructions=SEARCHER_INSTRUCTIONS,
model="gpt-4o",
tools=[search_web, read_page],
)
analyst_agent = Agent(
name="Research Analyst",
instructions=ANALYST_INSTRUCTIONS,
model="gpt-4o",
)
writer_agent = Agent(
name="Report Writer",
instructions=WRITER_INSTRUCTIONS,
model="gpt-4o",
)
# Wire up handoffs: each agent can delegate to the next in the pipeline
planner_agent.handoffs = [searcher_agent]
searcher_agent.handoffs = [analyst_agent]
analyst_agent.handoffs = [writer_agent]
Running the Pipeline
With the agents defined and handoffs configured, running the pipeline is straightforward. We start the run with the Planner agent and let handoffs propagate control automatically:
import asyncio
from agents import Runner
async def run_research_pipeline(question: str) -> str:
"""Run the full multi-agent research pipeline.
Args:
question: The research question to investigate.
Returns:
The final research report as a markdown string.
"""
print(f"Starting research pipeline for: {question}\n")
result = await Runner.run(
starting_agent=planner_agent,
input=f"Research question: {question}",
)
return result.final_output
# Example usage
if __name__ == "__main__":
question = (
"What are the key differences between RAG and fine-tuning "
"for adapting LLMs to domain-specific tasks, and when should "
"each approach be used?"
)
report = asyncio.run(run_research_pipeline(question))
print("=" * 60)
print("FINAL REPORT")
print("=" * 60)
print(report)
When you run this, the SDK handles the entire flow: the Planner decomposes the question, hands off to the Searcher, which uses tools to gather sources, hands off to the Analyst for extraction, and finally the Writer produces the report. The built-in tracing lets you inspect every step.
Adding Guardrails for Safety
In production, you want to validate inputs and outputs. The SDK supports guardrails that run alongside your agents. Let's add an input guardrail that rejects harmful or out-of-scope queries, and an output guardrail that checks the final report for uncited claims:
from agents import GuardrailFunctionOutput, input_guardrail, output_guardrail
@input_guardrail
async def scope_guardrail(ctx, agent, input_data):
"""Reject queries that are not research-related."""
blocked_keywords = ["hack", "exploit", "malware", "phishing"]
input_lower = str(input_data).lower()
is_blocked = any(kw in input_lower for kw in blocked_keywords)
return GuardrailFunctionOutput(
output_info={"blocked_reason": "out of scope" if is_blocked else None},
tripwire_triggered=is_blocked,
)
@output_guardrail
async def citation_guardrail(ctx, agent, output):
"""Check that the final report contains citations."""
output_str = str(output)
has_urls = "http" in output_str or "www." in output_str
return GuardrailFunctionOutput(
output_info={"has_citations": has_urls},
tripwire_triggered=not has_urls,
)
Attach the input guardrail to the Planner and the output guardrail to the Writer:
planner_agent = Agent(
name="Research Planner",
instructions=PLANNER_INSTRUCTIONS,
model="gpt-4o",
input_guardrails=[scope_guardrail],
)
writer_agent = Agent(
name="Report Writer",
instructions=WRITER_INSTRUCTIONS,
model="gpt-4o",
output_guardrails=[citation_guardrail],
)
If a guardrail's tripwire is triggered, the run stops immediately and raises a GuardrailTripwireTriggered exception that you can catch and handle gracefully.
Tracing and Observability
One of the SDK's most valuable features is automatic tracing. Every agent invocation, tool call, and handoff is recorded. You can view traces in the OpenAI dashboard or export them programmatically:
from agents import trace
async def run_research_pipeline_with_tracing(question: str) -> str:
with trace("Research Pipeline Run"):
result = await Runner.run(
starting_agent=planner_agent,
input=f"Research question: {question}",
)
print(f"Trace completed. View in OpenAI dashboard.")
return result.final_output
Traces show you exactly where time is spent, which tools were called, how many tokens each agent consumed, and where handoffs occurred. This is indispensable for debugging and cost optimization.
Best Practices
After building several production pipelines, the following practices consistently lead to better results:
- Keep agent instructions narrow. Each agent should have one clear job. If you find yourself writing "and also..." in a system prompt, consider splitting into two agents.
- Use typed context. Pydantic models prevent the silent schema drift that destroys multi-agent systems over time. Validate context at every handoff boundary.
- Limit tool sets per agent. Giving an agent tools it doesn't need increases the chance of irrelevant tool calls and wastes tokens. Only attach tools that agent will actually use.
- Design for failure. Web searches fail, pages return 404s, and APIs rate-limit. Build retry logic into your tools and have agents handle empty or error results gracefully.
- Use cheaper models for simpler agents. The Planner and Writer benefit from a strong model like gpt-4o, but the Searcher's tool-calling loop may work fine with gpt-4o-mini, cutting costs significantly.
- Test agents in isolation. Before running the full pipeline, test each agent individually with mock inputs. This makes it far easier to pinpoint where quality drops.
- Cache search results. During development you'll run the pipeline many times. Cache tool outputs to avoid redundant API calls and speed up iteration.
- Set max_turns. Without a limit, a stuck agent can loop indefinitely. Set
max_turns=10or similar onRunner.runto prevent runaway costs.
Extending the Pipeline
Once you have the basic four-agent pipeline working, there are several valuable extensions:
Parallel search agents: Instead of one Searcher handling all sub-questions sequentially, spawn a separate Searcher for each sub-question and run them concurrently with asyncio.gather. This can cut latency dramatically for multi-part questions.
async def parallel_search(sub_questions: list[str]) -> list[dict]:
"""Run searches for all sub-questions in parallel."""
tasks = [
Runner.run(
starting_agent=searcher_agent,
input=f"Search for: {sq}",
)
for sq in sub_questions
]
results = await asyncio.gather(*tasks)
return [r.final_output for r in results]
Fact-checker agent: Add a fifth agent that runs after the Writer and independently verifies key claims against the source material, flagging any unsupported assertions before the report is delivered to the user.
Human-in-the-loop: Use the SDK's input_fn parameter to pause the pipeline after the Planner produces sub-questions, let a human review and edit them, then resume execution. This is especially valuable for high-stakes research where getting the decomposition right matters.
Conclusion
Building a multi-agent research pipeline with the OpenAI Agents SDK transforms a fragile, hard-to-maintain mega-prompt into a modular, observable, and extensible system. By decomposing research into planning, searching, analysis, and writing — each handled by a specialized agent with its own tools and guardrails — you get higher-quality output, better debuggability, and a architecture that scales as your needs grow. Start with the four-agent pipeline in this guide, add tracing from day one, and iterate on individual agents using the best practices covered here. The result is a research system that produces consistently deeper, better-cited, and more reliable reports than any single-agent approach can match.