The author spent six months building a swarm for a task that was secretly a directed acyclic graph. Not six months of iteration. Six months of building, debugging, rewriting the coordination layer, adding tracing that was never needed, and watching a system that should have been deterministic behave like weather.
The task was document processing: extract, validate, enrich, summarize, format. Five stages. Genuinely sequential. The swarm version used peer-to-peer negotiation between agents. The pipeline rewrite took three weeks and was faster, cheaper, and more reliable.
The swarm wasn’t wrong as an architecture. It was wrong for the task. That mismatch is the cost nobody warns you about.
The Coordination Tax
A March 2026 controlled study tested four coordination architectures on identical software engineering tasks with a fixed $50 budget and the same LLM:
- Single agent: 28 of 28 tasks succeeded
- Hierarchical coordination: 18 of 28 tasks succeeded
- Self-organized swarm: 32% success rate
- Gated pipeline: never produced implementation code, consumed entire budget on planning phases
The hierarchy didn’t fail because the model was weak. It failed because coordination complexity introduces information loss that compounds at every handoff. The single agent retained 85.8% of the original specification entropy at verification. The hierarchy retained 25.8%. Three-quarters of the task intent was lost in translation.
This is not a model problem. It’s a topology problem.
Three Orchestration Patterns
Pipeline (Sequential DAG)
A pipeline is a directed acyclic graph where each node executes after its dependencies complete. State flows forward. Retry happens at the node level.
Runtime characteristics:
- Deterministic execution order
- No message passing overhead between stages
- State is immutable once a stage completes
- Retry semantics are local to the failing node
- Observability is linear: you can trace a single execution path
Failure modes:
- Brittle when dependencies are actually conditional
- No parallelism unless you explicitly fork the DAG
- Upstream failures cascade downstream without negotiation
When to use:
- Task has clear sequential dependencies
- Each stage produces a well-defined artifact for the next stage
- You need deterministic replay for debugging or compliance
Swarm (Peer-to-Peer Coordination)
A swarm is a collection of agents that coordinate through message passing or shared state. No central orchestrator. Agents negotiate task allocation and synchronization.
Runtime characteristics:
- Non-deterministic execution order
- High message passing overhead (agents poll or subscribe to state changes)
- State synchronization requires consensus or eventual consistency
- Retry semantics are distributed: one agent’s failure may trigger renegotiation across the swarm
- Observability requires distributed tracing across all agents
Failure modes:
- Coordination overhead dominates when task is actually sequential
- Debugging requires reconstructing causal chains from message logs
- Emergent behavior can be non-reproducible
When to use:
- Task decomposition is unknown at design time
- Agents need to adapt to changing conditions (resource availability, partial failures)
- Parallelism is essential and dependencies are sparse
Graph (Conditional DAG with Dynamic Routing)
A graph is a DAG where edges can be conditional and nodes can spawn new subgraphs at runtime. State flows forward but routing decisions happen dynamically.
Runtime characteristics:
- Execution order is partially deterministic (fixed topology, conditional edges)
- Message passing overhead is lower than swarm (only at decision points)
- State is versioned: conditional branches may need to reconcile divergent state
- Retry semantics are hybrid: local retries for nodes, global retries for subgraphs
- Observability requires tracking which branches were taken
Failure modes:
- Complexity explodes if you add too many conditional edges
- State reconciliation bugs when branches merge
- Harder to reason about than a pure pipeline, more brittle than a swarm
When to use:
- Task has known stages but routing between stages depends on runtime conditions
- You need determinism for the happy path but flexibility for edge cases
- Parallelism is needed but only within bounded subgraphs
Topology Mismatch Detection
You can detect topology mismatch during development by measuring coordination overhead relative to task execution time.
Pipeline mismatch signals:
- Agents are waiting idle because they can’t start until a sequential dependency completes, but the dependency is actually optional
- You’re adding conditional logic to skip stages, which means you need a graph
Swarm mismatch signals:
- Agents are negotiating task allocation but always converge to the same assignment (the task is deterministic)
- Message passing latency exceeds task execution time
- You’re adding sequence numbers or causal ordering to messages (you need a pipeline)
Graph mismatch signals:
- You have more than three conditional branches per decision point (you need a swarm to explore the space)
- State reconciliation logic is more complex than the task logic (you need a pipeline with explicit forks)
Architecture: Pipeline with Escape Hatches
For most document processing, data transformation, and multi-step reasoning tasks, start with a pipeline and add conditional edges only when runtime data forces it.
from typing import Protocol, TypeVar, Generic
from dataclasses import dataclass
from enum import Enum
T = TypeVar('T')
class StageResult(Generic[T]):
"""Immutable result from a pipeline stage."""
data: T
metadata: dict
next_stage: str | None # None = terminal
class Stage(Protocol[T]):
def execute(self, input: T) -> StageResult[T]:
"""Execute stage logic. Raise on failure."""
...
class PipelineExecutor:
def __init__(self, stages: dict[str, Stage]):
self.stages = stages
self.trace = [] # Linear execution log
def run(self, initial_data, start_stage: str):
current_stage = start_stage
data = initial_data
while current_stage:
stage = self.stages[current_stage]
try:
result = stage.execute(data)
self.trace.append({
'stage': current_stage,
'status': 'success',
'metadata': result.metadata
})
data = result.data
current_stage = result.next_stage
except Exception as e:
self.trace.append({
'stage': current_stage,
'status': 'failed',
'error': str(e)
})
raise
return data
Key properties:
- Each stage returns an immutable result and declares the next stage
- Conditional routing happens inside the stage (via
next_stage) - Trace is a linear log, not a distributed trace
- Retry logic is external to the executor (wrap stages in retry decorators)
When to add graph semantics:
- A stage needs to fork into parallel subgraphs (add a
ForkStagethat returns multiplenext_stagevalues) - A stage needs to wait for multiple upstream stages (add a
JoinStagethat blocks until all inputs arrive)
When to switch to swarm:
- You can’t predict the stage sequence at design time
- Agents need to bid on tasks based on runtime resource availability
Coordination Overhead Comparison
| Topology | Message Passing | State Sync | Retry Scope | Observability | Determinism |
|---|---|---|---|---|---|
| Pipeline | None (direct handoff) | Immutable forward flow | Per-stage | Linear trace | Full |
| Graph | At decision points | Versioned state at joins | Per-stage or per-subgraph | Branch trace | Partial (conditional edges) |
| Swarm | Continuous (polling or pub/sub) | Consensus or eventual consistency | Distributed renegotiation | Distributed trace | None |
Failure Mode Catalog
Pipeline forced into swarm topology:
- Agents negotiate task allocation for a task with fixed dependencies
- Coordination latency exceeds task execution time
- Non-deterministic execution order breaks assumptions about state availability
Swarm forced into pipeline topology:
- Central orchestrator becomes bottleneck for task allocation
- Agents can’t adapt to partial failures (pipeline retries entire stage)
- Parallelism is lost because stages are artificially serialized
Graph forced into pipeline topology:
- Conditional logic is pushed into stage code instead of topology
- State reconciliation happens manually instead of at join nodes
- You end up with a pipeline full of if/else branches
Graph forced into swarm topology:
- Agents negotiate routing decisions that are actually deterministic
- Coordination overhead for decisions that could be static
Technical Verdict
Use a pipeline when:
- Task has sequential dependencies that are known at design time
- Each stage produces a well-defined artifact
- You need deterministic replay for debugging or compliance
- Coordination overhead would exceed task execution time
Use a graph when:
- Task has known stages but routing depends on runtime conditions
- You need parallelism within bounded subgraphs
- State reconciliation at merge points is simpler than swarm consensus
Use a swarm when:
- Task decomposition is unknown at design time
- Agents need to adapt to changing resource availability
- Parallelism is essential and dependencies are sparse
- You can tolerate non-deterministic execution order
Avoid:
- Building a swarm for a task with fixed sequential dependencies (you’ll spend six months debugging coordination)
- Building a pipeline for a task that needs runtime adaptation (you’ll end up with a pipeline full of conditional logic)
- Building a graph with more than three conditional branches per decision point (switch to a swarm)
The rewrite from swarm to pipeline took three weeks because the task was always a DAG. The six months were spent fighting the topology, not the task.