Read our operating thesis — Machines work better in tandem
JOURNALFRAMEWORKS · JULY 2026

Mastra vs LangGraph vs CrewAI

The three differ on one axis: what they force you to make explicit. A team, a state machine, or a TypeScript application. Plus the LangGraph resume behaviour that will duplicate your side effects if you haven't read the tests.

WRITTEN BYTandem Machines
Coordinated Intelligence
PUBLISHED
READING TIME16 minutes
SHARE
LinkedInX
KEY TAKEAWAYS
  • 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

These frameworks are not competing on features — their feature lists converge. They differ in what they make you make explicit. CrewAI makes the team explicit. LangGraph makes the state machine explicit. Mastra makes the application explicit. Everything else follows from that choice, including how hard it is to leave.

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 abstractionYou must declareAwkward when
CrewAIA team of agentsRoles, goals, backstories, tasks, a processThe work isn't naturally a division of labour
LangGraphA state machineA state schema, nodes, edges, transitionsControl flow is genuinely dynamic and unplannable
MastraA TypeScript applicationAgents, tools with Zod schemas, workflowsYour stack is Python
THE THIRD COLUMN IS THE ONE THAT PREDICTS WHETHER YOU'LL BE FIGHTING THE FRAMEWORK

“All three have memory, tools and observability. What separates them is what the abstraction forces you to name.”

CrewAI: the abstraction is social

CrewAI models work as a team. You define 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.
CrewAI: hierarchical process
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()
HIERARCHICAL REQUIRES A MANAGER — EITHER AN LLM OR A DESIGNATED AGENT

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.

CrewAI Flow: typed state, declarative order
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.summary

That 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

LangGraph models an agent as a 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.

WHAT THE GRAPH MODEL BUYS
01Durable executionCheckpointed state; runs resume after failure
02Human-in-the-loopinterrupt() suspends a run; Command(resume=…) continues it
03Thread scopingthread_id gives per-conversation state across invocations
04Time travelget_state_history() and replay from any earlier checkpoint
IF YOU NEED NONE OF THESE, THE EXPLICITNESS IS PURE COST

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

Mastra is a TypeScript framework for building AI agents and applications, running on Node 22.18+. Agents are instantiated from an 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

When a LangGraph run resumes after an interrupt, the node containing the interrupt re-executes from the beginning— the framework’s own tests assert the node’s call count increments on resume. Any side effect placed before the 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.

The trap
# ✗ 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 {}
REPLAY FROM AN EARLIER CHECKPOINT RE-FIRES THE INTERRUPT TOO — NODES BEFORE IT DO NOT RE-RUN

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

If your agent is one loop calling tools, a framework is overhead. That shape is better served by a harness that supplies the loop and built-in tools without imposing an orchestration model. Frameworks earn their cost when you have real multi-step orchestration, branching, or durability requirements — not when you have one loop.

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 shapeReach for
One loop, custom tools, you host itA harness — Agent SDK or an API tool runner
Divisible work with clear rolesCrewAI, ideally a Flow wrapping Crews
Must survive failure, pause for humans, replayLangGraph
Product is a TypeScript appMastra
Hosted, scheduled, no infrastructure of your ownNone of these — a managed agent platform

The cost of leaving

Exit cost tracks abstraction height. A graph of functions over an explicit state object is mostly ordinary code and ports with moderate effort. Role-and-backstory definitions carry behaviour inside the framework’s prompt construction, so migrating means reproducing behaviour rather than moving code.

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 yoursWhat is the framework's
LangGraphNode functions, state schema, tool implementationsScheduling, checkpoint format, interrupt plumbing
MastraTool implementations, Zod schemas, app codeAgent construction, workflow runtime, integrations
CrewAITool implementations, task descriptionsPrompt construction from roles and goals, delegation logic
HIGHER ABSTRACTION MEANS LESS CODE NOW AND MORE RECONSTRUCTION LATER

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

Answer three questions in order: does this need to survive failure and pause for humans, is the work naturally divisible into roles, and is the surrounding product TypeScript or Python? Those three resolve most selections without comparing a single feature.
THE THREE QUESTIONS, IN ORDER
01DurabilityMust runs resume and pause? If yes, LangGraph leads
02Shape of workDivisible into roles, or one continuous task?
03Host languageTypeScript product? Mastra removes a whole runtime
ANSWER 01 FIRST — IT IS THE ONLY ONE THAT IS EXPENSIVE TO GET WRONG

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

  1. 01Mastra documentation Mastra, 2026
  2. 02LangGraph LangChain, 2026
  3. 03CrewAI CrewAI Inc., 2026
Tandem Machines
Notes on coordinated intelligence — one essay a month
Subscribe
PREVIOUS

The voice agent stack: latency, turn-taking and barge-in