Skip to contents

Choosing a multi-agent pattern

When a single agent cannot handle a task because it has distinct stages or requires different expertise for different subtasks, orchestr offers two multi-agent patterns: pipelines and supervisors.

A pipeline is the right choice when you know the sequence of steps at design time. Data flows linearly from one specialist to the next, like an assembly line. Each agent makes exactly one LLM call, so costs are predictable and execution order is deterministic.

A supervisor is the right choice when the routing depends on the content of the request. A coordinator agent reads the user’s message, decides which specialist should handle it, and dispatches accordingly. The supervisor can route to multiple workers across several iterations, which makes it suitable for open-ended tasks where the needed expertise is not known in advance.

Here is a comparison of the two patterns:

 Pattern     | Flow          | LLM Calls  | Best For
 ------------|---------------|------------|----------------------------------
 Pipeline    | A -> B -> C   | 1 per node | Fixed stages, predictable cost
 Supervisor  | Router -> *   | 2+ per     | Dynamic routing, open-ended tasks
             |               | iteration  |

If your workflow has a fixed sequence of steps, start with a pipeline. If the routing logic depends on the user’s input, use a supervisor. If you need something more complex (conditional branches, cycles, human-in-the-loop approvals), drop down to graph_builder() for full control over the topology.

Pipeline pattern

A pipeline passes state through a sequence of agents, each transforming it before handing off to the next. Each agent has a focused role, which makes individual agents easier to test and swap than a single monolithic prompt.

The pipeline flow looks like this:

 [User Input]
      |
      v
 +-----------+     +-----------+     +-----------+
 |  Agent A  | --> |  Agent B  | --> |  Agent C  |
 | (profile) |     | (analyze) |     |  (report) |
 +-----------+     +-----------+     +-----------+
                                          |
                                          v
                                    [Final Output]

Use pipeline_graph() for a concise setup, or graph_builder() for full control.

Using pipeline_graph()

The simplest way to create a pipeline. Pass agents in order and orchestr wires the edges automatically.

library(orchestr)
library(ellmer)

drafter <- agent("drafter", chat = chat_anthropic(
  system_prompt = "Write a short draft on the given topic."
))

editor <- agent("editor", chat = chat_anthropic(
  system_prompt = "Improve the following draft. Fix grammar and clarity."
))

pipeline <- pipeline_graph(drafter, editor)

result <- pipeline$invoke(list(
  messages = list("Write about the benefits of open source software.")
))
cat(result$messages[[length(result$messages)]])

Data analysis pipeline

A more realistic pipeline that profiles data, analyzes patterns, and produces a report. The profiler gathers facts, the analyst interprets them, and the reporter writes up the results. Each agent sees the accumulated conversation history, so the analyst can reference the profiler’s output and the reporter can reference both.

profiler <- agent("profiler", chat = chat_anthropic(
  system_prompt = "Profile datasets: describe columns, types, missing values, distributions."
))

analyst <- agent("analyst", chat = chat_anthropic(
  system_prompt = "Given a data profile, identify patterns, correlations, and anomalies."
))

reporter <- agent("reporter", chat = chat_anthropic(
  system_prompt = "Write a clear, non-technical summary of analytical findings."
))

graph <- pipeline_graph(profiler, analyst, reporter)
result <- graph$invoke(list(messages = list(
  "Analyze the mtcars dataset focusing on fuel efficiency factors."
)))

Using graph_builder() directly

For conditional edges, cycles, or custom node functions, use the builder API. pipeline_graph() and supervisor_graph() use this same API internally. The builder exposes the full graph topology: add nodes, wire edges (including conditional edges), and set the entry point explicitly.

drafter <- agent("drafter", chat = chat_anthropic(
  system_prompt = "Write a short draft on the given topic."
))

editor <- agent("editor", chat = chat_anthropic(
  system_prompt = "Improve the following draft. Fix grammar and clarity."
))

g <- graph_builder()
g$add_node("draft", drafter)
g$add_node("edit", editor)
g$add_edge("draft", "edit")
g$add_edge("edit", END)
g$set_entry_point("draft")

