- 01The three differ on what they force you to declare: a team, a state machine, or a TypeScript application.
- 02CrewAI's abstraction is social — roles, goals, backstories, and a sequential or hierarchical process.
- 03LangGraph's abstraction is a graph over explicit state, with checkpointing, interrupts and replay.
- 04Mastra's abstraction is your app — TypeScript-native, with real web-framework integration.
- 05On resume after a LangGraph interrupt, the interrupting node re-executes. Side effects before the interrupt fire twice.
Method.This is drawn from each project’s current documentation and source, not from a benchmark — we have not built the same agent three times and timed it. Version-specific behaviour changes fast; the primary sources are listed at the end. What is durable is the axis these frameworks actually differ on, which is not the one the comparison posts use.
The one axis that matters
Framework comparisons usually tabulate memory, RAG, tool calling and observability, conclude that all three have all of them, and leave you no better informed. The useful question is what the abstraction obliges you to name — because that determines what is easy, what is awkward, and what you cannot express at all.
| Core abstraction | You must declare | Awkward when | |
|---|---|---|---|
| CrewAI | A team of agents | Roles, goals, backstories, tasks, a process | The work isn't naturally a division of labour |
| LangGraph | A state machine | A state schema, nodes, edges, transitions | Control flow is genuinely dynamic and unplannable |
| Mastra | A TypeScript application | Agents, tools with Zod schemas, workflows | Your stack is Python |
“All three have memory, tools and observability. What separates them is what the abstraction forces you to name.”
CrewAI: the abstraction is social
Agents with a role, goal and backstory, Tasks with a description and an expected output, and a Crew that runs them under a Process — either sequential, where each task’s output feeds the next, or hierarchical, where a manager delegates and validates.from crewai import Crew, Process
crew = Crew(
agents=my_agents,
tasks=my_tasks,
process=Process.hierarchical,
manager_llm="gpt-4o", # or manager_agent=my_manager_agent
)
result = crew.kickoff()The role-and-backstory design is often dismissed as theatre. It is not: it is a prompt-engineering convention with a schema, and for genuinely divisible work — research, then draft, then edit — it produces good results with very little code. The cost is that a meaningful amount of behaviour lives in prose fields interpreted by the framework, which makes that behaviour hard to test and hard to port.
Flows are the part worth knowing about
CrewAI also ships Flow, a decorator-driven orchestration layer that is considerably more precise than a Crew — and it changes the assessment. A Flow[State] has a typed Pydantic state, a @start() entry point, and @listen(...) steps that fire on completion of a prior step. Crucially, a step can be a direct LLM call, a single agent, or a whole Crew.
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
class ResearchState(BaseModel):
topic: str = ""
raw_research: str = ""
summary: str = ""
class ResearchFlow(Flow[ResearchState]):
@start()
def research_topic(self):
result = llm.call(f"Research the topic: {self.state.topic}")
self.state.raw_research = result
return result
@listen(research_topic)
def write_summary(self, research_output):
# A step can be an LLM call, one agent, or an entire Crew.
self.state.summary = str(summarizer.kickoff(self.state.raw_research))
return self.state.summaryThat composition — deterministic Flow on the outside, agentic Crew on the inside — is the shape most production CrewAI should probably take, and it is a much better answer than an all-agentic crew for anything with compliance or cost constraints. It is also less discussed than the role-playing crews the framework is known for.
LangGraph: the abstraction is a state machine
StateGraph: nodes that transform an explicit state schema, edges that define transitions, and a checkpointer that persists state so runs survive restarts, pause for humans, and replay from history. It is the lowest-level of the three and the one that treats durability as a first-class concern.The trade is verbosity for control. You declare the state, the nodes and the wiring, and in exchange you get properties the others do not offer.
Durability is tunable rather than binary. Recent versions expose three modes — 'sync' persists before the next step starts, 'async' persists while the next step executes, and 'exit' persists only when the graph exits. That is a real latency-versus-recoverability dial, and picking exit for a long agentic run because it looked faster is a decision to lose the run on a crash.
The human-in-the-loop primitive is the strongest reason to choose LangGraph, and it is genuinely well built: a node calls interrupt(), the invocation returns with an __interrupt__ marker, and the caller resumes by invoking with Command(resume=...). The prebuilt HumanInterrupt type even carries an action request with per-request flags for whether the human may accept, edit, respond or ignore — which is the shape an approval UI actually needs.
Mastra: the abstraction is your application
Agent class with id, name, instructions and model; tools are created with createTool() taking an inputSchema validated by Zod and an execute() function.Its distinguishing property is not an agent concept at all — it is that it treats the surrounding application as the primary context. Documented integrations cover Next.js, React, Astro, Express, SvelteKit and Hono, plus a Mastra Studio UI for interactive development.
For a team whose product is already a TypeScript web application, that removes an entire category of friction: no second language runtime, no Python service to deploy alongside your Node app, shared types between your agent tools and your API layer, one deployment story. For a Python team it is simply irrelevant, and no feature comparison changes that.
- The honest selection rule for Mastra
- Choose it because your stack is TypeScript and you want the agent to live inside the application, not because of a capability line item. That is its actual advantage, and it is a good one.
One caveat on this section: Mastra’s documentation is thinner on workflow internals than the other two, so if durable multi-step orchestration is a hard requirement, verify the current workflow semantics directly rather than assuming parity with LangGraph’s checkpointing.
The resume trap
interrupt() call in that node therefore happens twice.This is the most consequential thing in this article, because it is a correctness trap that reads as a feature. The mental model people bring — the run pauses at the interrupt and continues from that line — is wrong, and the failure is silent.
# ✗ WRONG: the write happens twice.
def ask_human(state):
db.audit.insert(...) # runs on the first pass...
answer = interrupt("Approve?") # ...run suspends here...
return {"value": [answer]} # ...and on resume the WHOLE node re-runs,
# so the insert fires a second time.
# ✓ RIGHT: nothing before the interrupt but the interrupt.
def ask_human(state):
answer = interrupt("Approve?")
return {"answer": answer}
def record_decision(state): # a separate node, after the resume
db.audit.insert(...)
return {}The replay semantics compound it. Replaying from a checkpoint taken before the interrupt re-fires the interrupt and re-executes that node again, while nodes before the checkpoint and after the interrupt do not re-run. So the same node can execute three times across one logical approval, and the count is visible in the library’s tests rather than in a warning.
The rule that follows: an interrupting node should contain nothing but the interrupt. Side effects go in a node that runs after the resume, or they are made idempotent — which is the same discipline as everywhere else an agent can retry itself. See idempotency for agent actions for keying that survives a re-execution.
This is not a criticism of LangGraph. Re-execution is the correct implementation of resumable execution over checkpoints — the alternative would require suspending a Python stack frame. It is a criticism of how rarely it is stated, given that the natural way to write the node is the broken way.
When to use none of them
The loop is not the hard part and it is not where any differentiation lives. If that is all you need, take one and move on — the Agent SDK is one such harness, and the API tool runners are another.
| Your shape | Reach for |
|---|---|
| One loop, custom tools, you host it | A harness — Agent SDK or an API tool runner |
| Divisible work with clear roles | CrewAI, ideally a Flow wrapping Crews |
| Must survive failure, pause for humans, replay | LangGraph |
| Product is a TypeScript app | Mastra |
| Hosted, scheduled, no infrastructure of your own | None of these — a managed agent platform |
The cost of leaving
Worth weighing at selection time, because this category is young and you will likely make this decision again. The question to ask of any framework: if this project were abandoned tomorrow, what would I have to rewrite?
| What is yours | What is the framework's | |
|---|---|---|
| LangGraph | Node functions, state schema, tool implementations | Scheduling, checkpoint format, interrupt plumbing |
| Mastra | Tool implementations, Zod schemas, app code | Agent construction, workflow runtime, integrations |
| CrewAI | Tool implementations, task descriptions | Prompt construction from roles and goals, delegation logic |
In every case the tool implementations are yours, which is the practical argument for keeping tools in plain functions behind a thin adapter rather than defining them with framework decorators. That is a small discipline at the start and the difference between a port and a rewrite later.
Choosing
Durability comes first because it is the requirement you cannot retrofit cheaply. Adding roles to a graph is an afternoon; adding checkpointed resumable execution to something that never had it is a rewrite.
And whichever you pick, the layers the framework does not cover are still yours: what the agent is permitted to do without a human, and the record of what it did. No framework on this list supplies either — see the seven-layer stack for where they sit and why they are the ones that decide whether a pilot reaches production.
Frequently asked questions
What is the difference between Mastra, LangGraph and CrewAI?
They differ in what they make you declare. CrewAI models a team — agents with roles, goals and backstories, executing tasks under a sequential or hierarchical process. LangGraph models a state machine — a StateGraph of nodes and edges over an explicit state schema, with checkpointed persistence. Mastra models a TypeScript application that contains agents, with first-class integration into Node web frameworks.
Is Mastra a good choice for agents?
It fits well if your product is already a TypeScript application. Mastra is a TypeScript framework built on Node with agents instantiated from an Agent class, tools created with createTool() and Zod input schemas, and documented integrations for Next.js, React, Astro, Express, SvelteKit and Hono. If your stack is Python, that advantage disappears.
When should I use LangGraph?
When execution has to survive failure and pause for humans. LangGraph provides checkpointed persistence with selectable durability, interrupts that suspend a run and resume via a Command, thread-scoped state, and history you can replay from an earlier checkpoint. If you need none of that, its explicitness is overhead.
Does a LangGraph node re-execute after a human-in-the-loop interrupt?
Yes. When a run resumes after an interrupt, the node containing the interrupt call runs again from the start — the framework's own tests assert the call count increments. Any side effect placed before the interrupt in that node therefore happens twice, so side effects must be idempotent or moved into a node that runs after the resume.
Do I need an agent framework at all?
Not if your agent is a single loop calling tools. That shape is served better by an agent harness, which supplies the loop and built-in tools without imposing an orchestration model. Frameworks earn their cost when you have genuine multi-step orchestration, branching, or durability requirements.
Which agent framework is easiest to migrate away from?
Generally the one whose abstractions are closest to plain code. A graph of functions over an explicit state object maps onto other runtimes with moderate effort. Role-and-backstory abstractions carry prompt behaviour inside the framework, so leaving means reconstructing behaviour rather than moving code.
Sources
- 01Mastra documentation — Mastra, 2026
- 02LangGraph — LangChain, 2026
- 03CrewAI — CrewAI Inc., 2026