A single agent is good at following instructions. A pipeline of agents — each specialized, each doing one thing well — is genuinely useful for production workloads.
This article covers the architecture we use for multi-agent pipelines on ThetaZero: how to design agent roles, how to pass context between agents, how to handle failures, and when to use a coordinator vs. a sequential chain.
Why Multi-Agent?
The core insight is specialization reduces context contamination. When you ask a single agent to research, analyze, and write — all in one prompt — it compromises on each. The research phase pollutes the writing phase with raw noise. The writing phase rushes the analysis.
With separate agents:
- Each agent has a focused system prompt tuned for its role
- Each agent's context window is used only for its specific task
- You can retry individual stages without rerunning the whole pipeline
- You can swap in a cheaper model for simpler stages (e.g., use
claude-haiku-4for the Writer)
The tradeoff: more orchestration complexity and higher latency. For scheduled, async workloads — this is almost always worth it.
Pipeline Architecture
Here's the pipeline we'll build:
The Shared Context Store
Agents in a pipeline communicate via a shared context store — a key-value store scoped to the pipeline execution. Each agent reads the outputs of previous agents and writes its own output.
ThetaZero's Swarm Context API provides this out of the box:
// Researcher agent writes its findings
await ThetaZero.context.write('research/raw_findings', {
sources: [...],
quotes: [...],
data_points: [...],
gaps: [...] // What couldn't be found
});
// Analyst agent reads researcher output
const research = await ThetaZero.context.read('research/raw_findings');
// ... processes ...
await ThetaZero.context.write('analysis/insights', {
key_findings: [...],
confidence_scores: {...},
recommended_angles: [...]
});
// Writer reads analyst output
const insights = await ThetaZero.context.read('analysis/insights');
// ... writes report ...
The namespace structure (research/, analysis/) keeps outputs organized and avoids collisions.
The Coordinator Pattern
The Coordinator is an agent that doesn't do the work — it decides how to decompose the goal and dispatches subtasks to the right agents. This is the most important agent in a complex pipeline.
// Coordinator system prompt
const COORDINATOR_PROMPT = `You are a pipeline coordinator.
Your job is to break down the user's goal into specific subtasks
and dispatch them to the right agents.
Available agents:
- researcher: Gathers raw information from the web
- analyst: Extracts patterns and insights from raw data
- writer: Produces polished, formatted reports
Rules:
1. Always start with the researcher
2. Pass specific search queries to the researcher, not vague goals
3. Tell the analyst what to look for — don't just dump raw data
4. Tell the writer the format, audience, and tone
Output a JSON task plan with agent, instruction, and dependencies.`;
// Example coordinator output
const taskPlan = {
tasks: [
{
id: "t1",
agent: "researcher",
instruction: "Search for and scrape these 5 competitor pricing pages. Extract: tier names, monthly prices, feature limits per tier, any recent pricing changes (check Wayback Machine for last 90 days).",
dependencies: []
},
{
id: "t2",
agent: "analyst",
instruction: "Analyze the competitor pricing data. Find: price clustering points, feature differentiation patterns, which features are always in highest tier. Rate competitive positioning for each competitor 1-10.",
dependencies: ["t1"]
},
{
id: "t3",
agent: "writer",
instruction: "Write a pricing strategy briefing for our product team. Audience: B2B SaaS product managers. Tone: direct and data-driven. Include a comparison table and 3 concrete recommendations.",
dependencies: ["t2"]
}
]
};
Error Handling and Retries
In production pipelines, individual agents will fail. The pattern we use:
async function runPipelineStage(agentId, taskInstruction, maxRetries = 2) {
let attempt = 0;
while (attempt <= maxRetries) {
try {
const result = await ThetaZero.agents.run(agentId, {
task: taskInstruction,
timeout_ms: 120_000, // 2 minute timeout per stage
});
if (result.status === 'completed') {
return result;
}
// Partial success — log and retry with modified instruction
if (result.status === 'partial' && attempt < maxRetries) {
taskInstruction = `Previous attempt was incomplete: ${result.partial_output}.
Retry and fill in what's missing. Don't repeat what was already done.`;
}
} catch (err) {
console.error(`Stage ${agentId} failed (attempt ${attempt + 1}):`, err.message);
}
attempt++;
if (attempt <= maxRetries) {
await sleep(2000 * attempt); // Exponential backoff
}
}
throw new Error(`Pipeline stage ${agentId} failed after ${maxRetries + 1} attempts`);
}
Model Selection by Stage
Not every stage needs the most powerful model. Here's our production selection:
| Stage | Model | Reason |
|---|---|---|
| Coordinator | claude-opus-4 | High-stakes reasoning — decomposing the goal correctly |
| Researcher | claude-sonnet-4 | Tool use + judgment on source quality |
| Analyst | claude-opus-4 | Pattern recognition + nuanced insight extraction |
| Writer | claude-haiku-4 | Formatting structured data — speed over depth |
This saves roughly 40% on model costs vs. using Opus for every stage.
Putting It Together
Here's the full orchestration code for a research → analysis → report pipeline:
const ThetaZero = require('@thetazero/sdk');
const tz = new ThetaZero({ apiKey: process.env.TZ_API_KEY });
async function runResearchPipeline(topic) {
const pipelineId = `research-${Date.now()}`;
console.log(`Starting pipeline: ${pipelineId}`);
// Stage 1: Research
console.log('[1/3] Researcher running...');
await tz.agents.run('researcher', {
pipelineId,
task: `Research the following topic thoroughly: "${topic}".
Search for recent developments (last 6 months), key statistics,
expert opinions, and contrarian views. Save findings to context.`,
model: 'claude-sonnet-4',
tools: ['web_search', 'web_fetch', 'save_context'],
});
// Stage 2: Analysis
console.log('[2/3] Analyst running...');
const research = await tz.context.read(pipelineId, 'research/raw_findings');
await tz.agents.run('analyst', {
pipelineId,
task: `Analyze the research findings and extract the 5 most important
insights. For each insight, provide: supporting evidence, confidence
level (1-10), and business implications. Input: ${JSON.stringify(research)}`,
model: 'claude-opus-4',
tools: ['save_context'],
});
// Stage 3: Write
console.log('[3/3] Writer running...');
const insights = await tz.context.read(pipelineId, 'analysis/insights');
const report = await tz.agents.run('writer', {
pipelineId,
task: `Write a concise executive briefing on "${topic}" based on these
insights: ${JSON.stringify(insights)}. Format: markdown.
Include: TL;DR (3 bullets), Key Findings section, Implications,
Recommended Actions. Max 600 words.`,
model: 'claude-haiku-4',
tools: ['save_report'],
});
console.log(`Pipeline complete. Report saved.`);
return report;
}
// Run it
runResearchPipeline('AI agent compute costs in 2026')
.then(report => console.log('Done:', report.reportId))
.catch(err => console.error('Pipeline failed:', err));
When NOT to Use a Pipeline
Pipelines add latency and complexity. Use a single agent when:
- The task is under 5 minutes and fits in one context window
- You need interactive, low-latency output (user is watching)
- The task doesn't benefit from specialization — e.g., "summarize this text"
Upgrade to a pipeline when:
- The task has distinct phases with different skill requirements
- You're hitting context window limits in a single agent
- You want to parallelize independent research tasks (run multiple researchers simultaneously)
- You need quality checkpoints between stages
Next Steps
The pipeline in this tutorial runs sequentially. The next level is parallel orchestration — running multiple Researcher agents simultaneously on different subtopics, then merging their outputs in the Analyst stage. That reduces latency by 60–70% for research-heavy workloads.
We'll cover that pattern, plus circuit breakers and dead letter queues, in a follow-up post.