cheat sheet
crewAI
Orchestrate teams of role-playing AI agents with crewAI. Covers agents, tasks, crews, tools, LLM selection, memory, YAML config, and the kickoff lifecycle.
crewAI — Role-Based Agent Crews
What it is
crewAI is a Python framework for orchestrating teams of AI agents that each play a defined role. Each Agent has a role, goal, and backstory that shapes its behaviour; each Task has a description, expected output, and an assigned agent; a Crew coordinates them in a sequential or hierarchical process. crewAI is designed for long-horizon, multi-step workflows where distinct expert perspectives improve the final output — research + writing + editing, planning + execution + QA, and similar patterns.
Install
pip install crewai crewai-tools
Output: (none — exits 0 on success)
Quick example
from crewai import Agent, Task, Crew, Process
from crewai_tools import SerperDevTool
import os
search_tool = SerperDevTool(api_key=os.environ["SERPER_API_KEY"])
researcher = Agent(
role="AI Researcher",
goal="Find accurate, up-to-date information on the given topic.",
backstory="You are an experienced researcher skilled at finding credible sources.",
tools=[search_tool],
verbose=True,
)
writer = Agent(
role="Technical Writer",
goal="Transform research findings into a clear, engaging summary.",
backstory="You write concise technical content for developer audiences.",
verbose=True,
)
research_task = Task(
description="Research the key capabilities of LlamaIndex as of 2026.",
expected_output="A structured list of LlamaIndex features with brief descriptions.",
agent=researcher,
)
write_task = Task(
description="Write a 200-word blog introduction based on the research findings.",
expected_output="A polished 200-word blog introduction paragraph.",
agent=writer,
)
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
print(result.raw)
When / why to use it
- Workflows where distinct roles produce better results than a single all-knowing agent: research → draft → edit → QA.
- Long multi-step tasks requiring coordination, handoffs, and specialised prompting per stage.
- When you want agent behaviour shaped by
role,goal, andbackstoryrather than raw system prompts. - Combining agents with built-in tools (web search, file read/write, code execution) without manual tool-loop coding.
- YAML-driven config for team definitions that non-engineers can adjust.
Common pitfalls
Vague
expected_output— ifexpected_outputis ambiguous (e.g. "a good answer"), agents produce inconsistent formats and handoffs fail. Be specific: "A JSON object with fields title, summary, sources."
Overlapping agent roles — two agents with near-identical
roleandgoalwill produce redundant or contradictory outputs. Give each agent a clearly differentiated responsibility.
Hierarchical process requires a manager LLM —
Process.hierarchicalinternally creates a manager agent that routes subtasks. Setmanager_llm=on theCrew; omitting it defaults to the first agent's LLM, which may be inappropriate.
Set
max_iter=5on agents to limit retries when the LLM keeps producing off-format output. The default is 25 — runaway agents are expensive.
Use
crew.kickoff_async()in asyncio environments or FastAPI handlers to run the crew without blocking the event loop.
Richer example — research, write, and QA pipeline
from crewai import Agent, Task, Crew, Process
import os
# Use Anthropic Claude
researcher = Agent(
role="Research Analyst",
goal="Gather comprehensive technical information on the assigned topic.",
backstory=(
"You are a meticulous research analyst who synthesises information "
"from multiple sources into structured, factual reports."
),
llm="claude-sonnet-4-6",
verbose=True,
max_iter=5,
)
writer = Agent(
role="Technical Writer",
goal="Produce clear, developer-friendly documentation from research input.",
backstory=(
"You are a senior technical writer who turns dense research into "
"readable guides with examples and callouts."
),
llm="claude-sonnet-4-6",
verbose=True,
)
qa = Agent(
role="Quality Reviewer",
goal="Ensure the written content is accurate, complete, and follows best practices.",
backstory=(
"You are a QA engineer who reviews technical documentation for "
"accuracy, missing edge cases, and clarity."
),
llm="claude-sonnet-4-6",
verbose=True,
)
task1 = Task(
description=(
"Research polars, the Python DataFrame library. Cover: "
"architecture, performance characteristics, and the lazy API."
),
expected_output=(
"A structured report with sections: Overview, Performance, LazyFrame API, "
"and 3 code examples."
),
agent=researcher,
)
task2 = Task(
description="Write a developer guide section on polars based on the research report.",
expected_output=(
"A 400-word guide section with an introduction, 2 code blocks with output, "
"and a common pitfalls callout."
),
agent=writer,
context=[task1], # receives task1's output as additional context
)
task3 = Task(
description=(
"Review the written guide for technical accuracy and completeness. "
"Identify any missing information or errors."
),
expected_output=(
"A review report listing: Approved items (bullet list), "
"Issues found (numbered list), and Suggested changes."
),
agent=qa,
context=[task2],
)
crew = Crew(
agents=[researcher, writer, qa],
tasks=[task1, task2, task3],
process=Process.sequential,
verbose=True,
memory=True, # shared long-term memory across the crew
)
result = crew.kickoff()
print(result.raw)
Agent configuration
from crewai import Agent
from crewai_tools import FileReadTool, CodeInterpreterTool
agent = Agent(
role="Data Scientist",
goal="Analyse datasets and produce insights with Python code.",
backstory="You are an expert data scientist who writes clean, well-commented analysis code.",
llm="gpt-4o", # or "claude-sonnet-4-6", "gemini/gemini-1.5-flash"
tools=[FileReadTool(), CodeInterpreterTool()],
verbose=True, # print agent reasoning to stdout
memory=True, # give agent access to crew memory
max_iter=10, # max self-correction attempts per task
max_rpm=20, # max LLM requests per minute (rate limit guard)
allow_delegation=False, # prevent agent from delegating to others
)
Task configuration
from crewai import Task
task = Task(
description=(
"Analyse the sales CSV at data/sales_2026.csv and produce "
"a monthly revenue summary with the top 3 products by revenue."
),
expected_output=(
"A markdown table with columns: Month, Total Revenue, Top Product. "
"Include a brief 2-sentence insight below the table."
),
agent=data_scientist,
context=[previous_task], # list of Task objects whose output feeds this task
output_file="results/summary.md", # write output to file
human_input=False, # set True to pause and ask for human approval
)
Crew configuration and process modes
from crewai import Crew, Process
# Sequential — tasks run in order; each receives previous task outputs
crew = Crew(
agents=[researcher, writer, qa],
tasks=[task1, task2, task3],
process=Process.sequential,
verbose=True,
memory=True,
max_rpm=30,
output_log_file="crew_log.txt",
)
# Hierarchical — a manager agent routes tasks dynamically
crew = Crew(
agents=[researcher, writer, qa],
tasks=[task1, task2, task3],
process=Process.hierarchical,
manager_llm="gpt-4o", # manager agent uses this LLM
verbose=True,
)
kickoff — running the crew
# Synchronous
result = crew.kickoff()
print(result.raw) # final output string
print(result.tasks_output) # list of TaskOutput objects
print(result.token_usage) # total token counts
# With inputs (interpolated into task descriptions via {variable})
result = crew.kickoff(inputs={"topic": "LangChain", "year": "2026"})
# Async (for FastAPI, asyncio)
import asyncio
result = asyncio.run(crew.kickoff_async())
# Batch — run the same crew for a list of inputs
inputs_list = [{"topic": "LangChain"}, {"topic": "LlamaIndex"}, {"topic": "AutoGen"}]
results = crew.kickoff_for_each(inputs=inputs_list)
Tools — built-in and custom
crewAI-tools provides ready-made tools; you can also create custom tools with @tool or BaseTool.
from crewai_tools import (
SerperDevTool, # Google search via Serper API
WebsiteSearchTool, # RAG over a specific URL
FileReadTool, # read local files
DirectoryReadTool, # read all files in a directory
CodeInterpreterTool, # execute Python code
CSVSearchTool, # semantic search over a CSV
PDFSearchTool, # semantic search over a PDF
)
# Custom tool with @tool decorator
from crewai.tools import tool
@tool("Database Query Tool")
def query_database(sql: str) -> str:
"""Execute a read-only SQL query and return results as a string."""
import sqlite3
conn = sqlite3.connect("app.db")
cursor = conn.execute(sql)
rows = cursor.fetchmany(20)
return "\n".join(str(row) for row in rows)
Memory and shared context
crewAI supports three types of memory: short-term (in-task context window), long-term (persisted across crew runs using a local ChromaDB store), and entity memory (extracts and remembers entities mentioned across tasks).
from crewai import Crew, Process
crew = Crew(
agents=[researcher, writer],
tasks=[task1, task2],
process=Process.sequential,
memory=True, # enables all memory types
embedder={
"provider": "openai",
"config": {"model": "text-embedding-3-small"},
},
)
YAML-driven configuration
For production systems, define agents and tasks in YAML files and load them with the @CrewBase decorator pattern.
# config/agents.yaml
researcher:
role: Research Analyst
goal: Find accurate technical information on {topic}.
backstory: You are a meticulous researcher skilled at synthesising complex sources.
writer:
role: Technical Writer
goal: Write clear developer documentation based on research.
backstory: You write concise technical guides for developer audiences.
# config/tasks.yaml
research_task:
description: Research {topic} and produce a structured technical report.
expected_output: A structured report with sections and 3 code examples.
agent: researcher
write_task:
description: Write a developer guide on {topic} based on the research.
expected_output: A 400-word guide with code blocks and a pitfalls callout.
agent: writer
context: [research_task]
from crewai import Agent, Task, Crew
from crewai.project import CrewBase, agent, task, crew
import yaml
@CrewBase
class ResearchCrew:
agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml"
@agent
def researcher(self) -> Agent:
return Agent(config=self.agents_config["researcher"], llm="claude-sonnet-4-6")
@agent
def writer(self) -> Agent:
return Agent(config=self.agents_config["writer"], llm="claude-sonnet-4-6")
@task
def research_task(self) -> Task:
return Task(config=self.tasks_config["research_task"])
@task
def write_task(self) -> Task:
return Task(config=self.tasks_config["write_task"])
@crew
def crew(self) -> Crew:
return Crew(agents=self.agents, tasks=self.tasks, process=Process.sequential, verbose=True)
result = ResearchCrew().crew().kickoff(inputs={"topic": "ChromaDB"})
print(result.raw)
Inspecting results
result = crew.kickoff()
# Final output
print(result.raw)
# Per-task output
for task_output in result.tasks_output:
print(f"Task: {task_output.name}")
print(f"Agent: {task_output.agent}")
print(f"Output: {task_output.raw[:200]}")
# Token usage
usage = result.token_usage
print(f"Total tokens: {usage.total_tokens}")
print(f"Prompt: {usage.prompt_tokens}, Completion: {usage.completion_tokens}")
Quick reference
| Task | Code |
|---|---|
| Create agent | Agent(role="...", goal="...", backstory="...", llm="...") |
| Create task | Task(description="...", expected_output="...", agent=agent) |
| Task context | Task(..., context=[upstream_task]) |
| Sequential crew | Crew(agents=[], tasks=[], process=Process.sequential) |
| Hierarchical crew | Crew(..., process=Process.hierarchical, manager_llm="gpt-4o") |
| Run crew | crew.kickoff() |
| Run with inputs | crew.kickoff(inputs={"key": "value"}) |
| Async run | await crew.kickoff_async() |
| Batch run | crew.kickoff_for_each(inputs=[{...}, {...}]) |
| Enable memory | Crew(..., memory=True) |
| Custom tool | @tool("Name") def fn(arg: str) -> str: |
| Output to file | Task(..., output_file="out.md") |
| Limit iterations | Agent(..., max_iter=5) |
| Rate limit guard | Agent(..., max_rpm=20) |