CrewAI provides a high-level, opinionated framework for building collaborative agent teams that mirror real organizational structures: each agent has a role, a goal, and a backstory that shapes its behavior. Unlike lower-level orchestration graphs, CrewAI handles delegation, memory passing, and task chaining automatically, letting you focus on defining who your agents are rather than how they communicate. In enterprise settings, this structured approach dramatically reduces boilerplate while still supporting sophisticated multi-agent workflows through its Flows abstraction.
CrewAI 0.80+ Architecture Overview
CrewAI organizes everything around four core primitives: Crews (teams), Agents (specialists), Tasks (units of work), and Flows (stateful pipelines). Understanding how they compose is essential before writing a single line of code.
flowchart TD
F["Flow (Stateful Pipeline)"]
C["Crew (Team Orchestrator)"]
M["Manager Agent\n(hierarchical only)"]
A1["Agent: Researcher"]
A2["Agent: Analyst"]
A3["Agent: Writer"]
T1["Task: Gather Data"]
T2["Task: Analyze Findings"]
T3["Task: Draft Report"]
TL["Tool: Web Search"]
TM["Tool: DB Query"]
F --> C
C --> M
M --> A1
M --> A2
M --> A3
A1 --> T1
A2 --> T2
A3 --> T3
T1 --> T2 --> T3
A1 --> TL
A2 --> TM
style F fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style C fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style M fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style A1 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style A2 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style A3 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style TL fill:#fff7ed,stroke:#f59e0b,color:#0F172A
style TM fill:#fff7ed,stroke:#f59e0b,color:#0F172A
Each Agent is configured with a role, goal, and backstory, these three strings are injected into the system prompt and meaningfully shape output style. Agents hold a reference to their LLM and can carry tools, memory, and delegation permissions. Tasks specify what needs doing (description), who does it (agent), and what the output format looks like (expected_output). The Crew binds agents and tasks together under a process strategy.
Sequential vs Hierarchical Process
The process parameter on a Crew is the single most impactful architectural decision you make.
flowchart LR
subgraph SEQ["Sequential Process"]
direction LR
S1["Task 1"] --> S2["Task 2"] --> S3["Task 3"]
end
subgraph HIE["Hierarchical Process"]
direction TB
MGR["Manager LLM"]
H1["Agent A"]
H2["Agent B"]
H3["Agent C"]
MGR -->|"delegates"| H1
MGR -->|"delegates"| H2
H1 -->|"result"| MGR
MGR -->|"re-assigns"| H3
end
style MGR fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style H1 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style H2 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style H3 fill:#f0fdf9,stroke:#0D9488,color:#0F172A
Sequential is the right default: predictable, debuggable, and cost-controlled. Each task receives the previous task’s output as context automatically, no wiring required. Hierarchical introduces a manager LLM that reads all agent capabilities and dynamically assigns work. Use hierarchical when you cannot predetermine the task sequence at design time, for example in open-ended research where the scope expands based on findings.
Here is how to define a sequential crew for an enterprise market analysis workflow:
from crewai import Agent, Task, Crew, Process
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
from langchain_openai import AzureChatOpenAI
# Use a shared Azure OpenAI instance so all agents go through the same
# enterprise deployment with unified rate limits and audit logging.
llm = AzureChatOpenAI(
azure_deployment="gpt-5.6-sol",
api_version="2024-05-01-preview",
temperature=0.1, # low temp for factual research tasks; higher for creative writing agents
)
web_search = SerperDevTool()
scraper = ScrapeWebsiteTool()
# The backstory is not cosmetic: it shapes system prompt context and
# meaningfully steers the agent's reasoning style and output format.
researcher = Agent(
role="Senior Market Researcher",
goal="Find accurate, recent data on the target market segment",
backstory=(
"You are a seasoned analyst at a top-tier consulting firm. "
"You cite sources, quantify claims, and flag data quality issues."
),
tools=[web_search, scraper],
llm=llm,
max_iter=5, # cap tool-use loops; default is 15 which can be expensive
allow_delegation=False, # researchers stay focused; delegation risks scope creep
memory=True, # enable short-term memory so context survives multi-tool calls
verbose=False, # disable per-step console output in production
)
analyst = Agent(
role="Strategic Analyst",
goal="Synthesize research into actionable competitive insights",
backstory=(
"You transform raw data into executive-ready strategic recommendations. "
"You identify patterns, risks, and market opportunities."
),
llm=llm,
max_iter=3,
allow_delegation=False,
memory=True,
)
# output_file triggers CrewAI to persist this task's result to disk,
# useful for audit trails in enterprise settings.
research_task = Task(
description=(
"Research the current state of {market_segment} focusing on: "
"key players, recent funding rounds, regulatory changes in the past 6 months."
),
expected_output="A structured report with bullet points, citations, and data tables",
agent=researcher,
output_file="research_output.md", # persist for downstream systems or auditors
)
analysis_task = Task(
description=(
"Using the research report, identify the top 3 strategic opportunities "
"for an enterprise SaaS company entering {market_segment}."
),
expected_output="Executive summary with SWOT analysis and prioritized recommendations",
agent=analyst,
context=[research_task], # explicitly declare dependency; CrewAI passes output as context
)
# Process.sequential guarantees deterministic execution order,
# which is critical for tasks with data dependencies.
crew = Crew(
agents=[researcher, analyst],
tasks=[research_task, analysis_task],
process=Process.sequential,
memory=True, # enables cross-task memory via embeddings storage
embedder={ # configure your own embedder to avoid sending data to OpenAI
"provider": "azure_openai",
"config": {"model": "text-embedding-3-small"},
},
)
result = crew.kickoff(inputs={"market_segment": "AI-powered legal tech"})
CrewAI Flows for Stateful Pipelines
Flows are CrewAI’s answer to long-running, stateful pipelines where you need conditional routing, loops, and typed intermediate state. A Flow is not a Crew replacement, it is an orchestrator that calls Crews or individual LLM calls as steps.
flowchart TD
START["@start: ingest_request"]
VALIDATE["validate_input"]
RESEARCH["run_research_crew"]
DECISION{"Confidence\n> 0.8?"}
ENRICH["enrich_with_web_data"]
DRAFT["draft_report_crew"]
REVIEW["human_review_gate"]
DONE["deliver_output"]
START --> VALIDATE --> RESEARCH --> DECISION
DECISION -->|"Yes"| DRAFT
DECISION -->|"No"| ENRICH --> RESEARCH
DRAFT --> REVIEW --> DONE
style START fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style DECISION fill:#fff7ed,stroke:#f59e0b,color:#0F172A
style DONE fill:#f0fdf9,stroke:#0D9488,color:#0F172A
Flows use Pydantic models for state, which gives you type safety, IDE autocomplete, and validation at every step. The @start decorator marks entry points, and @listen decorators declare event-driven transitions, CrewAI routes between steps automatically based on method return values.
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel, Field
from typing import Optional
import asyncio
class ResearchState(BaseModel):
"""Typed state shared across all Flow steps.
Using Pydantic here (not a plain dict) means every step's
inputs and outputs are validated: catching schema drift early
before bad data propagates downstream.
"""
market_segment: str = ""
raw_research: str = ""
confidence_score: float = 0.0
enriched_data: Optional[str] = None
final_report: str = ""
iteration_count: int = 0
class MarketResearchFlow(Flow[ResearchState]):
"""End-to-end market research pipeline with confidence gating.
Flow state persists automatically between steps, eliminating the need
to manually pass results between functions or store them in globals.
"""
@start()
def ingest_request(self) -> str:
# @start marks this as the Flow's entry point.
# Return value is passed to all @listen(ingest_request) methods.
print(f"Starting research for: {self.state.market_segment}")
return "ready"
@listen(ingest_request)
async def run_research_crew(self, trigger: str) -> str:
# Calling a Crew from within a Flow lets you reuse existing
# multi-agent logic without duplicating agent definitions.
result = await asyncio.to_thread(
research_crew.kickoff, # run sync crew in thread to avoid blocking event loop
inputs={"market_segment": self.state.market_segment}
)
self.state.raw_research = str(result.raw)
# Simulate confidence scoring: in production, run an LLM judge
# or extract structured data with Pydantic from the crew output.
self.state.confidence_score = 0.75
self.state.iteration_count += 1
return "research_complete"
@listen(run_research_crew)
async def check_confidence(self, signal: str) -> str:
# Conditional routing based on typed state: no if/else spaghetti,
# just clean method chaining. CrewAI re-fires the correct @listen.
if self.state.confidence_score >= 0.8:
return "high_confidence"
elif self.state.iteration_count < 3:
return "needs_enrichment"
else:
# Hard stop after 3 iterations prevents infinite enrichment loops,
# which would silently inflate LLM costs in production.
return "high_confidence"
@listen("needs_enrichment")
async def enrich_with_web_data(self, signal: str) -> str:
# Enrichment runs only on low-confidence passes, gated by iteration count.
# This is the CrewAI equivalent of a retry with backoff.
self.state.enriched_data = f"Supplemental data for {self.state.market_segment}"
self.state.confidence_score += 0.15 # update state; next step reads the new value
return "ready" # re-triggers run_research_crew via the listen chain
@listen("high_confidence")
async def draft_final_report(self, signal: str) -> None:
# Terminal step: returns None to signal end of flow.
self.state.final_report = f"Report: {self.state.raw_research[:200]}..."
print(f"Flow complete after {self.state.iteration_count} iteration(s)")
async def main():
flow = MarketResearchFlow()
flow.state.market_segment = "AI-powered legal tech"
await flow.kickoff_async()
return flow.state.final_report
Custom Tools with @tool
CrewAI tools extend agent capabilities with any Python function. The @tool decorator wraps a function so CrewAI knows how to describe it to the LLM and handle input/output marshaling automatically.
from crewai.tools import tool
import httpx
from pydantic import BaseModel
class CompanyLookupInput(BaseModel):
company_name: str
include_financials: bool = False
@tool("Company Database Lookup")
def lookup_company(company_name: str, include_financials: bool = False) -> str:
"""Look up company details from the internal enterprise CRM.
Args:
company_name: Legal name of the company to look up
include_financials: Whether to include revenue and funding data
Returns structured JSON string with company profile.
"""
# The docstring is NOT just documentation: CrewAI sends it to the LLM
# as the tool description. Precise, example-rich docstrings dramatically
# improve the agent's ability to call the tool correctly.
endpoint = "https://crm.internal/api/v2/companies/search"
# Using httpx (not requests) here because CrewAI runs in async contexts;
# requests would block the event loop during network I/O.
with httpx.Client(timeout=10.0) as client:
resp = client.get(
endpoint,
params={"q": company_name, "financials": include_financials},
headers={"Authorization": f"Bearer {_get_crm_token()}"},
)
resp.raise_for_status()
data = resp.json()
# Return a string, not a dict: LLMs consume text, and returning raw
# dicts can cause serialization issues in some CrewAI versions.
return (
f"Company: {data['name']}\n"
f"HQ: {data['headquarters']}\n"
f"Employees: {data['employee_count']}\n"
f"Founded: {data['founded_year']}\n"
+ (f"ARR: ${data.get('arr_usd', 'N/A')}" if include_financials else "")
)
def _get_crm_token() -> str:
# Centralize credential retrieval; never hardcode secrets in tool functions.
# In production, use a secrets manager (AWS Secrets Manager, Vault, etc.)
import os
return os.environ["CRM_API_TOKEN"]
# Attach the tool to any agent that needs company data
sales_intel_agent = Agent(
role="Sales Intelligence Specialist",
goal="Build comprehensive prospect profiles for enterprise sales",
backstory="You research target accounts with precision, surfacing intent signals and key contacts.",
tools=[lookup_company, web_search],
llm=llm,
)
When CrewAI Beats LangGraph
mindmap
root["Choose CrewAI when..."]
Team["Role-based teams are natural"]
SR["Structure mirrors org chart"]
DR["Delegation is desirable"]
Memory["Built-in memory matters"]
EM["Entity memory"]
SM["Short-term context"]
Speed["Speed of development wins"]
LP["Low boilerplate"]
HC["High configurability"]
Flows["Stateful pipelines needed"]
TP["Typed Pydantic state"]
ER["Event routing"]
CrewAI wins when your problem maps naturally to a team of specialists: a researcher, an analyst, a writer, a reviewer. Its built-in delegation, memory layers (short-term, long-term, entity, contextual), and sequential/hierarchical processes handle 80% of enterprise multi-agent patterns without custom graph wiring.
LangGraph wins when you need fine-grained control over every edge, conditional branching with complex logic, custom state reducers, or deep integration with LangSmith observability. If you find yourself fighting CrewAI’s abstractions to express your workflow, switch to LangGraph, the extra boilerplate is worth the control.
A practical heuristic: start with CrewAI for prototyping and team-like workflows; migrate to LangGraph when you hit a wall with delegation control, custom state management, or streaming requirements.
CrewAI 0.80+ provides a production-ready foundation for structured multi-agent collaboration, and its Flows abstraction closes the gap with lower-level orchestrators for complex pipelines. When your agents naturally map to organizational roles with clear responsibilities, CrewAI’s conventions will accelerate delivery and reduce the cognitive overhead of managing agent coordination manually.