The role-first way to build multi-agent systems: give each agent a role, a goal, and a backstory, hand the team a list of tasks, and let a crew collaborate. Then graduate to Flows when you need deterministic control. This is the long version, from first install to production.
The mental model
CrewAI models a job as a small team. Each Agent is defined in natural language: a role, a goal, and a backstory that get injected straight into the system prompt and shape how the model behaves. You then write Tasks, each owned by one agent, and hand them to a Crew that runs them under a Process (sequential or hierarchical). The pitch is speed: you describe the roles and the work in prose, and the framework handles the collaboration loop, passing one task's output as context to the next.
That prose-first convenience is also the trade-off. A Crew decides a lot for you, so you own less of the control flow than you would in LangGraph. CrewAI answers this with Flows, a newer event-driven layer where you write plain Python methods wired by @start and @listen, thread explicit state, and call Crews only where fuzziness actually helps. Crews are the autonomous team; Flows are the deterministic conductor. Real systems use both.
flowchart TB
IN(["inputs: topic"]) --> C{{"Crew
process sequential"}}
C --> A1["Agent
Researcher"]
A1 --> T1["Task
research"]
T1 -->|"output as context"| T2["Task
write"]
T2 --> A2["Agent
Writer"]
A2 --> OUT(["CrewOutput"])
classDef model fill:#EEE6FF,stroke:#6D28D9,color:#09244B;
classDef code fill:#D6F5E3,stroke:#1B7F46,color:#09244B;
class A1,A2 model;
class C,T1,T2 code;
You hire a team by describing each person and the jobs to be done. The Crew is the manager that walks the jobs down the line, feeding each result into the next. You write less wiring than a graph, and in exchange you trust the framework with more of the flow.
Install & setup
CrewAI is Python-only (3.10 to 3.13 as of the 1.x line). Install the core, and add the tools extra for the prebuilt toolkit. CrewAI talks to models through LiteLLM, so a provider is just a string like anthropic/claude-sonnet-4-5 or openai/gpt-4o, and the matching env key is all it needs.
# core + the prebuilt tool library
pip install crewai "crewai[tools]"
# set the key for whichever provider your model string names
export ANTHROPIC_API_KEY="sk-ant-..."
# or OPENAI_API_KEY=... for openai/ models, plus SERPER_API_KEY for web search
CrewAI also ships a scaffolding CLI (crewai create crew my_project) that generates a project with YAML files for agents and tasks. This page teaches the pure-Python API instead, because seeing every object constructed in code is the fastest way to understand what the YAML is doing for you.
Hello world
The smallest useful CrewAI program is three objects: an Agent, a Task that agent owns, and a Crew that runs it. Curly-brace placeholders in the prose are filled from the inputs you pass to kickoff().
from crewai import Agent, Task, Crew
researcher = Agent(
role="Senior Researcher",
goal="Explain {topic} clearly and accurately",
backstory="You are a meticulous analyst who prizes primary sources.",
llm="anthropic/claude-sonnet-4-5", # LiteLLM-style model string
)
brief = Task(
description="Write a 5-bullet primer on {topic} for a busy engineer.",
expected_output="Exactly 5 concise bullet points, no preamble.",
agent=researcher,
)
crew = Crew(agents=[researcher], tasks=[brief])
result = crew.kickoff(inputs={"topic": "vector databases"})
print(result.raw) # the final text; result is a CrewOutput
role, goal, and backstory are not comments, they become the system prompt
The core · Agent
The three prose fields do the heavy lifting. role is who the agent is, goal is what it optimises for, and backstory is the context that colours its judgement. Beyond those, the arguments you reach for most are llm, tools, verbose, and allow_delegation. Keep allow_delegation=False unless an agent genuinely needs to hand work to a teammate, because delegation multiplies model calls fast.
from crewai import Agent, LLM
from crewai_tools import SerperDevTool
# the LLM class gives you temperature, max_tokens, etc.
llm = LLM(model="anthropic/claude-sonnet-4-5", temperature=0.2)
analyst = Agent(
role="Market Analyst",
goal="Surface hard numbers on {sector} demand",
backstory="Ex-equity researcher who never states a figure without a source.",
llm=llm,
tools=[SerperDevTool()], # web search; needs SERPER_API_KEY
allow_delegation=False, # do NOT let it spawn sub-tasks by default
max_iter=15, # cap the reason-act loop (default 20)
verbose=True, # print the agent's thinking to the console
)
role, goal, and backstory are your only real levers over behaviour, so write them like a system prompt, not a label. Vague personas produce vague, overlapping agents that step on each other. A sharp, narrow role is what keeps a multi-agent run coherent.
The core · Task
A Task is a unit of work owned by one agent. description is the instruction and expected_output is your acceptance test in prose: the more concrete it is, the more predictable the result. The most valuable production argument is output_pydantic, which forces the result into a typed model you can trust downstream instead of parsing free text. Use context to feed earlier task outputs into a later task explicitly.
from crewai import Task
from pydantic import BaseModel
class Finding(BaseModel):
headline: str
evidence: list[str]
confidence: float
research = Task(
description="Research {sector} demand for 2026 and cite each claim.",
expected_output="A headline, 3 to 5 evidence strings, and a 0-1 confidence.",
agent=analyst,
output_pydantic=Finding, # result.pydantic is a typed Finding
)
summary = Task(
description="Turn the research into a one-paragraph exec summary.",
expected_output="A single tight paragraph, no bullet points.",
agent=writer,
context=[research], # pass the research output in as context
output_file="summary.md", # also write it to disk
)
After a run, a task's result is on its output object: research.output.pydantic.headline gives you the typed field, and research.output.raw the plain text. In a sequential crew, each task's output is automatically threaded to the next even without context, but naming it explicitly makes the dependency readable.
The core · Crew & Process
A Crew binds agents and tasks together and executes them under a Process. There are two. Sequential runs tasks top to bottom, each output feeding the next; it is predictable and the right default. Hierarchical adds a manager that plans, delegates each task to the best agent, and reviews the results; you must supply a manager_llm or a custom manager_agent, and CrewAI creates the manager for you.
from crewai import Crew, Process
# sequential: a straight line, task1 -> task2 -> task3
crew = Crew(
agents=[analyst, writer],
tasks=[research, summary],
process=Process.sequential, # the default; explicit here for clarity
verbose=True,
)
# hierarchical: a manager delegates and reviews. tasks need no fixed agent.
managed = Crew(
agents=[analyst, writer],
tasks=[research, summary],
process=Process.hierarchical,
manager_llm="anthropic/claude-sonnet-4-5", # REQUIRED for hierarchical
)
result = crew.kickoff(inputs={"sector": "solar storage"})
print(result.raw) # final output
print(result.token_usage) # prompt/completion tokens for the whole run
flowchart TB
subgraph SEQ["Process sequential"]
direction LR
s1["task 1"] --> s2["task 2"] --> s3["task 3"]
end
subgraph HIER["Process hierarchical"]
direction TB
M{"Manager
plans and reviews"}
M -->|"delegate"| w1["Agent A"]
M -->|"delegate"| w2["Agent B"]
w1 -->|"result"| M
w2 -->|"result"| M
end
classDef model fill:#EEE6FF,stroke:#6D28D9,color:#09244B;
classDef code fill:#D6F5E3,stroke:#1B7F46,color:#09244B;
class s1,s2,s3 code;
class M,w1,w2 model;
To run the same crew over many inputs, use crew.kickoff_for_each(inputs=[{...}, {...}]), or the native async await crew.kickoff_async(...). Each call returns a fresh CrewOutput; a Crew is stateless by default and runs once to completion.
Tools
Tools are how agents touch the outside world. The crewai_tools package ships dozens ready to use, like SerperDevTool for web search, ScrapeWebsiteTool, FileReadTool, and PDFSearchTool. For your own logic, the fastest path is the @tool decorator: the docstring becomes the tool description the model reads.
from crewai.tools import tool
@tool("Order lookup")
def lookup_order(order_id: str) -> dict:
"""Look up an order status by its ID. Use for any order question."""
# validate + authorize here; the model only PROPOSES the call
return {"id": order_id, "status": "shipped"}
agent = Agent(role="Support Agent", goal="Resolve order questions",
backstory="Patient, precise, never guesses an order state.",
tools=[lookup_order])
When you need typed, validated arguments, subclass BaseTool and declare an args_schema. This is the production-grade shape: the schema rejects malformed calls before your code runs.
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Type
class RefundInput(BaseModel):
order_id: str = Field(..., description="The order to refund")
amount: float = Field(..., description="Amount in GBP, must be > 0")
class RefundTool(BaseTool):
name: str = "Issue refund"
description: str = "Refund an order. Only for verified, approved refunds."
args_schema: Type[BaseModel] = RefundInput
def _run(self, order_id: str, amount: float) -> str:
if amount <= 0:
return "Rejected: amount must be positive."
# side effect gated behind your own checks
return f"Refunded {amount} for {order_id}."
The model decides whether to call a tool and with what arguments. It does not decide whether that call is allowed. Validate arguments and check authorization inside _run for anything with a side effect: refunds, deletes, emails. Attaching a tool is not the same as approving every call it makes.
Memory
By default a Crew is stateless: each run starts blank. Pass memory=True and CrewAI adds three complementary layers. Short-term memory holds the current run's context via embeddings, so later tasks recall what earlier ones found. Long-term memory persists across runs in a local SQLite store, so a crew improves as it sees more work. Entity memory tracks the people, places, and things mentioned, so references stay consistent. Memory needs an embedder; it defaults to OpenAI, so set the key or point it elsewhere.
crew = Crew(
agents=[analyst, writer],
tasks=[research, summary],
memory=True, # short-term + long-term + entity
embedder={ # optional: override the default OpenAI embedder
"provider": "openai",
"config": {"model": "text-embedding-3-small"},
},
)
| Layer | Scope | Backing store |
|---|---|---|
| Short-term | within one kickoff | vector store (embeddings) |
| Long-term | across kickoffs | local SQLite |
| Entity | people, places, things | vector store (embeddings) |
CrewAI is consolidating these into a single unified Memory API in the 1.x line, but the memory=True switch remains the simplest way to enable recall, and it is what most projects still use.
Flows
A Crew is autonomous: you point it at a goal and it decides the moves. When you need to own the sequence, branch on results, or loop, reach for a Flow. A Flow is a plain Python class whose methods are wired by decorators. @start marks an entry point, @listen(x) runs a method when x finishes and receives its return value, and @router branches on a result. State lives on self.state, best typed with a Pydantic model, and it threads through every step. You call .kickoff() to run the whole thing.
from crewai.flow.flow import Flow, start, listen, router
from pydantic import BaseModel
class ReportState(BaseModel):
topic: str = ""
draft: str = ""
approved: bool = False
class ReportFlow(Flow[ReportState]):
@start()
def gather(self):
# a Crew can run INSIDE a flow step where fuzziness helps
self.state.draft = research_crew.kickoff(
inputs={"topic": self.state.topic}).raw
@router(gather)
def gate(self):
# deterministic branch: code owns the decision
return "ok" if len(self.state.draft) > 200 else "retry"
@listen("ok")
def publish(self):
self.state.approved = True
return f"Published: {self.state.draft[:60]}..."
flow = ReportFlow()
result = flow.kickoff(inputs={"topic": "tidal energy"})
Use or_(a, b) to fire when either upstream method finishes and and_(a, b) to wait for both. Because state is explicit and the wiring is code, a Flow is testable and debuggable in a way a bare Crew is not; it is CrewAI's answer to the control question that pushes many teams toward LangGraph.
flowchart TB ST(["kickoff"]) --> G["gather
runs a Crew"] G --> R{"router gate"} R -->|"ok"| P["publish"] R -->|"retry"| G P --> E(["done"]) classDef model fill:#EEE6FF,stroke:#6D28D9,color:#09244B; classDef code fill:#D6F5E3,stroke:#1B7F46,color:#09244B; class G model; class R,P code;
Crews vs Flows
This is the single most important distinction in CrewAI, and the one beginners miss. A Crew hands the model the wheel; a Flow keeps it in code and calls a Crew only where open-ended reasoning earns its cost. They compose: a Flow step can kick off a Crew, and that is the recommended production shape.
| Dimension | Crew | Flow |
|---|---|---|
| Who drives | the agents decide | your code decides |
| Control flow | implicit, sequential or managed | explicit via @start / @listen / @router |
| State | stateless by default | explicit self.state, typed |
| Branching & loops | limited | first-class |
| Best for | open-ended collaboration | reliable pipelines with fuzzy steps |
| Cost profile | harder to predict | you gate every model call |
Most tasks want a workflow, not a crew of agents. CrewAI makes multi-agent cheap to try, which is exactly why you must justify reaching for it. Start with a Flow you own, drop a Crew into the one step that genuinely needs autonomous collaboration, and prove it beats a single agent before you ship.
Putting it together
Two agents with sharp, non-overlapping roles. A researcher gathers cited facts into a typed Finding; a writer turns that into a short brief. The research output is threaded to the writer as context, memory is on so entities stay consistent, and the whole thing runs from a single kickoff.
from crewai import Agent, Task, Crew, Process, LLM
from crewai_tools import SerperDevTool
from pydantic import BaseModel
llm = LLM(model="anthropic/claude-sonnet-4-5", temperature=0.3)
class Finding(BaseModel):
headline: str
evidence: list[str]
researcher = Agent(
role="Senior Researcher",
goal="Find cited, current facts about {topic}",
backstory="You never state a claim without a source and a date.",
llm=llm, tools=[SerperDevTool()], allow_delegation=False,
)
writer = Agent(
role="Technical Writer",
goal="Turn findings into a crisp brief for an engineer",
backstory="You write tight, jargon-light prose and cut every spare word.",
llm=llm, allow_delegation=False,
)
research = Task(
description="Research {topic}. Cite each claim with a source.",
expected_output="A headline and 3 to 5 evidence strings.",
agent=researcher,
output_pydantic=Finding,
)
write = Task(
description="Write a 150-word brief on {topic} from the research.",
expected_output="One tight paragraph, no headings.",
agent=writer,
context=[research], # feed the typed findings in
output_file="brief.md",
)
crew = Crew(
agents=[researcher, writer],
tasks=[research, write],
process=Process.sequential,
memory=True,
verbose=True,
)
result = crew.kickoff(inputs={"topic": "on-device LLM inference"})
print(result.raw) # the final brief
print(research.output.pydantic.headline) # typed access to step 1
print(result.token_usage) # cost accounting for the run
two roles, one line each of real judgement, and the framework runs the relay
Production
output_pydantic on any task another system consumes, so you validate instead of parse.result.token_usage after every run, cap the loop with max_iter and max_rpm, and keep allow_delegation=False unless a role truly needs it._run before anything touches money or data you cannot un-send.crewai==1.15.* and read the release notes before you bump.Gotchas
Two agents with fuzzy, similar roles argue, repeat each other, and burn tokens. The fix is not more agents, it is sharper prose: give each role a narrow remit and a distinct goal, or collapse them into one agent. Add a second agent only when the split is real.
Process.hierarchical without manager_llm or manager_agent raises. And the manager itself makes extra calls to plan and review, so hierarchical is materially more expensive than sequential. Default to sequential and reach for hierarchical only when dynamic delegation earns its cost.
memory=True defaults to the OpenAI embedder. If OPENAI_API_KEY is unset, memory fails or degrades even when your agents run on Anthropic. Set the key, or configure a local embedder like Ollama in the embedder block.
expected_output is your acceptance test. Leave it hand-wavy and the model fills the gap however it likes. Spell out the format, the length, and the shape; that one field buys most of the reliability you will get from a task.
Reach for it when
Skip it when
The principle it teaches
The code / model boundary should be explicit. A Crew hides it: the agents own the flow. A Flow makes it visible again: your code drives, and a Crew acts only inside the step you allow it to. CrewAI teaches the lesson by contrast, that autonomy is a cost you opt into per step, not a default you accept for the whole system.
A Week 4 tool, and a useful cautionary contrast back in Week 1. When you sketch a multi-agent architecture, CrewAI is the fast way to try it; put it next to LangGraph, which you met in Week 1 as the control-first alternative. CrewAI optimises for how quickly a team stands up, LangGraph for how much of the flow you own. The course asks you to prove the crew earns its extra cost against a single agent before it ships.