MCP & AI InfrastructureDeep Dives

AI Agent Workflow Patterns: Fan-Out, Pipeline, and Orchestration

Three fundamental patterns for multi-agent systems.

The Prompt Engineering Project February 26, 2025 12 min read

Quick Answer

AI agent workflow patterns are reusable architectural blueprints for orchestrating autonomous AI agents. The most effective patterns include sequential chains, parallel fan-out, hierarchical delegation, state-machine loops, and human-in-the-loop checkpoints. Choosing the right pattern depends on task complexity, latency requirements, and the level of autonomy your use case demands.

A single AI agent can answer a question. Multiple AI agents working together can run a business process. The difference between these two capabilities is not a matter of scaling up -- it is a matter of architecture. How you coordinate agents determines whether your system is fast or slow, reliable or brittle, cost-efficient or ruinously expensive. And the architecture choices you make at the start become very difficult to change once traffic is flowing.

After building and auditing dozens of multi-agent systems, we have found that nearly every coordination problem maps to one of three fundamental patterns: Fan-Out, Pipeline, and Orchestration. Each has a distinct topology, a specific set of tradeoffs, and a clear set of use cases where it excels. Understanding these patterns is the difference between designing a system and stumbling into one.

Pattern 1: Fan-Out

Fan-Out is the simplest multi-agent pattern. You take one task, dispatch it to multiple agents simultaneously, and aggregate the results. Each agent operates independently. None of them need to know about the others. The coordinator waits for all agents to finish (or for a timeout), then merges the outputs into a unified response.

The architecture looks like this:

fan-out-topology.txt
                    ┌─── Agent A ───┐
                    │               │
Input ── Dispatch ──┼─── Agent B ───┼── Aggregate ── Output
                    │               │
                    └─── Agent C ───┘

Each agent receives the same input.
Each agent produces an independent result.
The aggregator merges all results into a final output.

Fan-Out excels in three scenarios. First, research tasks where you want multiple perspectives on the same question. Send the same query to agents with different system prompts -- one optimized for technical depth, another for business implications, a third for risk analysis -- and combine the results into a comprehensive report. Second, comparison tasks where you need to evaluate alternatives. Dispatch the same evaluation criteria to an agent per option and collect structured assessments in parallel. Third, redundancy tasks where reliability matters more than cost. Run the same task on multiple agents and use majority voting or confidence scoring to select the best output.

fan-out.ts
interface AgentResult<T> {
  agentId: string
  result: T
  latencyMs: number
  error?: string
}

async function fanOut<T>(
  task: string,
  agents: AgentConfig[],
  timeoutMs: number = 30_000
): Promise<AgentResult<T>[]> {
  const controller = new AbortController()
  const timeout = setTimeout(() => controller.abort(), timeoutMs)

  try {
    const promises = agents.map(async (agent) => {
      const start = Date.now()
      try {
        const result = await agent.execute(task, {
          signal: controller.signal,
        })
        return {
          agentId: agent.id,
          result,
          latencyMs: Date.now() - start,
        }
      } catch (err) {
        return {
          agentId: agent.id,
          result: null as T,
          latencyMs: Date.now() - start,
          error: err instanceof Error ? err.message : 'Unknown error',
        }
      }
    })

    const settled = await Promise.allSettled(promises)
    return settled
      .filter((s): s is PromiseFulfilledResult<AgentResult<T>> =>
        s.status === 'fulfilled'
      )
      .map((s) => s.value)
  } finally {
    clearTimeout(timeout)
  }
}
O(1)
Latency (parallel)
O(n)
Cost (per agent)
High
Fault tolerance
None
Agent coupling

Fan-Out Tradeoffs

The primary advantage of Fan-Out is latency. Because all agents run in parallel, your total latency is determined by the slowest agent, not the sum of all agents. A system that fans out to five agents finishes in roughly the same time as a system that calls one agent. This makes Fan-Out ideal for user-facing applications where response time matters.

The primary cost is literal cost. Every agent consumes tokens independently. If you fan out to five agents, you pay five times the inference cost. There is no way around this -- parallelism trades money for time. The aggregation step also introduces complexity. Merging five independent outputs into a coherent result is a non-trivial problem. You need to handle conflicts, deduplicate information, and decide what to do when agents disagree.

Error handling in Fan-Out is comparatively simple. If one agent fails, the others continue. You can return partial results, retry the failed agent, or use the successful results to compensate. The key decision is your failure threshold: does the system succeed if three of five agents complete, or do you require all five?

Set a global timeout on the Fan-Out operation, not just per-agent timeouts. A single slow agent should not hold up the entire response. Return the results you have when the clock runs out.

Pattern 2: Pipeline

A Pipeline is a sequential chain where each agent's output becomes the next agent's input. The agents are ordered, and each one performs a specific transformation or enrichment on the data as it flows through the system. Unlike Fan-Out, agents in a Pipeline are dependent -- Agent B cannot start until Agent A finishes.

pipeline-topology.txt
Input ── Agent A ── Agent B ── Agent C ── Agent D ── Output

