CrewAI vs LangGraph

Multi-agent orchestration vs stateful workflows

Overview

Multi-agent orchestration vs stateful workflows

Verdict

Multi-agent orchestration vs stateful workflows

Details

CrewAI vs LangGraph

Overview

A detailed comparison of two popular AI orchestration frameworks: CrewAI (by CrewAI Inc.) and LangGraph (by LangChain). Both enable building sophisticated AI agent systems, but with fundamentally different approaches to orchestration.

Quick Comparison Table

FeatureCrewAILangGraph
MaintainerCrewAI Inc.LangChain
ParadigmRole-based multi-agent teamsStateful workflow graphs
Agent DefinitionYAML/Python with roles, goals, backstoriesTypedDict state + node functions
Process TypesSequential, Hierarchical, ConsensualCustom graph with any topology
State ManagementTask output chainingExplicit TypedDict state
Human-in-the-LoopVia callbacks and custom logicBuilt-in interrupt/resume
PersistenceLimited (via custom implementation)Built-in checkpointers
StreamingLimitedNative streaming support
Multi-AgentRole-based delegationGraph nodes as specialized functions
Learning CurveLow (declarative)Moderate (graph concepts)
Best ForContent creation, research workflowsComplex pipelines, production systems

Philosophy

CrewAI

"Multi-agent orchestration for humans"

CrewAI treats AI systems as teams of specialists:

  • Agents have roles, goals, and backstories
  • Tasks are assigned based on capabilities
  • Process manages execution order
  • Focus on natural language workflows
from crewai import Agent, Task, Crew, Process

researcher = Agent(
    role='Senior Research Analyst',
    goal='Uncover cutting-edge developments in AI',
    backstory='You are a senior research analyst...',
)

writer = Agent(
    role='Technical Content Writer',
    goal='Create compelling content about AI',
    backstory='You are a technical writer...',
)

research_task = Task(
    description='Research AI agent frameworks',
    expected_output='A detailed research report',
    agent=researcher,
)

write_task = Task(
    description='Write an article based on research',
    expected_output='A well-structured article',
    agent=writer,
    context=[research_task],
)

crew = Crew(
    agents=[researcher, writer],
    tasks=[research_task, write_task],
    process=Process.sequential,
)

result = crew.kickoff()

LangGraph

"Stateful, multi-actor applications with LLMs"

LangGraph treats AI systems as computational graphs:

  • Nodes represent actions (LLM calls, tools, etc.)
  • Edges define control flow
  • State flows through the graph
  • Explicit, deterministic execution
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
from langchain_core.messages import BaseMessage

class AgentState(TypedDict):
    messages: List[BaseMessage]
    research: dict
    draft: str
    iteration: int

def research_node(state: AgentState) -> dict:
    # Perform research
    return {"research": results, "iteration": state["iteration"] + 1}

def write_node(state: AgentState) -> dict:
    # Write based on research
    return {"draft": article}

builder = StateGraph(AgentState)
builder.add_node("research", research_node)
builder.add_node("write", write_node)
builder.add_edge("research", "write")
builder.set_entry_point("research")
builder.add_edge("write", END)

workflow = builder.compile()
result = workflow.invoke({"messages": [], "iteration": 0})

Agent Definition

CrewAI

Declarative agent definition:

# agents.yaml
researcher:
  role: >
    Senior Research Analyst
  goal: >
    Uncover cutting-edge developments in {topic}
  backstory: >
    You're a senior research analyst with deep expertise in {topic}.
    You have a knack for identifying emerging trends...
  verbose: true
  allow_delegation: false

# Load and create
with open("config/agents.yaml") as f:
    agents_config = yaml.safe_load(f)

researcher = Agent(
    config=agents_config["researcher"],
    llm=ChatOpenAI(model="gpt-4o"),
    tools=[search_tool],
    verbose=True,
)

Features:

  • YAML configuration
  • Role-based identity
  • Goal-driven behavior
  • Backstory for personality
  • Delegation support

LangGraph

Programmatic node definition:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o")

