Multi-Agent Orchestration with CrewAI
L1 named the multi-agent topologies; L5 gave you durable single graphs. Now you compose teams. CrewAI models a team as a crew of role-playing agents working tasks — the most approachable on-ramp to multi-agent, and a clean lens on when splitting the work actually helps.
Instead of one agent doing everything, you can have several specialized agents collaborate: a researcher, a writer, a reviewer. Orchestration is coordinating who does what and how they pass work along. This section covers the main frameworks (CrewAI, AutoGen) and patterns for reliable multi-agent systems.
The words you'll hear (in plain terms):
| Term | What it actually means |
|---|---|
| multi-agent | several agents, each with a role, working on parts of a task. |
| orchestration | coordinating agents — order, handoffs, and who decides what. |
| role | an agent's job + expertise (e.g. 'senior researcher'). |
| handoff | passing work (and context) from one agent to the next. |
| crew / team | a named group of agents with a shared goal. |
What you need before starting:
- Build a single agent (Ch 4) first — multi-agent is agents composed.
- Python basics; comfort with the agent loop concept.
pip install crewaiorautogen-agentchatfor the labs.
New to the topic? Read this box, then take the chapters in order — each section is tagged essential → expert so you always know the depth you're at.
- an Anthropic API key (
ANTHROPIC_API_KEY) +pip install anthropic
Learning objectives
- Explain CrewAI's model: Agents, Tasks, Crews, and Processes.
- Build a sequential crew where each agent's output feeds the next.
- Use a hierarchical (manager) process — the supervisor topology from L1.
- Give agents tools and know when to reach for CrewAI Flows instead.
- Decide honestly whether a task needs multiple agents at all.
The CrewAI model: role-play as architecture essential
CrewAI's core metaphor is a team of specialists. You define agents by role, give each a goal and a backstory, hand them tasks, and assemble them into a crew that runs under a process (sequential or hierarchical). The role/goal/backstory isn't fluff — it's how you steer each agent's behavior, exactly like a system prompt (Chapter 2).
| Concept | What it is |
|---|---|
| Agent | A role-playing worker: role, goal, backstory, an LLM, and optional tools. |
| Task | A unit of work with a description and an expected_output, assigned to an agent. |
| Crew | The team: a set of agents + tasks + a process that runs them. |
| Process | How tasks run: sequential (in order) or hierarchical (a manager delegates). |
This picture is the whole idea of a crew: a small team of AI agents, each with one job, passing work down a line like an assembly line. There are three specialists here and the work flows left to right.
- The three boxes are three agents, each a specialist: a Researcher who gathers facts, a Writer who drafts a report from those facts, and an Editor who polishes the draft. Each one is really just its own LLM with a role.
- The arrows are the hand-offs. The researcher's finished output becomes the writer's starting material; the writer's draft becomes the editor's. Nobody starts from scratch — each agent builds on the one before it.
- This left-to-right, fixed order is called a sequential process in CrewAI: tasks run one after another in the order you list them.
In short: A crew is a pipeline of experts. Read it as "first this specialist, then hand the result to the next" — exactly how a human team would pass a document around for research, writing, then editing.
Lab M1.1 · A sequential crew essential
shellpip install crewai
crew.pyfrom crewai import Agent, Task, Crew, Process
# CrewAI uses LiteLLM under the hood; point it at Claude via the model string.
LLM = "anthropic/claude-opus-4-8"
researcher = Agent(
role="Research Analyst",
goal="Find accurate, current facts on the topic",
backstory="You are meticulous and cite sources.",
llm=LLM,
)
writer = Agent(
role="Technical Writer",
goal="Turn research into a clear one-page brief",
backstory="You write plainly for busy engineers.",
llm=LLM,
)
research = Task(
description="Research the current state of {topic}.",
expected_output="5 bullet points with sources.",
agent=researcher,
)
write = Task(
description="Write a one-page brief from the research.",
expected_output="A markdown brief, < 300 words.",
agent=writer,
context=[research], # writer sees the researcher's output
)
crew = Crew(agents=[researcher, writer], tasks=[research, write],
process=Process.sequential)
print(crew.kickoff(inputs={"topic": "vector databases"}))
This is a complete, runnable CrewAI program. It builds two agents (a researcher and a writer), gives each a task, bundles them into a crew, and runs it. Read it top to bottom — that order (agents → tasks → crew → run) is the shape of almost every CrewAI script.
LLM = "anthropic/claude-opus-4-8"just picks which model every agent will think with. CrewAI talks to Claude through this model string — nothing else to configure.- Each Agent is defined by three plain-English fields:
role(its job title),goal(what it's trying to achieve), andbackstory(its personality and standards). These aren't decoration — they steer the agent's behaviour, just like a system prompt from Chapter 2. - Each Task says what work to do (
description) and what a finished result should look like (expected_output), and names whichagentowns it. The{topic}in the description is a blank filled in later when you run the crew. - The key hand-off line is
context=[research]on the writer's task: it tells CrewAI to feed the researcher's output into the writer as its starting material. This is the arrow in the diagram, written in code. Crew(...)assembles the team andprocess=Process.sequentialsays "run the tasks in the order I listed them."crew.kickoff(inputs={...})starts the run and fills{topic}with"vector databases".
What the output means: It prints the crew's final result — here the writer's one-page markdown brief on vector databases, built from the facts the researcher gathered first.
Try this: Change the topic to something you know well, run it, and check whether the brief matches your expectations. Then tighten the writer's expected_output (say, "exactly 3 bullet points") and watch the output obey it.
expected_output is structured output in disguiseEvery task declares what "done" looks like. That's the same discipline as Chapter 2's schemas and L1's plan-and-execute steps — a concrete target makes each agent's result checkable and makes the hand-off to the next agent reliable. Vague expected_output is the top cause of a crew drifting.Lab M1.2 · Hierarchical (manager) process essential
Switch the process to hierarchical and CrewAI adds a manager agent that decides which worker handles what, in what order — the supervisor topology from L1, built in.
This is the other way to run a crew. Instead of a fixed left-to-right line, one extra agent — the Manager — sits on top and decides who does what, and in what order.
- The top box (Manager) is a boss agent. It reads the overall goal, breaks it into pieces, and hands each piece to a worker. You don't pre-wire the order — the manager decides it at run time.
- The three arrows going down are the manager delegating to the workers (Researcher, Writer, Fact-checker). When they finish, the manager gathers and combines their results.
- This is called a hierarchical process. It's more flexible than sequential — good when the right order isn't obvious in advance — but the manager becomes the single most important (and most expensive) agent, so it needs a strong model.
In short: Sequential = a fixed assembly line you designed. Hierarchical = a boss who figures out the plan on the fly. Use the boss only when deciding the order is itself the hard part.
hierarchical.pycrew = Crew(
agents=[researcher, writer, fact_checker],
tasks=[research, write, check],
process=Process.hierarchical,
manager_llm="anthropic/claude-opus-4-8", # the manager needs its own capable model
)
result = crew.kickoff(inputs={"topic": "GraphRAG"})
Here's the surprise: turning the sequential crew above into the manager-led version from the diagram is basically one line. You reuse the same agents and tasks and just change how the crew runs them.
agents=[researcher, writer, fact_checker]andtasks=[research, write, check]are the same building blocks as before — three workers and the jobs for them.process=Process.hierarchicalis the switch. Instead of running the tasks in a fixed order, CrewAI now adds a manager agent that plans and delegates (the top box in the diagram).manager_llm="anthropic/claude-opus-4-8"gives that manager its own model. The manager does the hard thinking — splitting up the work and combining results — so it should be a capable model even if the workers are cheaper ones.crew.kickoff(inputs={"topic": "GraphRAG"})runs it exactly like before; only the coordination style changed.
What the output means: result holds the crew's combined answer on "GraphRAG" — but this time the manager decided which worker ran when, rather than you fixing the order.
Try this: Run the same task both ways — sequential vs hierarchical — and compare the token usage. The manager adds extra LLM calls, so hierarchical usually costs more; it's worth it only when the routing is a genuine decision.
Giving agents tools intermediate
Agents are far more useful when they can act. CrewAI agents take a tools list — search, scraping, your own functions, or your L2/L3 retriever wrapped as a tool.
Requires: pip install crewai
tools.pyfrom crewai.tools import tool
@tool("Search internal docs")
def search_docs(query: str) -> str:
"""Search the internal knowledge base. Use for company-specific questions."""
return retriever.invoke(query) # your RAG retriever from L2 / Ch 3
researcher = Agent(role="Research Analyst", goal="...", backstory="...",
llm=LLM, tools=[search_docs])
Agents get much more useful when they can do things, not just talk. A tool is a normal Python function the agent is allowed to call — here, one that searches your internal docs. This snippet defines a tool and hands it to an agent.
- The
@tool("Search internal docs")line turns the function below it into a tool the agent can use. The text label and the docstring tell the agent when to reach for it — clear descriptions are what make the agent pick the right tool. - Inside, the function calls
retriever.invoke(query)— that's the RAG retriever from earlier chapters, wrapped so an agent can use it. The function takes aquerystring and returns text, which is all a tool needs to be. tools=[search_docs]on the Agent hands it that capability. Now, while working its task, the researcher can decide on its own to search the docs whenever it needs company-specific facts.
Try this: Think of a tool as a power you grant an agent. Add a second tool (say, a calculator function) to the same list and the agent can choose between them based on the descriptions you wrote — so write those descriptions carefully.
Crews vs Flows intermediate
A Crew is autonomous — agents figure out how to collaborate. When you need deterministic control over the sequence (branches, conditionals, precise state), CrewAI offers Flows, an event-driven, code-first orchestration layer. It's the same tension you met in L1 (agent vs workflow) and L4 (chain vs graph).
| Use a Crew when… | Use a Flow (or LangGraph) when… |
|---|---|
| Roles collaborate on an open-ended goal | You need exact, auditable control flow |
| You want quick, autonomous teamwork | Branching/looping logic must be explicit |
| The exact steps don't need to be fixed | Determinism & testability matter most |
The honest question: do you need a crew? intermediate
Multi-agent looks impressive and is often the wrong tool. Before splitting work across agents, apply this gate:
Common pitfalls advanced
| Pitfall | Fix |
|---|---|
Vague expected_output | State a concrete, checkable target per task |
Weak manager_llm in hierarchical mode | Give the manager a frontier model |
Forgetting context=[...] between tasks | Wire each task to the prior outputs it needs |
| Using a crew where one agent would do | Apply the "do you need a crew?" gate |
| Expecting determinism from an autonomous crew | Use Flows / LangGraph when control matters |
| Unbounded delegation loops | Cap iterations; monitor the manager's decisions |
Exercises advanced
Exercise M1.1 — Research → write → check
Context: A fact-checker that verifies the writer's claims against the research is the canonical three-role crew where context wiring matters.
Your task: Build a three-agent sequential crew — researcher (with a search tool), writer, and fact-checker — wiring context so each agent sees what it needs.
Requirements:
- Three agents: researcher (with a search tool), writer, fact-checker
- The fact-checker verifies the writer's claims against the research
- Wire each Task's context so it receives the right upstream output
- Run it end to end on a topic
💡 Hint: The fact-checker's context must include the research, not just the draft, so it can check claims against sources.
Exercise M1.2 — Flip to hierarchical
Context: Switching a fixed pipeline to a manager shows exactly when hierarchical is worth its extra token cost.
Your task: Take the three-agent crew and switch it to Process.hierarchical with a manager, then compare output and token cost to the sequential version.
Requirements:
- Reuse the same agents under a hierarchical process with a manager
- Run the same task
- Compare output quality and token cost to the sequential run
- State when the manager was worth it
💡 Hint: Hierarchical shines when task ordering isn't obvious up front; for a fixed research→write→check pipeline, sequential is usually cheaper and just as good.
Show what to look for
Hierarchical shines when task ordering isn't obvious up front; for a fixed research→write→check pipeline, sequential is cheaper and just as good. The manager earns its cost only when routing is a real decision.
Exercise M1.3 — One agent instead
Context: Sometimes one well-prompted agent beats a crew — and comparing them on quality, cost, and latency is the judgment the chapter is really after.
Your task: Rebuild the three-agent crew as a single agent (L3 create_react_agent) with the search tool and a "research, then write, then self-check" prompt, then compare to the crew.
Requirements:
- One agent with the search tool
- A prompt that folds research, writing, and self-checking into one flow
- Compare quality, cost, and latency to the crew
- Write two sentences on which you'd ship
💡 Hint: The single agent trades the crew's role separation for fewer calls and lower latency — whether that's worth it depends on how distinct the roles really are.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: CrewAI's Agent is a role-playing actor defined by a role, a goal, and a backstory — the same idea as a system prompt, scoped per team member.
Your task: Model an Agent as a dataclass and instantiate a researcher and a writer.
Requirements:
- An Agent holds role, goal, and backstory fields
- Instantiate at least two distinct agents (e.g. researcher, writer)
- The three fields steer that agent's behavior
- Print each agent's role and goal
💡 Hint: A plain dataclass captures it — the point is that an agent is configuration, not magic.
Show solution
An agent is configuration, not magic. Runnable stdlib:
from dataclasses import dataclass
@dataclass
class Agent:
role: str
goal: str
backstory: str
researcher = Agent("Senior Researcher",
"find and summarize the key facts",
"You dig up primary sources and cite them.")
writer = Agent("Tech Writer",
"turn findings into a clear paragraph",
"You write plainly for a general audience.")
for a in (researcher, writer):
print(f"{a.role}: {a.goal}")
The role/goal/backstory steer the LLM's behavior for that agent — the same idea as a system prompt, scoped per team member.
Context: In a sequential process each Task's output becomes the next Task's context — the assembly-line metaphor CrewAI is built around.
Your task: Model a sequential crew that runs tasks in order, threading each result forward.
Requirements:
- Run a list of tasks in order over a shared context
- Each task receives the prior task's output as input
- The final context is the last task's output
- Show a research → analyze → write pipeline
- Print the intermediate output of each stage
💡 Hint: Fold the tasks over a running context variable; each agent's output is the next agent's input.
Show solution
Sequential = a pipeline of tasks over a shared context. Runnable:
def run_sequential(tasks, context=""):
for task in tasks:
context = task(context) # each task sees the prior output
print(f"[{task.__name__}] -> {context}")
return context
def research(ctx): return "facts: X grew 30% in 2025"
def analyze(ctx): return ctx + " | trend: strong growth"
def write(ctx): return "Report: " + ctx
final = run_sequential([research, analyze, write])
print("FINAL:", final)
Each agent's task output is the next agent's input — the assembly-line metaphor CrewAI is built around.
Context: The hierarchical process adds a manager that delegates subtasks to workers — L1's supervisor topology in code, rather than a fixed pipeline.
Your task: Model a manager that routes each subtask to the right specialist and collects the results.
Requirements:
- A registry maps specialist names to worker functions
- The manager routes each subtask to the matching worker
- Collect the results keyed by subtask
- Handle a subtask with no matching worker gracefully
- Show a plan of subtasks being delegated and collected
💡 Hint: The manager is a router over specialists; it decides who handles each subtask instead of running a fixed sequence.
Show solution
The manager is a router over specialists. Runnable:
WORKERS = {
"research": lambda t: f"researched: {t}",
"code": lambda t: f"coded: {t}",
"write": lambda t: f"wrote: {t}",
}
def manager(subtasks):
# decide which worker handles each subtask, then collect
results = {}
for name, payload in subtasks:
if name not in WORKERS:
results[name] = "no worker for this skill"
else:
results[name] = WORKERS[name](payload)
return results
plan = [("research", "market size"), ("write", "exec summary")]
for k, v in manager(plan).items():
print(k, "->", v)
The hierarchical process is L1's supervisor topology in code: one coordinator delegates to specialists rather than a fixed pipeline.
Context: Agents can call tools — but a specialist should stay in its lane, so scoping tools to roles is the least-privilege version of L3's tool binding.
Your task: Model a worker agent with a tool registry and a per-role guard that only lets it use tools its role is allowed, then run a task needing one.
Requirements:
- A tool registry maps tool names to callables
- A per-role allow-list defines which tools each role may use
- A tool call by a disallowed role is denied
- An allowed call runs the tool
- Show an allowed call, a denied call, and a privileged role's allowed call
💡 Hint: Check the role's allow-list before dispatching to the tool; a denied call returns a refusal, not a result.
Show solution
Scope tools to roles so an agent can't overreach. Runnable:
TOOLS = {"search": lambda q: f"results for {q}",
"delete": lambda x: f"deleted {x}"}
ROLE_ALLOWED = {"Researcher": {"search"}, "Admin": {"search", "delete"}}
def use_tool(role, tool, arg):
if tool not in ROLE_ALLOWED.get(role, set()):
return f"DENIED: {role} may not use '{tool}'"
return TOOLS[tool](arg)
print(use_tool("Researcher", "search", "vLLM")) # allowed
print(use_tool("Researcher", "delete", "prod-db")) # denied
print(use_tool("Admin", "delete", "temp")) # allowed
Tools plus a per-role allow-list keep a specialist agent inside its lane — the least-privilege version of L3's tool binding.
Context: The honest question CrewAI's chapter forces: many tasks don't need a crew, because multi-agent adds coordination cost that must be justified.
Your task: Write a decision helper that recommends single-agent vs multi-agent from the task's shape.
Requirements:
- If one prompt would do, a crew is overkill
- Distinct specialties or parallelizable work justify a crew
- Otherwise a single agent suffices
- Take the deciding signals as parameters
- Show a case for each of the three verdicts
💡 Hint: A crew earns its complexity only for genuinely distinct roles or parallel work — otherwise one agent (or one prompt) is cheaper and more reliable.
Show solution
Multi-agent adds coordination cost — justify it. Runnable:
def need_crew(distinct_specialties, steps_parallelizable, one_prompt_would_do):
if one_prompt_would_do:
return "single agent (or just a prompt) -- a crew is overkill"
if distinct_specialties >= 2 or steps_parallelizable:
return "crew -- distinct roles or parallel work justify the coordination"
return "single agent -- one role, sequential steps"
print(need_crew(1, False, True)) # overkill
print(need_crew(3, True, False)) # crew justified
print(need_crew(1, False, False)) # single agent
A crew earns its complexity only when there are genuinely distinct roles or parallelizable work — otherwise one agent (or one prompt) is cheaper and more reliable.
Context: The production version is real CrewAI: two Agents, two Tasks, and a sequential Crew — the same pipeline you modeled offline, now with the framework.
Your task: Write the real CrewAI code using documented crewai APIs. (Needs the framework installed.)
Requirements:
- Define two
Agents with role/goal/backstory - Define two
Tasks with descriptions and expected outputs, each assigned to an agent - Thread output forward with
context=[...]on the second task - Assemble a
CrewwithProcess.sequentialand runkickoff - Note that
Process.hierarchicalwould add a manager; label it as needingcrewai+ a model key
💡 Hint: context=[research_task] threads output forward, exactly like the offline sequential crew — class names shift across versions, so verify against what you install.
Show solution
Correct CrewAI. Needs pip install crewai + a model API key:
from crewai import Agent, Task, Crew, Process
researcher = Agent(
role="Senior Researcher",
goal="Find the key facts about {topic}",
backstory="You dig up primary sources and cite them.",
)
writer = Agent(
role="Tech Writer",
goal="Write a clear one-paragraph summary",
backstory="You write plainly for a general audience.",
)
research_task = Task(
description="Research {topic} and list the top 3 facts.",
expected_output="A bulleted list of 3 cited facts.",
agent=researcher,
)
write_task = Task(
description="Turn the research into one clear paragraph.",
expected_output="A single paragraph.",
agent=writer,
context=[research_task], # write_task consumes research_task's output
)
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
process=Process.sequential, # each task feeds the next
)
result = crew.kickoff(inputs={"topic": "local LLM serving"})
print(result)
Same pipeline you modeled offline: context=[research_task] threads output forward, and Process.hierarchical would swap in a manager. Class names shift across versions — verify against what you install.
✓ Checkpoint — you can move on when you can…
- Define Agent, Task, Crew, and Process and how they fit together.
- Build a sequential crew and wire task context.
- Run a hierarchical crew and explain the manager's role.
- Give a crew agent a tool and describe it well.
- Decide between a crew, a Flow/LangGraph, and a single agent.
Knowledge check check yourself
In CrewAI, what is the difference between a sequential and a hierarchical process, and what extra cost does hierarchical add?
Show answer
Why is a task's expected_output called "structured output in disguise," and what does a vague one cause?
Show answer
expected_output is the top cause of a crew drifting off course.