Each agent receives the previous agent's output.
Each agent performs one transformation.
The final output is the result of all transformations.

Pipelines are the right choice for multi-step transformations where each step requires different capabilities or context. Consider a content pipeline: Agent A extracts key information from raw documents. Agent B synthesizes the extracted information into a structured summary. Agent C rewrites the summary in a specific brand voice. Agent D performs a quality review and flags issues. Each agent is a specialist. Each one does one thing well. The pipeline as a whole accomplishes something that no single agent could do reliably.

Another strong use case is review chains. Agent A generates a draft. Agent B reviews the draft against a rubric. Agent C revises the draft based on the review. This pattern separates generation from evaluation, which is critical because a single agent reviewing its own work is subject to the same blind spots that produced the original errors.

pipeline.ts
interface PipelineStep<TIn, TOut> {
  name: string
  execute: (input: TIn) => Promise<TOut>
  validate?: (output: TOut) => boolean
}

async function runPipeline<T>(
  input: T,
  steps: PipelineStep<any, any>[],
  onStepComplete?: (step: string, result: any) => void
): Promise<{ result: any; trace: StepTrace[] }> {
  let current: any = input
  const trace: StepTrace[] = []

  for (const step of steps) {
    const start = Date.now()
    try {
      const output = await step.execute(current)

      if (step.validate && !step.validate(output)) {
        throw new Error(
          `Validation failed at step: ${step.name}`
        )
      }

      trace.push({
        step: step.name,
        latencyMs: Date.now() - start,
        status: 'success',
      })

      onStepComplete?.(step.name, output)
      current = output
    } catch (err) {
      trace.push({
        step: step.name,
        latencyMs: Date.now() - start,
        status: 'error',
        error: err instanceof Error ? err.message : 'Unknown',
      })
      throw new PipelineError(step.name, err, trace)
    }
  }

  return { result: current, trace }
}

interface StepTrace {
  step: string
  latencyMs: number
  status: 'success' | 'error'
  error?: string
}

A Pipeline separates generation from evaluation. An agent reviewing its own work is subject to the same blind spots that produced the original errors.

Pipeline Tradeoffs

The primary advantage of a Pipeline is decomposition. Complex tasks that overwhelm a single agent become manageable when broken into focused steps. Each agent can have a specialized system prompt, different model selection (use a fast cheap model for extraction, a powerful model for synthesis), and independent evaluation criteria. You can test, debug, and optimize each step in isolation.

The primary cost is latency. Total latency is the sum of all step latencies. A four-step pipeline where each step takes three seconds takes twelve seconds end-to-end. There is no way to parallelize steps that depend on each other. For user-facing applications, this means Pipeline architectures need streaming or progressive updates to remain usable. Show users what each step produces as it completes, rather than making them wait for the final output.

Error handling in Pipelines is more complex than in Fan-Out because failures cascade. If Agent B fails, Agents C and D never execute. You need a strategy for each step: retry the step, fall back to a simpler agent, skip the step and pass the input through unchanged, or abort the entire pipeline. The right choice depends on whether the step is essential or optional.

Never pass raw, unvalidated output from one pipeline step to the next. Add a validation function between steps that checks structure, length, and content quality. A corrupt intermediate result will poison every downstream step.

Pattern 3: Orchestration

Orchestration is the most powerful and most complex of the three patterns. A coordinator agent receives the initial task and dynamically decides which specialist agents to invoke, in what order, and with what parameters. The routing logic is not predetermined -- it emerges from the coordinator's analysis of the task at hand.

orchestration-topology.txt
                    ┌─── Specialist A
                    │
Input ── Coordinator ──┼─── Specialist B
              │  ^     │
              │  │     └─── Specialist C
              │  │
              └──┘  (loop: coordinator re-evaluates
                     after each specialist responds)

The coordinator is itself an agent -- typically backed by a powerful model -- that understands the capabilities of each specialist and the requirements of the task. It might invoke Specialist A for data retrieval, examine the results, decide that additional context is needed, invoke Specialist B for web search, then route both results to Specialist C for synthesis. The routing graph is constructed at runtime based on the specific input.

This pattern is the right choice for complex workflows where the path through the system depends on the data. Customer support is a canonical example: a coordinator triages the request, routes billing issues to a billing specialist, technical issues to a technical specialist, and escalation requests to a human handoff agent. The coordinator might invoke multiple specialists for a single request if the issue spans domains.

orchestrator.ts
interface Specialist {
  id: string
  description: string
  capabilities: string[]
  execute: (task: string, context: any) => Promise<any>
}

interface RoutingDecision {
  specialistId: string
  task: string
  context: any
  reason: string
}

