You have a research task that needs searching the web, analyzing what was found, then writing a structured report. One LLM call can do a passable job at all three, but the output tends to be shallow. Each step deserves focused context, its own prompt, and the ability to use tools. That is what CrewAI is built for.
CrewAI lets you define agents with specific roles, give each one tools, chain their tasks together, and let them hand results to each other. You can run tasks in a fixed order (sequential) or let a manager agent delegate them based on who is best suited (hierarchical). This tutorial covers both.
Prerequisites
- Python 3.10 or higher (check with
python3 --version). CrewAI requires Python >=3.10 and <3.14. uvinstalled. If you do not have it:curl -LsSf https://astral.sh/uv/install.sh | sh- An OpenAI API key (or another LLM provider, see the CrewAI LLM docs for alternatives).
- A Serper.dev API key for web search. The free tier gives 2,500 queries, which is enough for testing.
Step 1: Install CrewAI
Install the CrewAI CLI as a global tool:
uv tool install crewai
Verify it landed:
uv tool list
# crewai v1.x.x
# - crewai
If you get a PATH warning, run uv tool update-shell and restart your terminal.
Step 2: Scaffold a new project
Create a crew project:
crewai create crew research-crew
cd research-crew
This generates a folder with src/research_crew/ containing config files, an agents directory, and tasks. The default project uses JSONC config files for agents and crew definitions, which is the current recommended structure.
Set your API keys in .env at the project root:
OPENAI_API_KEY=sk-your-key-here
SERPER_API_KEY=your-serper-key-here
Step 3: Define three agents
Open src/research_crew/config/agents.yaml. Replace the contents with three agents: a researcher, an analyst, and a writer.
researcher:
role: "Senior Research Analyst"
goal: "Find credible, recent information about {topic} using web search"
backstory: "You are a meticulous researcher. You cross-reference sources, prefer primary documentation over blog posts, and flag uncertainty rather than guessing."
tools: ["serper_dev_tool"]
verbose: true
analyst:
role: "Research Synthesizer"
goal: "Analyze the raw findings and identify key themes, contradictions, and gaps"
backstory: "You have spent years turning messy research notes into structured analysis. You care about what the evidence actually says, not what sounds compelling."
verbose: true
writer:
role: "Technical Report Writer"
goal: "Turn the analysis into a clear, well-structured markdown report"
backstory: "You write reports that engineers and product managers can act on. You use clear headings, avoid filler, and cite sources inline."
verbose: true
Each agent has a role (what it does), a goal (what it optimizes for), and a backstory (personality and working style). The tools field lists tool names that CrewAI resolves from its built-in toolkit. Only the researcher gets the Serper web search tool here, the other two work with text.
Step 4: Define the tasks
Open src/research_crew/config/tasks.yaml. Replace the contents:
research_task:
description: "Search the web for recent, credible information about {topic}. Focus on: (1) what the topic is and why it matters now, (2) key tools or projects, (3) notable practitioners or companies, (4) open debates or criticism. Use at least 5 different search queries to get coverage. Return raw findings with source URLs."
expected_output: "A list of findings with source URLs, organized by the four focus areas. 800-1200 words."
agent: researcher
analysis_task:
description: "Read the research findings and produce a structured analysis. Identify 3-5 key themes. Note where sources agree, where they contradict, and where evidence is thin. Suggest what the report should emphasize."
expected_output: "A structured analysis with 3-5 themes, each with a summary, supporting evidence, and confidence level. 500-800 words."
agent: analyst
writing_task:
description: "Write a markdown report based on the analysis. Structure: executive summary, key themes (one section each), implications, and a sources list. Keep paragraphs short. No marketing language."
expected_output: "A markdown report, 1500-2000 words, with clear headings, inline source citations, and a sources section at the end."
agent: writer
output_file: "output/report.md"
The context field is optional and lets one task see the output of another. In sequential mode, each task automatically gets the output of the previous task as context, so you do not need to set it explicitly. The output_file on the last task saves the final report to disk.
Step 5: Wire up the crew
Open src/research_crew/crew.py and replace the contents:
from crewai import Agent, Crew, Process, Task
from crewai_tools import SerperDevTool
from pydantic import BaseModel
import yaml
from pathlib import Path
# Load config
config_dir = Path(__file__).parent / "config"
def load_yaml(name):
with open(config_dir / f"{name}.yaml") as f:
return yaml.safe_load(f)
agents_config = load_yaml("agents")
tasks_config = load_yaml("tasks")
# Tool
serper = SerperDevTool()
# Build agents
agents = []
for key, cfg in agents_config.items():
tools = []
if "serper_dev_tool" in cfg.get("tools", []):
tools = [serper]
agent = Agent(
role=cfg["role"],
goal=cfg["goal"],
backstory=cfg["backstory"],
tools=tools,
verbose=cfg.get("verbose", False),
)
agents.append(agent)
agent_map = {key: agent for key, agent in zip(agents_config.keys(), agents)}
# Build tasks
tasks = []
for key, cfg in tasks_config.items():
task = Task(
description=cfg["description"],
expected_output=cfg["expected_output"],
agent=agent_map[cfg["agent"]],
output_file=cfg.get("output_file"),
)
tasks.append(task)
# The crew
research_crew = Crew(
agents=agents,
tasks=tasks,
process=Process.sequential,
verbose=True,
)
The key pieces: a Crew takes a list of agents, a list of tasks, and a process type. Process.sequential means tasks run in the order they appear in the list, and each task gets the previous one's output as context. The SerperDevTool comes from crewai_tools and wraps the Serper.dev search API.
Step 6: Run the crew
Kick off the crew with a topic:
crewai run
Or from Python directly:
from research_crew.crew import research_crew
result = research_crew.kickoff(inputs={"topic": "Model Context Protocol"})
print(result.raw)
You should see verbose logs showing each agent picking up its task, using tools (the researcher will show search queries), and passing output to the next agent. The final report lands in output/report.md.
Switching to hierarchical process
Sequential works well when the task order is fixed and obvious. When the order is less clear, or you want an agent to decide who handles what, use hierarchical process. This adds a manager agent that delegates tasks.
Change the crew definition:
from crewai import Agent, Crew, Process, Task
manager = Agent(
role="Research Project Manager",
goal="Coordinate the research crew to produce the best report for {topic}",
backstory="You are a project manager who delegates to the right specialist and keeps the work moving.",
allow_delegation=True,
verbose=True,
)
research_crew = Crew(
agents=agents,
tasks=tasks,
process=Process.hierarchical,
manager_llm="gpt-4o",
verbose=True,
)
result = research_crew.kickoff(inputs={"topic": "Model Context Protocol"})
In hierarchical mode, the manager agent receives the task list and decides execution order and delegation. Each agent can also delegate to other agents if allow_delegation=True is set on them. The manager_llm parameter specifies which model the manager uses for planning and delegation. You need to provide it because the manager is an auto-generated agent, not one you defined in YAML.
The trade-off: hierarchical gives more flexibility but costs more tokens and is less predictable. For a fixed pipeline like research-analyze-write, sequential is the better default. Hierarchical is worth it when tasks have dependencies that depend on intermediate results.
When to use CrewAI vs alternatives
CrewAI fits when you have a multi-step workflow where each step needs a different role or toolset, and the steps benefit from passing context to each other. The YAML-based config makes it easy to tweak agent behavior without touching code.
If you need fine-grained control over the conversation graph (conditional branching, loops), LangGraph is a better fit. CrewAI focuses on team-based workflows, LangGraph focuses on state machines. If you just need a single agent with tool calling, the OpenAI Assistants API or Claude Agent SDK are simpler.
Troubleshooting
"No module named 'crewai_tools'": Run uv add crewai-tools inside your project virtual environment.
Agent outputs are short or generic: Check your agent backstory and task expected_output. Vague prompts produce vague results. The expected_output field with specific word counts and structure is what constrains the agent.
Rate limit errors: Set max_rpm on agents or the crew. For OpenAI gpt-4o, 30 RPM is a safe default for a personal plan.
Hierarchical process does not delegate: Make sure manager_llm is set. Without it, the manager has no model to reason with.
References
- CrewAI Documentation: Introduction - overview of Flows and Crews architecture
- CrewAI Documentation: Agents - agent attributes and configuration
- CrewAI Documentation: Tasks - task attributes, sequential vs hierarchical execution