Saturday, March 21

Most developers hit the same wall when building multi-agent workflows with Claude: the first prototype works beautifully in a notebook, then falls apart the moment you add a second agent, a retry loop, or real user traffic. The failure usually isn’t the model — it’s the architecture. After shipping several production multi-agent systems using Claude’s API, I’ve collected enough scar tissue to give you a pattern set that actually holds up.

This guide covers orchestration topology choices, prompt design for agent-to-agent communication, error propagation, and cost control. There’s working code throughout. By the end, you’ll have a concrete implementation blueprint, not a whiteboard diagram.

Why Multi-Agent Architectures Break in Production

Single-agent systems are forgiving. You have one context window, one failure point, one cost center. The moment you chain agents together, you multiply every risk: latency compounds, token costs stack, and a hallucination in agent 2 poisons agents 3 through N.

The three failure modes I see most often:

  • Context bleed: Passing too much raw text between agents instead of structured summaries. Agent 2 spends 3,000 tokens re-reading stuff it doesn’t need.
  • Silent failures: An agent returns a plausible-sounding but wrong result, and the next agent treats it as ground truth with no validation step.
  • Unbounded retries: No circuit breaker logic, so a flaky tool call spins indefinitely and costs you $12 instead of $0.04.

Fix these three things and you’re ahead of 80% of production multi-agent deployments I’ve reviewed.

Choosing Your Orchestration Topology

There are three patterns worth knowing. Pick based on your task structure, not what sounds most impressive.

Sequential Pipeline

Agent A → Agent B → Agent C. Each agent does one thing well and passes a structured result forward. This is the right default for document processing, data enrichment, or any workflow where the output of one step is cleanly the input of the next.

Use this when: steps are deterministic, you need maximum debuggability, and the task graph is linear.

Orchestrator + Subagents

A planning agent decomposes a task and delegates to specialist subagents. The orchestrator collects results and synthesizes. This is the pattern I use for research workflows, code review pipelines, and anything where task decomposition is non-trivial.

The orchestrator is the most expensive agent in the graph — it runs Claude Sonnet or Opus because it’s doing reasoning. Subagents run Haiku where possible. At current pricing, Haiku costs roughly $0.00025 per 1K input tokens versus Sonnet’s $0.003 — a 12x difference that matters at scale.

Parallel Fan-Out with Aggregator

One orchestrator spawns N parallel agents, then an aggregator merges results. Great for competitive analysis, multi-source research, or any task you can split into independent subtasks. Latency is bounded by the slowest agent, not the sum of all agents.

The trap here: developers often fan out too aggressively and hit rate limits. Claude’s API has per-minute token limits that vary by tier. If you’re spawning 10 parallel Sonnet calls, you’ll hit the limit faster than you expect on a standard account.

Production-Ready Orchestrator Pattern

Here’s a minimal but battle-tested orchestrator implementation. I’m using the Anthropic Python SDK with structured outputs via tool use.

import anthropic
import json
from typing import Any

client = anthropic.Anthropic()

# Define the task decomposition tool the orchestrator uses
DECOMPOSE_TOOL = {
    "name": "decompose_task",
    "description": "Break a complex task into discrete subtasks for specialist agents",
    "input_schema": {
        "type": "object",
        "properties": {
            "subtasks": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "agent_type": {"type": "string"},
                        "instruction": {"type": "string"},
                        "context": {"type": "string"},
                        "priority": {"type": "integer"}
                    },
                    "required": ["agent_type", "instruction", "priority"]
                }
            },
            "synthesis_instructions": {"type": "string"}
        },
        "required": ["subtasks", "synthesis_instructions"]
    }
}