pipeline <- g$compile(verbose = TRUE)

result <- pipeline$invoke(list(
  messages = list("Write about functional programming in R.")
))
cat(result$messages[[length(result$messages)]])

Supervisor pattern

Supervisors make routing decisions at runtime. A supervisor agent reads the user’s request, picks the right specialist, and dispatches the work using a route tool that orchestr injects automatically. After the worker responds, control returns to the supervisor, which can route again or finish.

A question about calculus goes to the math worker; a request to polish prose goes to the writing worker. The supervisor decides using the LLM’s own judgment, guided by its system prompt and the worker descriptions.

The routing flow looks like this:

                 +------------+
                 | Supervisor |
                 +-----+------+
                       |
          route("math") | route("writing") | FINISH
               |                |               |
               v                v               v
          +--------+      +----------+      [Output]
          |  Math  |      | Writing  |
          +--------+      +----------+
               |                |
               +----+     +----+
                    |     |
                    v     v
               (back to Supervisor)
supervisor <- agent("supervisor", chat = chat_anthropic(
  system_prompt = "You coordinate workers to solve tasks."
))

math_worker <- agent("math", chat = chat_anthropic(
  system_prompt = "You are a math expert. Solve math problems step by step."
))

writing_worker <- agent("writing", chat = chat_anthropic(
  system_prompt = "You are a writing expert. Help with writing tasks."
))

graph <- supervisor_graph(
  supervisor = supervisor,
  workers = list(math = math_worker, writing = writing_worker),
  max_iterations = 10
)

# Route to math
result <- graph$invoke(list(
  messages = list("Calculate the integral of x^2 from 0 to 1.")
))

The supervisor automatically receives a route tool that it calls to dispatch to workers or finish. Start with low max_iterations values to control costs; each iteration involves an LLM call for the supervisor plus the selected worker.

State management

All graph types in orchestr share a common state model. State is a named list that flows through the graph and gets modified by each node. By default, orchestr uses a simple schema where messages is a list that gets appended to as agents respond.

State flow determines what each agent sees. In a pipeline, the second agent sees the first agent’s output in the message history. In a supervisor, each worker sees the full conversation including the supervisor’s routing decisions. Agents build on each other’s work without explicit message passing.

For advanced use cases, StateSchema$new() lets you define typed fields with custom reducers. For example, you might accumulate a findings list across pipeline stages while keeping a summary field that gets overwritten by the final agent.

Streaming state snapshots

Use $stream() to collect state snapshots at each step. This is useful for building progress indicators in interactive applications, or for debugging graph execution by looking at intermediate states.

pipeline <- pipeline_graph(
  agent("drafter", chat = chat_anthropic(
    system_prompt = "Write a short draft on the given topic."
  )),
  agent("editor", chat = chat_anthropic(
    system_prompt = "Improve the following draft."
  ))
)

snapshots <- pipeline$stream(list(
  messages = list("Write about functional programming in R.")
))

for (snap in snapshots) {
  cat(sprintf("Step %d, node: %s\n", snap$step, snap$node))
}

Visualizing the graph

as_mermaid() generates a Mermaid diagram string for a graph, which you can render in any Mermaid-compatible viewer (GitHub markdown, pkgdown sites, Quarto documents, or the Mermaid live editor).

supervisor <- agent("supervisor", chat = chat_anthropic(
  system_prompt = "You coordinate workers."
))
math_worker <- agent("math", chat = chat_anthropic(
  system_prompt = "Math expert."
))
writing_worker <- agent("writing", chat = chat_anthropic(
  system_prompt = "Writing expert."
))

graph <- supervisor_graph(
  supervisor = supervisor,
  workers = list(math = math_worker, writing = writing_worker)
)

cat(as_mermaid(graph))
# Output:
# graph TD
#     supervisor[supervisor]
#     math[math]
#     writing[writing]
#     supervisor -->|math| math
#     supervisor -->|writing| writing
#     math --> supervisor
#     writing --> supervisor

Next steps