async def research_node(state: AgentState) -> dict:
    prompt = ChatPromptTemplate.from_messages([
        ("system", "You are a research assistant."),
        ("human", "Research: {topic}"),
    ])
    chain = prompt | llm
    response = await chain.ainvoke({"topic": state["topic"]})
    return {"research": {"content": response.content}}

Features:

  • Full Python control
  • Type-safe state
  • Custom logic per node
  • Dependency injection
  • Async support

Process Management

CrewAI

Three built-in process types:

# Sequential - tasks run in order
crew = Crew(
    agents=[researcher, writer, editor],
    tasks=[research_task, writing_task, editing_task],
    process=Process.sequential,
)

# Hierarchical - manager agent delegates
crew = Crew(
    agents=[researcher, writer, editor, manager],
    tasks=[research_task, writing_task, editing_task],
    process=Process.hierarchical,
    manager_llm=ChatOpenAI(model="gpt-4o"),
)

# Consensual - all agents contribute
crew = Crew(
    agents=[agent1, agent2, agent3],
    tasks=[task1, task2, task3],
    process=Process.consensual,
)

LangGraph

Custom graph topology:

from langgraph.graph import StateGraph, START, END
from typing import Literal

def should_continue(state: AgentState) -> Literal["write", "finalize"]:
    if state["iteration"] >= 3:
        return "finalize"
    return "write"

builder = StateGraph(AgentState)
builder.add_node("research", research_node)
builder.add_node("write", write_node)
builder.add_node("finalize", finalize_node)

builder.add_edge(START, "research")
builder.add_edge("research", "write")
builder.add_conditional_edges("write", should_continue, {
    "write": "write",
    "finalize": "finalize",
})
builder.add_edge("finalize", END)

workflow = builder.compile()

Features:

  • Any graph topology
  • Conditional edges
  • Parallel branches
  • Loops and cycles
  • Dynamic routing

State Management

CrewAI

Task output chaining:

research_task = Task(
    description='Research AI frameworks',
    expected_output='A detailed research report',
    agent=researcher,
    output_file='outputs/research.md',
)

writing_task = Task(
    description='Write article based on research',
    expected_output='A well-structured article',
    agent=writer,
    context=[research_task],  # Access research output
    output_file='outputs/article.md',
)

editing_task = Task(
    description='Edit article for quality',
    expected_output='A polished, publication-ready article',
    agent=editor,
    context=[writing_task],  # Access draft
    output_file='outputs/final.md',
)

LangGraph

Explicit TypedDict state:

from typing import TypedDict, Annotated, List
from langgraph.graph import add_messages

class AgentState(TypedDict):
    messages: Annotated[List[BaseMessage], add_messages]
    topic: str
    research: dict
    draft: str
    iteration: int

def research_node(state: AgentState) -> dict:
    # Read from state
    topic = state["topic"]
    
    # Write to state
    return {
        "research": results,
        "iteration": state["iteration"] + 1,
    }

def write_node(state: AgentState) -> dict:
    # Access research from state
    research = state["research"]
    return {"draft": article}

Human-in-the-Loop

CrewAI

Via callbacks and custom logic:

def approval_callback(output: str, task: Task):
    """Custom approval callback."""
    print(f"Task output: {output}")
    # Implement approval logic
    return output

task = Task(
    description='Write article',
    expected_output='An article',
    agent=writer,
    callback=approval_callback,
)

# Or use hierarchical process with manager review
crew = Crew(
    agents=[researcher, writer, editor, manager],
    tasks=[research_task, writing_task, editing_task],
    process=Process.hierarchical,
    manager_llm=ChatOpenAI(model="gpt-4o"),
)

LangGraph

Built-in interrupt and resume:

from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
workflow = builder.compile(checkpointer=checkpointer)

# Interrupt before a node
builder.interrupt_before("review")

# Run workflow
config = {"configurable": {"thread_id": "session-1"}}
workflow.invoke(input_data, config=config)

# Check state
snapshot = workflow.get_state(config)
if snapshot.next:
    # Human needs to review
    print("Waiting for human approval...")
    
    # Resume after approval
    workflow.invoke(None, config=config)

# Time travel
history = workflow.get_state_history(config)
for checkpoint in history:
    print(f"Checkpoint: {checkpoint.values}")