def orchestrate(task: str, available_agents: list[str]) -> dict:
    """
    Orchestrator: decomposes task into subtasks and routes to agents.
    Uses Sonnet for reasoning quality; subagents will use Haiku.
    """
    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        tools=[DECOMPOSE_TOOL],
        tool_choice={"type": "tool", "name": "decompose_task"},
        messages=[{
            "role": "user",
            "content": f"""You are a task orchestrator. Available specialist agents: {available_agents}
            
            Decompose this task into subtasks. Assign each subtask to the most appropriate agent.
            Keep context minimal — only include what each agent strictly needs.
            
            Task: {task}"""
        }]
    )
    
    # Extract the tool use result
    tool_result = next(
        block for block in response.content 
        if block.type == "tool_use"
    )
    return tool_result.input


def run_subagent(agent_type: str, instruction: str, context: str = "") -> dict:
    """
    Subagent runner. Uses Haiku for cost efficiency.
    Returns structured result with status field for error handling.
    """
    system_prompts = {
        "researcher": "You are a research specialist. Return findings as structured data, not prose.",
        "analyst": "You are a data analyst. Return analysis with confidence scores.",
        "writer": "You are a technical writer. Return clean, formatted content only."
    }
    
    system = system_prompts.get(agent_type, "Complete the assigned task precisely.")
    
    try:
        response = client.messages.create(
            model="claude-haiku-4-5",  # Haiku for cost efficiency on subagents
            max_tokens=512,
            system=system,
            messages=[{
                "role": "user",
                "content": f"Context: {context}\n\nTask: {instruction}" if context else instruction
            }]
        )
        return {
            "status": "success",
            "agent_type": agent_type,
            "result": response.content[0].text,
            "tokens_used": response.usage.input_tokens + response.usage.output_tokens
        }
    except Exception as e:
        # Structured error — don't let exceptions silently corrupt downstream agents
        return {
            "status": "error",
            "agent_type": agent_type,
            "error": str(e),
            "result": None,
            "tokens_used": 0
        }

The key decisions here: the orchestrator forces a tool call (no free-form text that requires parsing), subagents return a status field so failures are explicit, and context is trimmed before passing downstream.

Error Handling That Doesn’t Lie to You

The worst pattern I see is try/except that swallows errors and returns empty strings. The next agent gets an empty string, produces confident-sounding garbage, and you spend 40 minutes debugging the wrong thing.

Build error propagation into your result schema from day one:

def aggregate_results(subtask_results: list[dict], synthesis_instructions: str) -> str:
    """
    Aggregator agent. Handles partial failures gracefully.
    Only synthesizes successful results, but reports failures explicitly.
    """
    successful = [r for r in subtask_results if r["status"] == "success"]
    failed = [r for r in subtask_results if r["status"] == "error"]
    
    # If more than half the agents failed, abort rather than synthesize garbage
    if len(failed) > len(successful):
        raise RuntimeError(
            f"Too many subagent failures ({len(failed)}/{len(subtask_results)}). "
            f"Errors: {[f['error'] for f in failed]}"
        )
    
    # Build context for synthesis — include failure info so the synthesizer knows what's missing
    context_parts = []
    for r in successful:
        context_parts.append(f"[{r['agent_type'].upper()}]\n{r['result']}")
    
    if failed:
        failure_note = f"\nNote: The following agents failed and their data is unavailable: {[f['agent_type'] for f in failed]}"
        context_parts.append(failure_note)
    
    context = "\n\n".join(context_parts)
    
    response = client.messages.create(
        model="claude-sonnet-4-5",  # Back to Sonnet for synthesis reasoning
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"Synthesize the following agent outputs per these instructions: {synthesis_instructions}\n\n{context}"
        }]
    )
    
    total_tokens = sum(r["tokens_used"] for r in subtask_results) + response.usage.input_tokens + response.usage.output_tokens
    print(f"Total tokens used: {total_tokens} (~${total_tokens * 0.000003:.4f} at Sonnet pricing)")
    
    return response.content[0].text

Cost Control at the Architecture Level

Token costs in multi-agent systems compound faster than people expect. A workflow with 5 agents, each passing full context forward, can easily consume 50,000 tokens for a task that should take 8,000. Here’s what actually moves the needle:

Model Tiering

