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 context parameter on Task.

  • Hierarchical: you must supply a manager_llm or a custom manager_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_ and and_ combine triggers.

  • @router() returns a label (typed with Literal[...]) 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_id in 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." Use PostgresSaver in production. SqliteSaver is for local development.

  • With PostgresSaver, keep thread_id values 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 own FlowPersistence backend.

  • 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 fresh state.id and 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 same thread_id, and that value becomes the return value of interrupt().

  • Typed answers: langgraph>=1.2.12 adds a response_schema that validates the resume value.

Three rules from the docs that will bite you if you skip them:

  1. 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.

  2. Don't wrap interrupt() in a bare try/except. It works by raising a special exception, and catching it breaks the pause.

  3. 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, not while 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_history keeps 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=True means "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

  • 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 bare LLM.call(...). The docs warn that flow.kickoff().token_usage only 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_before and interrupt_after as 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.

Keep Reading