Getting Started with CrewAI Multi-Agent Systems
Building AI applications often means chaining prompts together manually, managing complex flows, and hoping the output from one LLM call makes sense as input for the next. CrewAI flips this paradigm on its head. Instead of one monolithic prompt pipeline, you assemble a team of specialized AI agents ā each with a distinct role, goal, and toolset ā who collaborate to accomplish complex tasks. Think of it as an AI-powered startup team inside your codebase.
What Exactly Is CrewAI?
CrewAI is an open-source Python framework for orchestrating role-playing, autonomous AI agents. It provides a structured way to define agents with specific personas, assign them discrete tasks, and then sequence those tasks into a workflow ā called a Crew. Agents can use tools (like search APIs, calculators, or custom functions), delegate work to one another, and produce structured outputs. The framework sits on top of LangChain, giving you access to virtually any LLM provider.
At its core, CrewAI gives you four primary building blocks:
- Agent ā A role-playing entity with a backstory, goals, tools, and an LLM backing its reasoning.
- Task ā A discrete unit of work assigned to an agent, with a description and expected output.
- Tool ā A function or API that agents can invoke to gather information or perform actions.
- Crew ā The orchestrator that binds agents and tasks together, managing execution order and inter-agent communication.
Why Multi-Agent Systems Matter
Single-agent LLM workflows hit a ceiling quickly. When you ask one model to research a topic, write a report, fact-check itself, and format everything nicely, you're essentially asking a single "generalist" to hold too much context and discipline at once. Multi-agent systems solve this by:
- Separation of concerns ā Each agent focuses on one domain (research, writing, reviewing), reducing hallucination and improving output quality.
- Specialized tool access ā A researcher agent gets search tools, a coder gets a REPL, a reviewer gets style guides ā no single agent carries all tools.
- Iterative refinement ā Agents can critique and improve each other's work, creating a natural feedback loop without human micromanagement.
- Parallel execution ā Multiple agents can work simultaneously on independent sub-tasks, dramatically reducing end-to-end latency.
- Transparency ā You can log and inspect exactly what each agent thought, decided, and produced, making debugging far easier than with monolithic prompts.
Building Your First AI Team: A Step-by-Step Guide
Step 1: Installation and Environment Setup
First, install CrewAI and its dependencies. You'll also need an API key for your LLM provider (OpenAI, Anthropic, Ollama for local models, etc.).
# Create a fresh virtual environment
python -m venv crew-env
source crew-env/bin/activate # On Windows: crew-env\Scripts\activate
# Install CrewAI with tools support
pip install crewai crewai-tools
# Set your API key (or use a .env file)
export OPENAI_API_KEY="sk-your-key-here"
For a cleaner setup, create a .env file in your project root:
OPENAI_API_KEY=sk-your-key-here
# Or for local models:
# OPENAI_API_BASE=http://localhost:11434/v1
# OPENAI_MODEL_NAME=llama3
Step 2: Define Your Agents
Each agent needs a role, a goal, and a backstory. The backstory is crucial ā it primes the LLM with contextual framing that shapes how the agent thinks and communicates. You can also attach tools and set behavioral flags like verbose (to see the agent's reasoning) and allow_delegation (to let it pass work to other agents).
from crewai import Agent
from crewai_tools import SerperDevTool, WebsiteSearchTool
# A researcher agent with search capabilities
researcher = Agent(
role="Senior Market Researcher",
goal="Uncover emerging trends and gather factual, up-to-date information",
backstory=(
"You are a seasoned market researcher with 15 years of experience "
"at top consulting firms. You're known for finding obscure but critical "
"data points and synthesizing them into clear, actionable insights. "
"You always cite your sources and flag any assumptions."
),
tools=[SerperDevTool(), WebsiteSearchTool()],
verbose=True,
allow_delegation=False,
llm="gpt-4o"
)
# A writer agent who takes research and crafts polished content
writer = Agent(
role="Technical Content Writer",
goal="Transform research findings into engaging, accurate blog posts",
backstory=(
"You are a bestselling technology author and former journalist. "
"Your writing is clear, compelling, and always backed by data. "
"You excel at explaining complex topics to a general audience "
"without sacrificing technical accuracy."
),
verbose=True,
allow_delegation=True,
llm="gpt-4o"
)
# A reviewer agent who checks facts and style
reviewer = Agent(
role="Content Reviewer and Fact-Checker",
goal="Ensure all published content meets quality standards and is factually correct",
backstory=(
"You are a meticulous editor with a background in academic publishing. "
"You catch inconsistencies, logical gaps, and stylistic issues that others miss. "
"You are not afraid to send work back for revision if it doesn't meet standards."
),
verbose=True,
allow_delegation=True,
llm="gpt-4o"
)
Step 3: Create Tasks with Clear Expected Outputs
Tasks bind work to specific agents. Each task has a description (the instruction) and an expected_output (a concrete deliverable description). You can also set the output_file parameter to save results to disk, and use the context parameter to pass outputs from previous tasks.
from crewai import Task
research_task = Task(
description=(
"Research the current state of quantum computing in 2025. "
"Focus on: major hardware breakthroughs, key players (IBM, Google, IonQ, etc.), "
"practical applications that are live today, and realistic timelines for widespread adoption. "
"Gather at least 5 credible sources and compile a structured research brief."
),
expected_output=(
"A detailed research brief (markdown format) with sections: "
"1) Executive Summary, 2) Hardware Landscape, 3) Key Players & Investments, "
"4) Live Applications, 5) Adoption Timeline, 6) Source List with URLs."
),
agent=researcher,
output_file="research_brief.md"
)
writing_task = Task(
description=(
"Using the research brief provided as context, write a compelling blog post "
"titled 'Quantum Computing in 2025: What's Actually Happening'. "
"Target audience: technically curious professionals who aren't quantum experts. "
"Keep it under 1200 words, use engaging analogies, and include concrete examples."
),
expected_output=(
"A polished blog post in markdown format, ready for publication, "
"with a clear headline, engaging introduction, structured body with subheadings, "
"and a thought-provoking conclusion."
),
agent=writer,
context=[research_task],
output_file="draft_blog_post.md"
)
review_task = Task(
description=(
"Review the blog post draft for factual accuracy, logical flow, and readability. "
"Cross-check claims against the original research brief. "
"If you find issues, suggest specific revisions. "
"If the draft passes review, append a 'Reviewed and Approved' stamp with your name."
),
expected_output=(
"A review report containing: 1) Overall assessment (Pass/Needs Revision), "
"2) Specific factual or stylistic issues found, 3) Recommended changes, "
"4) Final approved version of the post if it passes."
),
agent=reviewer,
context=[writing_task],
output_file="final_blog_post.md"
)
Step 4: Assemble and Kick Off the Crew
The Crew class ties everything together. You specify the agents, tasks, and a process type. Process.sequential runs tasks one after another in the order you define them. Process.hierarchical lets agents delegate and collaborate dynamically ā a manager agent (automatically created or specified) coordinates the work.
from crewai import Crew, Process
content_crew = Crew(
agents=[researcher, writer, reviewer],
tasks=[research_task, writing_task, review_task],
process=Process.sequential, # Tasks execute in order: research -> write -> review
verbose=True, # Set to 2 for even more detailed logs
memory=True # Agents remember previous interactions within the crew
)
# Kick off the crew and get results
result = content_crew.kickoff()
print("\n===== FINAL OUTPUT =====\n")
print(result)
print("\nCheck 'final_blog_post.md' for the approved version.")
A Complete, Run-Ready Example
Below is a full script you can copy, paste, and run. It creates a two-agent research-and-summarize team that investigates any topic you provide and returns a concise briefing. This example uses a free search tool and works with OpenAI or any compatible API.
# filename: research_crew.py
import os
from dotenv import load_dotenv
from crewai import Agent, Task, Crew, Process
from crewai_tools import SerperDevTool
load_dotenv()
# --- Configuration ---
# Set SERPER_API_KEY in your .env for the search tool
# Set OPENAI_API_KEY for the LLM
os.environ.get("SERPER_API_KEY") or os.environ.update({"SERPER_API_KEY": "your-serper-key"})
# --- Define Tools ---
search_tool = SerperDevTool(
country="us",
locale="en",
num_results=5
)
# --- Define Agents ---
research_analyst = Agent(
role="Research Analyst",
goal="Conduct thorough, accurate research on the given topic and produce a structured briefing",
backstory=(
"You work at a premier think tank. Your research briefings are known for their "
"depth, neutrality, and clarity. You always distinguish between established facts, "
"emerging evidence, and speculation. You cite your sources meticulously."
),
tools=[search_tool],
verbose=True,
allow_delegation=False,
llm="gpt-4o-mini" # Use a faster, cheaper model for research
)
synthesizer = Agent(
role="Chief Synthesizer",
goal="Distill complex research into a crisp executive summary with actionable takeaways",
backstory=(
"You are a former intelligence analyst who briefs C-level executives. "
"Your summaries are famously concise ā never exceeding 300 words ā yet they "
"capture every essential insight. You structure everything in the BLUF "
"(Bottom Line Up Front) military briefing format."
),
verbose=True,
allow_delegation=False,
llm="gpt-4o" # Use a more capable model for synthesis
)
# --- Dynamic Task Creator ---
def create_tasks_for_topic(topic: str):
"""Generate research and synthesis tasks for any given topic."""
research_task = Task(
description=(
f"Research the topic: '{topic}'. "
"Gather key facts, recent developments (within the last 12 months), "
"major competing viewpoints, and any relevant data or statistics. "
"Identify at least 3 credible sources for each major claim. "
"Organize findings into a structured research document."
),
expected_output=(
"A markdown research document with: "
"1) Topic Overview, 2) Key Facts & Figures, "
"3) Recent Developments (with dates), 4) Competing Perspectives, "
"5) Source Citations with URLs."
),
agent=research_analyst,
output_file="raw_research.md"
)
synthesis_task = Task(
description=(
"Take the research document and produce an executive briefing. "
"Use BLUF format: start with the single most important takeaway, "
"then provide 3-5 supporting bullet points, and end with a brief "
"outlook or recommendation. Keep total output under 300 words."
),
expected_output=(
"A concise executive briefing (max 300 words) in BLUF format: "
"Bottom Line, Supporting Points (bullets), Outlook/Recommendation."
),
agent=synthesizer,
context=[research_task],
output_file="executive_briefing.md"
)
return [research_task, synthesis_task]
# --- Main Execution ---
if __name__ == "__main__":
topic = input("What topic would you like researched? > ")
tasks = create_tasks_for_topic(topic)
crew = Crew(
agents=[research_analyst, synthesizer],
tasks=tasks,
process=Process.sequential,
verbose=True,
memory=True
)
print(f"\nš Researching: {topic}\n")
result = crew.kickoff()
print("\n" + "="*60)
print("FINAL BRIEFING:")
print("="*60)
print(result)
Understanding CrewAI's Execution Model
When you call crew.kickoff(), several things happen under the hood:
- Task Parsing ā Each task description is parsed, and the agent assigned to it receives the full prompt augmented with its backstory and goal.
- Tool Invocation ā If an agent has tools, it can invoke them mid-reasoning. The framework handles the tool call, feeds results back into the agent's context, and lets it continue reasoning.
- Context Passing ā Tasks with a
contextparameter receive the raw output of their dependency tasks. This output is injected into the task prompt, giving the agent full visibility into upstream work. - Memory ā When
memory=True, CrewAI maintains a short-term memory store (using embeddings by default) that agents can query. This allows agents to recall earlier decisions and findings across multiple steps. - Output Storage ā Results are saved to the specified
output_filepaths and also returned as a string fromkickoff().
Best Practices for Production-Grade Crews
1. Craft Detailed Backstories
The backstory is not window dressing ā it's the primary mechanism for shaping agent behavior. A vague backstory produces generic outputs. Be specific: mention the agent's experience level, industry, communication style, pet peeves, and standards. For example, instead of "You are a helpful assistant", write "You are a cybersecurity auditor with 10 years in fintech. You never approve a report without at least two concrete evidence references. You flag missing compliance citations immediately."
2. Use the Right Process for Your Workflow
- Process.sequential ā Best for linear pipelines (research ā draft ā review ā publish). Predictable, easy to debug.
- Process.hierarchical ā Best for complex, non-linear work where agents need to negotiate, delegate, or iterate. Requires a manager LLM call at each step, so it's more expensive and slower but handles ambiguity better.
3. Match LLM Models to Agent Roles
Not every agent needs GPT-4o. A research agent doing bulk web scraping can use a faster, cheaper model like GPT-4o-mini or Claude Haiku. Reserve expensive, reasoning-heavy models for synthesis, review, and decision-making agents. This cuts costs by 40-60% without sacrificing output quality.
4. Implement Proper Error Handling
CrewAI tasks can fail due to API errors, tool timeouts, or malformed outputs. Wrap your kickoff() calls in try-except blocks and implement retry logic for critical workflows:
import time
from crewai import Crew
def run_crew_with_retry(crew: Crew, max_retries=3):
for attempt in range(max_retries):
try:
result = crew.kickoff()
return result
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # Exponential backoff
else:
raise
return None
result = run_crew_with_retry(content_crew)
5. Structure Outputs for Downstream Consumption
Use the expected_output field to enforce output schemas. If you need JSON, specify it clearly in the expected output description. Better yet, use CrewAI's built-in output parsing with Pydantic models (available in recent versions):
from pydantic import BaseModel, Field
from typing import List
class ResearchOutput(BaseModel):
executive_summary: str = Field(description="A 100-word summary")
key_findings: List[str] = Field(description="3-5 bullet-point findings")
sources: List[str] = Field(description="List of source URLs")
confidence_score: float = Field(description="1-10 rating of information reliability")
# In your task definition:
research_task = Task(
description="...",
expected_output="A JSON object matching the ResearchOutput schema exactly.",
agent=researcher,
output_json=ResearchOutput # CrewAI handles parsing automatically
)
6. Leverage Async and Parallel Execution
For crews with independent tasks (e.g., researching three different topics simultaneously), use the asynchronous execution methods to run tasks in parallel:
import asyncio
from crewai import Crew, Task, Agent, Process
# Define three independent research agents and tasks
# ... (agent and task definitions)
crew = Crew(
agents=[agent_a, agent_b, agent_c],
tasks=[task_a, task_b, task_c],
process=Process.sequential # Even with sequential, you can kickoff async
)
# Run multiple crews in parallel
async def run_parallel_crews():
crew1 = Crew(agents=[agent_a], tasks=[task_a])
crew2 = Crew(agents=[agent_b], tasks=[task_b])
results = await asyncio.gather(
crew1.kickoff_async(),
crew2.kickoff_async()
)
return results
results = asyncio.run(run_parallel_crews())
7. Log and Monitor Agent Activity
Enable verbose logging (verbose=True on agents, verbose=2 on the crew) during development. For production, integrate with your existing monitoring stack by capturing crew callbacks:
from crewai import Crew
def log_step(step_info):
# Send to your logging system (Datadog, Sentry, etc.)
print(f"[LOG] Step completed: {step_info.get('task')} by {step_info.get('agent')}")
crew = Crew(
agents=[...],
tasks=[...],
step_callback=log_step # Called after each task completion
)
8. Keep Agent Scopes Narrow
A common beginner mistake is creating "god agents" that do everything. This defeats the purpose of a multi-agent system. Each agent should have exactly one clear responsibility. If you find yourself writing a backstory that covers multiple domains, split that agent into two or three specialized ones. The overhead of more agents is minimal, and the output quality improvement is significant.
Conclusion
CrewAI transforms how you build with LLMs. Instead of wrestling with ever-longer prompts and fragile chaining logic, you assemble purpose-built agents that collaborate like a skilled human team. The framework handles the orchestration, context management, and tool integration, freeing you to focus on designing the right team composition and workflows. Start with a simple two-agent crew ā researcher and writer ā and gradually add reviewers, fact-checkers, and domain specialists as your needs grow. With the best practices outlined above, you'll be shipping reliable, high-quality AI pipelines that are transparent, debuggable, and cost-effective. The multi-agent paradigm isn't just a pattern; it's the most natural way to harness the strengths of LLMs while mitigating their individual weaknesses. Build your first crew today and watch what a team of focused AI agents can accomplish together.