Persistence

CrewAI

Limited built-in support:

# Custom persistence via output files
task = Task(
    output_file='outputs/research.md',
)

# Or custom implementation
import json
from crewai import Crew

class PersistentCrew(Crew):
    def save_state(self, path: str):
        state = {
            "agents": [a.role for a in self.agents],
            "tasks": [t.description for t in self.tasks],
        }
        with open(path, 'w') as f:
            json.dump(state, f)
    
    def load_state(self, path: str):
        with open(path) as f:
            state = json.load(f)
        # Reconstruct crew

LangGraph

Built-in checkpointers:

# In-memory (development)
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()

# SQLite (persistent)
from langgraph.checkpoint.sqlite import SqliteSaver
checkpointer = SqliteSaver.from_conn_string("checkpoints.db")

# PostgreSQL (production)
from langgraph.checkpoint.postgres import PostgresSaver
checkpointer = PostgresSaver(conn)

workflow = builder.compile(checkpointer=checkpointer)

# Persistence is automatic
config = {"configurable": {"thread_id": "session-1"}}
workflow.invoke(input_data, config=config)

# State persists across runs
snapshot = workflow.get_state(config)

Streaming

CrewAI

Limited streaming support:

# Stream via verbose output
crew = Crew(
    agents=[researcher, writer],
    tasks=[research_task, writing_task],
    verbose=True,
)

result = crew.kickoff()
# Verbose output shows progress

LangGraph

Native streaming support:

# Stream updates
async for event in workflow.astream(input_data, config=config, stream_mode="updates"):
    for node_name, node_output in event.items():
        print(f"[{node_name}] {node_output}")
        yield node_output

# Stream state
async for state in workflow.astream(input_data, config=config, stream_mode="values"):
    print(f"Step: {state['iteration']}")
    yield state

# Stream custom
async for event in workflow.astream(input_data, config=config, stream_mode="custom"):
    yield event

Multi-Agent Patterns

CrewAI

Role-based team:

# Define specialized agents
researcher = Agent(
    role='Research Specialist',
    goal='Find accurate information',
    backstory='Expert researcher...',
)

writer = Agent(
    role='Content Writer',
    goal='Create engaging content',
    backstory='Professional writer...',
)

editor = Agent(
    role='Content Editor',
    goal='Ensure quality and accuracy',
    backstory='Meticulous editor...',
)

seo_agent = Agent(
    role='SEO Specialist',
    goal='Optimize for search',
    backstory='SEO expert...',
)

# Assemble crew
crew = Crew(
    agents=[researcher, writer, editor, seo_agent],
    tasks=[research_task, writing_task, editing_task, seo_task],
    process=Process.sequential,
    memory=True,  # Enable agent memory
    cache=True,   # Enable caching
)

LangGraph

Specialized nodes:

# Define node functions
async def researcher_node(state: AgentState) -> dict:
    """Research step."""
    results = await perform_research(state["topic"])
    return {"research": results}

async def writer_node(state: AgentState) -> dict:
    """Writing step."""
    draft = await write_content(state["research"])
    return {"draft": draft}

async def editor_node(state: AgentState) -> dict:
    """Editing step."""
    edited = await edit_content(state["draft"])
    return {"draft": edited}

async def seo_node(state: AgentState) -> dict:
    """SEO optimization."""
    optimized = await optimize_seo(state["draft"])
    return {"draft": optimized}

# Build graph
builder = StateGraph(AgentState)
builder.add_node("research", researcher_node)
builder.add_node("write", writer_node)
builder.add_node("edit", editor_node)
builder.add_node("seo", seo_node)

builder.add_edge("research", "write")
builder.add_edge("write", "edit")
builder.add_edge("edit", "seo")

workflow = builder.compile()

Tool Integration

CrewAI

Built-in and custom tools:

from crewai_tools import tool
from langchain_community.tools import DuckDuckGoSearchRun
from crewai_tools import LangChainTool

# Built-in tool
search_tool = LangChainTool(
    tool=DuckDuckGoSearchRun(),
    name="DuckDuckGo Search",
    description="Search the web for information",
)