Use Opus only when you genuinely need maximum reasoning (complex planning, ambiguous instructions). Use Sonnet for orchestration and synthesis. Use Haiku for everything mechanical: formatting, extraction, classification, simple Q&A. A well-tiered 5-agent workflow costs roughly $0.003–$0.006 per run. An un-tiered one using Sonnet everywhere costs $0.03–$0.05. At 10,000 runs a month, that’s $250 vs $2,500.

Context Compression Between Agents

Never pass raw agent output to the next agent if you can pass a structured summary instead. If your researcher agent returns 800 words of findings, have it also return a 50-word structured summary that downstream agents use by default. Only unpack the full result if the downstream agent explicitly needs it.

Token Budgets per Agent

Set max_tokens aggressively for subagents. If a classification agent needs 50 tokens, don’t give it 512. Unused capacity isn’t charged, but it signals to the model that long responses are acceptable — and models tend to fill available space.

Debugging Multi-Agent Workflows Without Losing Your Mind

When a workflow produces a bad output, you need to know which agent caused it. Instrument every agent call with a correlation ID and log the full input/output:

import uuid
import logging

logger = logging.getLogger(__name__)

def run_subagent_instrumented(agent_type: str, instruction: str, 
                               context: str = "", run_id: str = None) -> dict:
    """Wrapped subagent with full observability."""
    run_id = run_id or str(uuid.uuid4())[:8]
    
    logger.info(f"[{run_id}] Starting {agent_type} agent")
    logger.debug(f"[{run_id}] Input tokens estimate: {len(instruction.split()) * 1.3:.0f}")
    
    result = run_subagent(agent_type, instruction, context)
    
    logger.info(f"[{run_id}] {agent_type} completed: status={result['status']}, tokens={result['tokens_used']}")
    if result["status"] == "error":
        logger.error(f"[{run_id}] {agent_type} failed: {result['error']}")
    
    result["run_id"] = run_id
    return result

Ship this to a structured log aggregator (Datadog, Axiom, even a simple Postgres table) and you can reconstruct any workflow execution after the fact. This matters more than you think — the first time a client asks “why did it produce X last Tuesday?” you’ll be glad you have it.

When to Use This Architecture (and When Not To)

Multi-agent workflows with Claude make sense when your task genuinely has separable concerns that benefit from specialization. Research → analysis → writing is the canonical example. Code review with security, performance, and style agents is another good fit.

Don’t multi-agent everything. A single well-prompted Claude Sonnet call handles most tasks that look like they need agents. The overhead of orchestration — latency, cost, debugging complexity — only pays off when the task is genuinely complex enough that one context window or one model role can’t handle it well.

My rule: if you can write a single system prompt that handles the full task reliably, do that. Only decompose when you’re hitting quality ceilings with a single agent or when you need true parallelism for latency reasons.

Who Should Build What

Solo founders and small teams: Start with the sequential pipeline. It’s the easiest to debug, cheapest to run, and handles 80% of use cases. Add orchestration complexity only when you hit a concrete quality problem that simpler approaches can’t solve.

Teams building production automation products: Implement the orchestrator + subagents pattern with full instrumentation from day one. The logging overhead is worth it. Use model tiering aggressively — it’s the single biggest cost lever you have.

High-volume workflows (10K+ runs/month): Invest in a proper observability stack before you scale. The cost of debugging production issues without it far exceeds the cost of building it upfront. Also seriously evaluate caching for deterministic subtasks — if your researcher agent is answering the same questions repeatedly, prompt caching on Claude can cut those costs by up to 90%.

Building reliable multi-agent workflows with Claude is an engineering problem more than an AI problem. Get the plumbing right — error handling, observability, model tiering — and the LLM part mostly takes care of itself.

Editorial note: API pricing, model capabilities, and tool features change frequently — always verify current details on the vendor’s website before building in production. Code examples are tested at time of writing; pin your dependency versions to avoid breaking changes. Some links in this article may be affiliate links — we may earn a commission if you sign up, at no extra cost to you.

Share.
Leave A Reply