async function orchestrate(
  input: string,
  specialists: Specialist[],
  maxSteps: number = 10
): Promise<{ result: any; decisions: RoutingDecision[] }> {
  const decisions: RoutingDecision[] = []
  let context: any = { originalInput: input, results: [] }

  for (let step = 0; step < maxSteps; step++) {
    const decision = await coordinator.route({
      input,
      specialists: specialists.map((s) => ({
        id: s.id,
        description: s.description,
        capabilities: s.capabilities,
      })),
      previousResults: context.results,
      step,
    })

    if (decision.action === 'complete') {
      return { result: decision.finalResult, decisions }
    }

    const specialist = specialists.find(
      (s) => s.id === decision.specialistId
    )
    if (!specialist) {
      throw new Error(
        `Unknown specialist: ${decision.specialistId}`
      )
    }

    const result = await specialist.execute(
      decision.task,
      context
    )
    context.results.push({
      specialistId: specialist.id,
      result,
      step,
    })
    decisions.push(decision)
  }

  throw new Error('Orchestration exceeded max steps')
}
Variable
Latency
Variable
Cost
Medium
Fault tolerance
High
Flexibility

Orchestration Tradeoffs

The advantage of Orchestration is adaptability. The system can handle novel inputs that no predetermined workflow anticipated. The coordinator examines the task, selects the right tools, and constructs a custom execution plan. This is the closest analogy to how a human manager delegates work -- not by following a script, but by understanding the problem and matching it to available resources.

The costs are substantial. The coordinator itself consumes tokens on every routing decision. If the coordinator makes poor decisions, the entire system degrades. You are dependent on the coordinator's judgment, which means you need a high-capability model for the coordination layer even if your specialists can run on cheaper models. Debugging is harder because the execution path is dynamic. Two identical inputs might take different routes through the system.

The critical safety mechanism is a step limit. Without one, a confused coordinator can loop indefinitely, invoking specialists in circles while consuming tokens. Set a hard maximum on the number of routing steps and implement a circuit breaker that aborts if cost exceeds a threshold. Log every routing decision so you can audit the coordinator's reasoning after the fact.

Orchestration is powerful because the execution path is dynamic. It is dangerous for exactly the same reason.

Error Handling Across Agents

Every multi-agent system must answer one question before it handles a single request: what happens when an agent fails? The answer is different for each pattern, but the underlying principles are universal.

1

Classify the failure

Distinguish between transient errors (timeouts, rate limits) that should be retried and permanent errors (invalid input, capability mismatch) that should not. Retrying a permanent error wastes tokens and time.

2

Scope the blast radius

In Fan-Out, a failed agent affects only its own result. In a Pipeline, a failed step blocks all downstream steps. In Orchestration, a failed specialist forces the coordinator to re-plan. Design your error handling to match the blast radius of the pattern.

3

Preserve partial work

Never discard successful results because a later step failed. In a Pipeline, cache the output of each completed step so you can resume from the point of failure. In Fan-Out, return the results you have even if some agents timed out.

4

Degrade gracefully

Define what a degraded response looks like. A research Fan-Out that receives three of five results is still useful. A Pipeline that skips the quality review step produces a draft, not nothing. An Orchestrator that cannot reach a specialist can fall back to a general-purpose agent.

Log the full trace of every multi-agent execution: which agents were invoked, in what order, with what inputs, and what they returned. When something goes wrong in production, the trace is your only debugging tool.

Choosing the Right Pattern

The choice between these three patterns is not a matter of preference. It is a matter of constraints. If your primary constraint is latency and you can afford the cost, use Fan-Out. If your primary constraint is correctness and you can afford the time, use Pipeline. If your task is unpredictable and requires dynamic routing, use Orchestration.

In practice, production systems often combine patterns. An Orchestrator might fan out to multiple research agents, collect the results, then pipeline them through a synthesis and review chain. The patterns compose. The key is to be deliberate about which pattern you are applying at each level of the system and why.

Start with the simplest pattern that meets your requirements. A Pipeline handles most real-world needs. Graduate to Orchestration only when the task genuinely requires dynamic routing. And use Fan-Out when you need speed or redundancy that a single agent cannot provide.


Key Takeaways

1

Fan-Out dispatches the same task to multiple agents in parallel. Use it for research, comparison, and redundancy. It trades cost for latency.

2

Pipeline chains agents sequentially where each output feeds the next. Use it for multi-step transformations and review chains. It trades latency for decomposition and correctness.

3

Orchestration uses a coordinator agent to dynamically route tasks to specialists. Use it for unpredictable workflows. It is the most flexible and the most complex.

4

Every multi-agent system needs a strategy for partial failure. Classify errors, scope blast radius, preserve partial work, and degrade gracefully.

5

Production systems often compose patterns. Start with the simplest one that works and add complexity only when the task demands it.

Frequently Asked Questions

Common questions about this topic

Securing AI Tool Use: A Practical GuideContext Delivery Patterns: Feeding AI the Right Information

Related Articles

MCP & AI Infrastructure

Building an MCP Server: Architecture Decisions

Language choice, transport layer, tool registration, error handling, security model, testing, and deployment for a produ...

Intelligent Operations

From Experiment to Production: An AI Operations Checklist

Evaluation framework, prompt versioning, monitoring, cost tracking, fallback strategies, security review, documentation,...

MCP & AI Infrastructure

The MCP Pattern: Giving AI Tools It Can Actually Use

What problem MCP solves, how our server works, and the tool design patterns that make AI agents actually useful.

All Articles