# Custom tool
@tool
def fetch_webpage(url: str) -> str:
    """Fetch and return the content of a webpage."""
    import requests
    response = requests.get(url)
    return response.text

# Add to agent
researcher = Agent(
    config=agents_config["researcher"],
    tools=[search_tool, fetch_webpage],
)

LangGraph

LangChain tools:

from langchain.tools import tool
from langchain_community.tools import DuckDuckGoSearchRun

@tool
def fetch_webpage(url: str) -> str:
    """Fetch and return the content of a webpage."""
    import requests
    response = requests.get(url)
    return response.text

# Add to agent
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent

tools = [fetch_webpage, DuckDuckGoSearchRun()]
agent = create_tool_calling_agent(llm, tools, prompt)

Error Handling

CrewAI

Callback-based error handling:

def error_callback(error: Exception, task: Task):
    print(f"Task {task.description} failed: {error}")
    # Implement retry logic or fallback

research_task = Task(
    config=tasks_config["research_task"],
    agent=researcher,
    callback=error_callback,
)

# Or use try-except
try:
    result = crew.kickoff()
except Exception as e:
    print(f"Crew failed: {e}")

LangGraph

Node-level error handling:

from langgraph.errors import NodeInterrupt

async def resilient_node(state: AgentState) -> dict:
    max_retries = 3
    for attempt in range(max_retries):
        try:
            result = await perform_action()
            return {"result": result}
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            await asyncio.sleep(2 ** attempt)

# Or use edges for error routing
def on_error(error: Exception) -> str:
    return "error_handler"

builder.add_conditional_edges("node", should_handle, {
    "success": "next_node",
    "error": "error_handler",
})

Best Use Cases

Choose CrewAI When:

  • Content creation workflows: Blog posts, articles, reports
  • Research pipelines: Multi-step research processes
  • Role-based teams: Clear agent roles and responsibilities
  • Rapid prototyping: Quick setup with YAML configuration
  • Non-technical users: Declarative, easy to understand
  • Sequential processes: Linear task execution

Choose LangGraph When:

  • Complex workflows: Non-linear, conditional execution
  • Production systems: Persistence, streaming, error handling
  • State management: Explicit state tracking and debugging
  • Human-in-the-loop: Interrupts, approvals, time travel
  • Custom topologies: Any graph structure needed
  • Testing: Deterministic, testable workflows

Performance Comparison

MetricCrewAILangGraph
Setup timeFast (declarative)Moderate (programmatic)
Execution speedSimilar (depends on LLM)Similar (depends on LLM)
Memory usageLowLow-Moderate
ScalabilityGood for linear workflowsExcellent for complex workflows
DebuggingVerbose outputState inspection, tracing

Migration Guide

From CrewAI to LangGraph

# CrewAI
researcher = Agent(role='Researcher', ...)
writer = Agent(role='Writer', ...)
crew = Crew(agents=[researcher, writer], tasks=[r_task, w_task])
result = crew.kickoff()

# LangGraph
class State(TypedDict):
    research: dict
    draft: str

builder = StateGraph(State)
builder.add_node("research", research_node)
builder.add_node("write", write_node)
builder.add_edge("research", "write")
workflow = builder.compile()
result = workflow.invoke({"research": {}, "draft": ""})

From LangGraph to CrewAI

# LangGraph
builder = StateGraph(State)
builder.add_node("research", research_node)
workflow = builder.compile()
result = workflow.invoke(input)

# CrewAI
researcher = Agent(role='Researcher', ...)
research_task = Task(description='Research...', agent=researcher)
crew = Crew(agents=[researcher], tasks=[research_task])
result = crew.kickoff()

Conclusion

Both frameworks are excellent for building AI agent systems. The choice depends on your needs:

  • CrewAI excels at role-based multi-agent teams with declarative configuration, ideal for content creation and research workflows.
  • LangGraph excels at complex stateful workflows with explicit control flow, ideal for production systems requiring persistence and human-in-the-loop.

For rapid prototyping and content workflows, I recommend CrewAI. For production systems requiring reliability and control, LangGraph is the better choice.

Resources


Last updated: May 2026