If you're choosing between LangGraph and CrewAI, you aren't really picking a "better" framework. You're picking where the control flow lives. LangGraph has you draw the graph yourself. CrewAI starts from a team of role-based agents and gives you Flows when you need to pin the steps down. This guide compares the two on the things that decide production fit: how execution is ordered, how state is kept, how a human approves a step, and how memory works. Every claim links to the official docs.
TL;DR
| You need… | Lean toward |
|---|---|
| Explicit, inspectable control flow you define node by node | LangGraph |
| A quick "team of specialists" with roles and tasks | CrewAI (Crews) |
| Event-driven pipelines that chain crews and plain code | CrewAI (Flows) |
| Pause-for-approval that survives restarts | Both can do it. Read the persistence and HITL sections |
| Long-term memory that works out of the box | CrewAI's Memory is more batteries-included; LangGraph gives you a store primitive |
1. Execution model: who decides what runs next?
LangGraph. You build a StateGraph, add nodes (plain functions that read and return state), and connect them with edges and conditional edges. The LangGraph interrupts docs show the full pattern: builder.add_node(...), builder.add_edge(START, "agent"), builder.add_conditional_edges("agent", should_continue, [...]), then builder.compile(checkpointer=...). The classic agent loop is literally a cycle in your graph: an agent node calls the model, a conditional edge routes to a tool_node when the model asked for tools, and an edge leads back to agent. Nothing runs that you didn't wire up.
CrewAI Crews. A crew is a set of agents plus a list of tasks, and you choose a process. The CrewAI Processes docs define two:
- Sequential: tasks run in list order, and each task's output is context for the next. You can override that with the
contextparameter onTask. - Hierarchical: you must supply a
manager_llmor a custommanager_agent. Tasks aren't pre-assigned. The manager "allocates tasks to agents based on their capabilities, reviews outputs, and assesses task completion."
So with hierarchical crews, an LLM manager decides the order at runtime. That's handy for open-ended work, but it's less predictable than a graph you wrote yourself.
CrewAI Flows. For deterministic orchestration, CrewAI has Flows: "structured, event-driven workflows" built from decorated methods.
@start()marks entry points.@listen(method)runs a method when another one emits output.or_andand_combine triggers.@router()returns a label (typed withLiteral[...]) that picks the next branch.
The docs say Flows support "conditional logic, loops, and branching" and can call whole Crews, single Agents, or bare LLM calls from any step.
Takeaway: a LangGraph graph and a CrewAI Flow are much closer than their marketing suggests. Both give you explicit, code-defined control flow. The real difference is that CrewAI also offers the higher-level crew abstraction (roles plus processes), while LangGraph starts low-level.
2. State
LangGraph state is a typed schema (the docs use TypedDict or Pydantic) that every node reads and partially updates. You can attach reducers to fields. The interrupts docs annotate a list with operator.add so parallel nodes append to it instead of overwriting it.
CrewAI Flows give you both options. Unstructured state is a dict on self.state. Structured state is a Pydantic BaseModel passed as Flow[MyState]. Either way, each state automatically gets a UUID id (Flows docs).
Both get you typed, validated state if you want it. Use it. Untyped dict state is the first thing to rot in a growing agent.
3. Persistence and resuming
This is where production pain usually shows up.
LangGraph has two persistence layers (Persistence docs):
- Checkpointers save a thread's graph state as checkpoints. They're used for "conversation continuity, human-in-the-loop workflows, time travel, and fault tolerance," and you pick the thread with a
thread_idin the config. - Stores hold application-defined key-value data that lives across threads.
The docs give clear warnings:
InMemorySaver/MemorySaver"store checkpoints in RAM. When the process restarts, all checkpoints are lost." UsePostgresSaverin production.SqliteSaveris for local development.- With
PostgresSaver, keepthread_idvalues under 255 characters. - Checkpoints "accumulate" over long conversations. Prune them or set a retention policy.
CrewAI puts persistence on Flows with the @persist decorator, at class or method level (Flows docs).
- Default backend:
SQLiteFlowPersistence. You can write your ownFlowPersistencebackend. - Resume:
kickoff(inputs={"id": <uuid>})loads the latest snapshot and keeps writing under the same ID. - Fork:
kickoff(restore_from_state_id=<uuid>)hydrates a new run with a freshstate.idand leaves the original history alone. - Watch out: if the ID you pass doesn't match any persisted state, kickoff "falls back silently." Alert on that yourself, or a typo quietly starts a fresh run.
4. Human-in-the-loop
LangGraph: you call interrupt(payload) inside any node (Interrupts docs).
- What it needs: a checkpointer and a
thread_id. - How it pauses: the graph saves state and "waits indefinitely." You resume by invoking again with
Command(resume=value)on the samethread_id, and that value becomes the return value ofinterrupt(). - Typed answers:
langgraph>=1.2.12adds aresponse_schemathat validates the resume value.
Three rules from the docs that will bite you if you skip them:
- The node re-runs from the top on resume. Code before
interrupt()runs again, so side effects before it must be idempotent. Put them after the interrupt or in a separate node. - Don't wrap
interrupt()in a baretry/except. It works by raising a special exception, and catching it breaks the pause. - Keep interrupt order deterministic. Resume values are matched to interrupts by index, so skipping one conditionally or looping over changing data misaligns them. For validation loops, call
interrupt()once per node and loop with a conditional edge, notwhile True.
CrewAI: the @human_feedback decorator (CrewAI ≥ 1.8.0) pauses a Flow method to collect feedback (Flows docs).
- Routing on feedback: with
emit=["approved", "rejected", ...], an LLM reads the human's free-text feedback, "collapses" it into one of those outcomes, and triggers the matching@listen. - Default outcome: you can set a
default_outcome. - Feedback history:
self.human_feedback_historykeeps every answer. - Async channels: the docs point to custom providers (Slack, webhooks) for non-blocking feedback.
Design note: CrewAI's LLM step that turns feedback into an outcome is convenient, but it adds a model call to your approval path. For anything irreversible (payments, deletes, outbound email), consider a strict boolean outcome rather than free text parsed by an LLM. LangGraph's typed response_schema gets you that directly.
5. Memory
LangGraph keeps short-term memory in thread state through checkpointers and long-term memory in stores (Persistence docs). What you remember, when, and how you retrieve it is up to you.
CrewAI ships a unified Memory class (Memory docs) that replaces the older short-term, long-term, entity, and external memory types.
- On save: an LLM infers the scope, categories, and importance.
- On recall: results are ranked by a weighted score of semantic similarity, recency (exponential decay), and importance.
- In a crew:
memory=Truemeans "after each task, the crew automatically extracts discrete facts from the task output and stores them," and recalls relevant context before each task. - Defaults:
- embeddings: OpenAI
text-embedding-3-large - analysis LLM:
gpt-4o-mini - storage: LanceDB under
./.crewai/memory
- embeddings: OpenAI
- Privacy: the docs warn that memory content is sent to the configured LLM for analysis. Use a local model if that's a problem.
So CrewAI gives you a lot for free, including hidden LLM calls and an OpenAI default. LangGraph gives you primitives and leaves the policy to you. We cover this in depth in our AI agent memory guide.
6. Observability and cost
- CrewAI: Flows expose
flow.usage_metrics, which adds up tokens across every LLM call in a run, including nested crews and bareLLM.call(...). The docs warn thatflow.kickoff().token_usageonly covers the last crew, so it's the wrong number for cost tracking (Flows docs).flow.plot()renders an interactive HTML graph of the flow. - LangGraph: the docs point to LangSmith for inspecting traces and debugging interrupts, and to
interrupt_beforeandinterrupt_afteras static breakpoints you can use for debugging (Interrupts docs).
How to choose
Pick LangGraph if:
- you want every transition visible in code and testable node by node
- your approval steps need typed, validated input
- you're happy to own the memory policy
Pick CrewAI if:
- your problem naturally splits into roles and tasks
- you want working long-term memory with one flag
- you'd rather write event-driven Python methods than graph wiring
Use Flows as soon as you need determinism. Hierarchical crews let an LLM manager decide the order.
Either way, the production checklist is the same:
- a durable persistence backend (not in-memory)
- idempotent side effects around pause points
- explicit run IDs you can alert on
- token accounting at the workflow level, not just the last step
Agent Signal is a weekly newsletter for people building AI agents. Subscribe to get the next deep dive.