---
title: "Best AI agent frameworks for production: frameworks vs durable infrastructure"
description: "A comparison of 7 agent frameworks and Trigger.dev as durable execution infrastructure: orchestration, durable execution, state, retries, observability, human approval, TypeScript and Python support, deployment, self-hosting and pricing."
date: 2026-10-09
---

Most comparisons of agent frameworks look at prompts, tools and handoffs. Those matter. But there's a second question that decides whether an agent survives production: what happens to a run when the process dies halfway through a tool call, when a redeploy lands mid-run, or when a human takes 3 days to approve step 4.

That's a different job, and this comparison splits it out.

## TL;DR

- **Short verdict.** If you're a TypeScript team building an agent that waits on a human or runs longer than a request, write the agent with Mastra, the Vercel AI SDK or `@openai/agents` and run it on Trigger.dev: you deploy from the CLI, any wait over 60 seconds releases the machine, and the free plan includes $5 of compute a month. In Python, LangGraph has the most complete open-source durability design of the frameworks here (checkpointer, per-node retries, interrupts), with LlamaIndex Workflows close behind and Google ADK catching up fast in 2.0. In .NET or on Azure, Microsoft Agent Framework is the natural choice.
- **Agent frameworks** (LangGraph, CrewAI, OpenAI Agents SDK, Microsoft Agent Framework, Google ADK, LlamaIndex Workflows, Mastra) define what an agent does: prompts, tools, routing, handoffs between agents.
- **Durable execution infrastructure** (Trigger.dev is one) decides what happens to a run when the process crashes, a step hangs, or a human takes days to respond.
- Trigger.dev is the execution layer. It runs your TypeScript (including any of the TypeScript frameworks above) on long-running machines with retries, queues, checkpointed waits and human approval tokens. It won't help you write a prompt, and it has no Python SDK.
- Everything below was checked against official docs on October 9, 2026, with version numbers. Where a feature is only in a paid platform, or still marked experimental, the table says so.

## Two jobs: deciding and surviving

An agent framework governs **deciding**. Which tool to call, which sub-agent gets the task next, when to stop. LangGraph does that with a graph. CrewAI does it with roles. The OpenAI Agents SDK does it with handoffs that look like tool calls to the model.

Durable execution governs **surviving**. A run is 40 minutes in when the worker gets OOM-killed. A redeploy rolls the fleet while 200 runs are mid-flight. A tool call to a flaky API hangs for 10 minutes. An approval sits in someone's inbox over a weekend. In each case, something has to remember where the run got to, decide whether to retry, and make sure the retry doesn't send the same email twice.

The two layers blur, which is why these comparisons get muddled. LangGraph's checkpointer is real durability. Mastra's suspend and resume is real durability. To their credit, each framework also documents exactly where its durability stops: Mastra's docs say an in-process crash during a step can leave a run stuck in `running`. ADK's docs say tools run "at least once, and may run more than once" on resume. LangGraph's docs say a node restarts from its beginning after an interrupt, so anything before the interrupt had better be idempotent.

None of that is a flaw in the frameworks. It's the shape of the problem, and the same caveats apply to Trigger.dev (see its section). Pick the framework for how you want to write the agent, then decide separately how its runs should survive failure.

## How this comparison was built

Every claim below comes from official documentation, official GitHub repositories or official pricing pages, checked on October 9, 2026. No blog posts, no vendor comparison pages (including ours), no news coverage.

Each table cell is labelled one of:

- **Native**: in the open-source package.
- **Integration**: the package plugs into something else you run or pay for (named).
- **Paid platform**: only in the vendor's hosted or licensed product (named).
- **External**: you supply it yourself.
- **Partial**: exists with a documented caveat that matters in production.

Versions checked: LangGraph 1.2.14 (Python) and 1.4.21 (JS), CrewAI 1.15.26, OpenAI Agents SDK 0.23.1 (Python) and 0.20.0 (JS), Microsoft Agent Framework 1.21.0 (Python) and 1.24.0 (.NET), Google ADK 2.11.0 (Python) and 2.0 (TypeScript), LlamaIndex Workflows via the `llama-agents` packages, Mastra 1.75.0, Trigger.dev v4.

## LangGraph

[LangGraph](https://github.com/langchain-ai/langgraph) is a low-level graph framework. You define nodes and edges on a `StateGraph`, and a node can redirect the run with `Command(goto=...)`. There's an official TypeScript package, [`@langchain/langgraph`](https://docs.langchain.com/oss/javascript/langgraph/overview), and both are MIT.

Durability is native, with a condition. Compile the graph with a checkpointer and pass a `thread_id`, and [runs resume after failure](https://docs.langchain.com/oss/python/langgraph/durable-execution). The condition is the checkpointer. `InMemorySaver` stores checkpoints in RAM and loses them on restart. The production options are [`PostgresSaver` and `SqliteSaver`](https://docs.langchain.com/oss/python/langgraph/persistence), which you run. Keep `thread_id` under 255 characters on Postgres.

Retries are per node via [`RetryPolicy`](https://docs.langchain.com/oss/python/langgraph/use-graph-api). The default policy skips `ValueError`, `TypeError` and friends, and for HTTP libraries retries only 5xx. Human-in-the-loop is [`interrupt()`](https://docs.langchain.com/oss/python/langgraph/interrupts), resumed with `Command(resume=...)`. The docs are explicit that the node re-runs from its start on resume.

Observability is via [LangSmith](https://docs.langchain.com/oss/python/langgraph/observability), which has a free Developer tier (5k traces a month) and a Plus tier at $39 per seat. Deployment is [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments): managed Cloud on Plus and above, or a [standalone Agent Server](https://docs.langchain.com/langsmith/deploy-standalone-server) you run on Docker or Kubernetes with your own Postgres and Redis and a `LANGGRAPH_CLOUD_LICENSE_KEY`. The docs don't publish what that key costs.

**Best fit:** Python or TypeScript teams that want explicit control over a stateful agent graph. Of the frameworks here, it has the most complete durability design in the open-source package: checkpoints, per-node retries and interrupts are all first-class.

**Limits:** the checkpointer, the tracing and the server are things you operate or pay LangChain for. That's normal for a library, but budget for it when comparing against a hosted runtime.

## CrewAI

[CrewAI](https://github.com/crewAIInc/crewAI) is Python-only (3.10 to 3.13) and MIT. There's no official JavaScript SDK. It has two models: **Crews**, where role-based agents work through tasks [sequentially or under a manager agent](https://docs.crewai.com/en/concepts/processes), and **Flows**, an event-driven pipeline built from [`@start()`, `@listen()` and `@router()`](https://docs.crewai.com/en/concepts/flows).

Persistence is opt-in. A Flow decorated with `@persist` saves state to a database (SQLite by default, custom backends via `FlowPersistence`), and you resume by calling `kickoff` again with the same `id`. Crews have separate [checkpointing](https://docs.crewai.com/en/concepts/checkpointing) with `CheckpointConfig` and `Crew.from_checkpoint()`. Two caveats from the docs: automatic checkpoint writes are best-effort (failures are logged, not raised), and resume is a manual re-kickoff, nothing restarts the run for you.

Retries sit at the agent and guardrail level: `max_retry_limit` on an [agent](https://docs.crewai.com/en/concepts/agents) (default 2) and `guardrail_max_retries` on a [task](https://docs.crewai.com/en/concepts/tasks) (default 3). We couldn't find a per-tool-call retry policy.

Human-in-the-loop is [`@human_feedback()`](https://docs.crewai.com/en/learn/human-in-the-loop) on Flows (CrewAI 1.8 and later) and `human_input=True` on tasks, both console-style. Async, webhook-based approval is an Enterprise feature. Observability is through [integrations](https://docs.crewai.com/en/observability/overview) (Langfuse, Phoenix, MLflow, others) or CrewAI's own tracing, which you enable with `crewai login`. Hosting is the [CrewAI AMP platform](https://docs-platform.crewai.com/platform/en/introduction). [Pricing](https://www.crewai.com/pricing) shows a free Basic tier (50 workflow executions a month) and a custom Enterprise tier, with no published dollar figures in between.

**Best fit:** Python teams who think in roles. It's the fastest route from "three agents with different jobs" to a working crew, and Flows give you a controlled pipeline when you outgrow that.

**Limits:** the open-source resume path is manual, retries are agent-level rather than per tool call, and async approvals and managed hosting are platform features. If you're happy on AMP that's a complete stack; if you want to self-run, plan the durability layer yourself.

## OpenAI Agents SDK

The [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) is deliberately small: agents, tools, [handoffs](https://openai.github.io/openai-agents-python/handoffs/) (exposed to the model as `transfer_to_<agent>` tools), guardrails, sessions, tracing. There's no graph or workflow engine. The [TypeScript SDK](https://github.com/openai/openai-agents-js), `@openai/agents`, is official and active (Node 22+, also Deno, Bun and experimental Cloudflare Workers). Both are MIT and work with non-OpenAI models.

Durable execution is explicitly external. The [running agents docs](https://openai.github.io/openai-agents-python/running_agents/) tell you to pair the SDK with a separate durable runtime for failure recovery. What the SDK does natively is narrower and useful: a run can pause at a tool marked [`needs_approval`](https://openai.github.io/openai-agents-python/human_in_the_loop/), serialize its `RunState` with `to_string()`, and resume in a new process with `Runner.run(agent, state)`. That survives a restart. It doesn't re-run a tool call that died mid-flight.

[Sessions](https://openai.github.io/openai-agents-python/sessions/) persist conversation history, not execution progress. Python ships SQLite, Redis, SQLAlchemy and MongoDB backends plus OpenAI's server-side Conversations. The [JS SDK](https://github.com/openai/openai-agents-js) ships only an in-memory session and the OpenAI-hosted ones; for anything else you implement the `Session` interface.

Retries cover model calls only, via [`ModelRetrySettings`](https://openai.github.io/openai-agents-python/models/) in Python and an opt-in `modelSettings.retry` in JS. Tool and step execution has no built-in retry. Tracing is on by default and goes to the [OpenAI Traces dashboard](https://openai.github.io/openai-agents-python/tracing/), with processors for 25+ other vendors. There's no hosting product; it's a library.

The 2026 [sandbox agents](https://openai.github.io/openai-agents-python/sandbox_agents/) feature snapshots a sandbox *workspace* (files, session state) so a fresh container can pick up where one died. That's useful for coding agents and gets reported as "durability". It isn't a general durable-execution engine, and the docs don't claim it is.

**Best fit:** teams on OpenAI models who want the thinnest possible agent layer in Python or TypeScript. The serializable `RunState` for approvals is a genuinely nice design, and the small surface makes it the easiest framework here to drop inside someone else's runtime.

**Limits:** by design it doesn't do crash recovery or step retries, and the JS SDK ships fewer session backends than Python. The docs are upfront that you should pair it with a runtime, so budget for one.

## Microsoft Agent Framework

[Microsoft Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/) is the successor to both Semantic Kernel and AutoGen, built by the same teams. It ships for Python and .NET, with Go in public preview. There's no TypeScript SDK. MIT.

Orchestration is graph [workflows](https://learn.microsoft.com/en-us/agent-framework/concepts/workflows/) built with `WorkflowBuilder` from executors and edges, run in supersteps, plus prebuilt sequential, concurrent, [handoff](https://learn.microsoft.com/en-us/agent-framework/workflows/orchestrations/handoff), group-chat and Magentic patterns.

Durability has two tiers. Native [checkpoints](https://learn.microsoft.com/en-us/agent-framework/workflows/checkpoints) are taken at superstep boundaries and can be restored in a new process with `workflow.run(checkpoint_id=..., checkpoint_storage=...)`. Storage options are in-memory, file (single machine) and Cosmos DB (prerelease). The docs flag that checkpoints use a restricted pickle and that storage is a trust boundary. For real crash recovery and replay, the docs point to the [Durable Extension](https://learn.microsoft.com/en-us/agent-framework/hosting/azure-functions) on Azure Durable Task, via Azure Functions or your own compute.

There's no retry policy API. The [exception-handling docs](https://learn.microsoft.com/en-us/agent-framework/agents/middleware/exception-handling) say to put retries in middleware. Human-in-the-loop is native: `ctx.request_info()` with a `@response_handler` in workflows, and `@tool(approval_mode="always_require")` for tool approval. Pending requests are saved in checkpoints. [Observability](https://learn.microsoft.com/en-us/agent-framework/agents/observability) is OpenTelemetry out of the box, exported to Azure Monitor, Aspire, Langfuse or MLflow.

[Hosting](https://learn.microsoft.com/en-us/agent-framework/hosting/) is Foundry Hosted Agents (managed), self-hosting (Python packages are prerelease), or the Azure Functions route. The framework is free; Foundry and Durable Task Scheduler are metered Azure services.

**Best fit:** .NET or Azure shops that want typed graph workflows. OpenTelemetry out of the box and checkpoints that carry pending human requests are both ahead of most of the list.

**Limits:** no TypeScript SDK and no retry policy API yet. Some production pieces (Cosmos checkpoints, Python self-hosting) are still prerelease, and the strongest durability path runs through Azure Durable Task, which is ideal if that's where you deploy and a constraint if it isn't.

## Google ADK

[Google's Agent Development Kit](https://adk.dev/) is the most multilingual entry: Python (2.11.0), [TypeScript](https://github.com/google/adk-js) (`@google/adk`, 2.0 GA since August 2026), Go (2.0 GA), Java (pre-GA) and Kotlin. All Apache 2.0.

ADK 2.0 added graph [workflows](https://adk.dev/workflows/) alongside the original LLM, sequential, loop and parallel agents. Agents are now graph nodes, and routing is by edges.

Durability is native but uneven across languages. Set [`ResumabilityConfig(is_resumable=True)`](https://adk.dev/runtime/resume/) on the app and a run can be resumed by invocation ID. That's Python and Kotlin only at the time of checking; TypeScript and Go aren't listed. The docs are direct about semantics: on resume, tools run "at least once, and may run more than once". [Dynamic workflows](https://adk.dev/graphs/dynamic/) in 2.0 checkpoint automatically and skip completed sub-nodes.

[Sessions](https://adk.dev/sessions/session/) have three backends: `InMemorySessionService` (lost on restart), `DatabaseSessionService` (Postgres or SQLite) and `VertexAiSessionService` (managed). Retries arrived in 2.0 as `RetryConfig` with `max_attempts` and backoff. [Observability](https://adk.dev/observability/) is native OpenTelemetry, with Cloud Trace, Phoenix, Langfuse and Datadog as integrations.

Human-in-the-loop uses [`LongRunningFunctionTool`](https://adk.dev/tools-custom/function-tools/), where the tool starts something, the run pauses, and the client resumes with the answer. Tool [confirmation](https://adk.dev/tools-custom/confirmation/) (`require_confirmation`) exists but is marked experimental and, per its docs, isn't supported with the Database or Vertex session services.

[Deployment](https://adk.dev/deploy/) is Agent Runtime on Google's Agent Platform (the renamed Vertex AI Agent Engine), Cloud Run, GKE or any container. Agent Runtime is [usage-priced](https://cloud.google.com/vertex-ai/pricing) by vCPU-hour and GiB-hour with a free monthly allowance.

**Best fit:** teams on Gemini and Google Cloud who want the same framework across Python, TypeScript and Go. No other framework here offers that language spread under one Apache 2.0 licence, and the 2.0 retry and resume work closed a lot of the gap with LangGraph.

**Limits:** resume hasn't reached the TypeScript and Go SDKs yet, resumed tools are at-least-once (so make them idempotent), and tool confirmation is still experimental and isn't yet supported with the Database or Vertex session services.

## LlamaIndex Workflows

LlamaIndex's workflow engine now lives in the [`llama-agents`](https://github.com/run-llama/llama-agents) packages (`llama-index-workflows` for the core, plus server, client and `llamactl`). MIT. The model is [event-driven](https://developers.llamaindex.ai/python/workflows/): `@step` async functions take typed events and return typed events, and whichever step accepts the returned event type runs next.

Durability is native and more thorough than most. `Context.to_dict()` and `Context.from_dict()` let you snapshot a run and [resume it in another process](https://developers.llamaindex.ai/python/llamaagents/workflows/durable_workflows/). Completed steps don't re-run; in-flight ones do, so steps are at-least-once. A [`WorkflowServer`](https://developers.llamaindex.ai/python/llamaagents/workflows/deployment/) with `SqliteWorkflowStore` or `PostgresWorkflowStore` auto-resumes unfinished runs at startup (the default store is in-memory and lost on restart).

[Retries](https://developers.llamaindex.ai/python/workflows/retry_steps/) are per step with `retry_policy()`: default 3 attempts, 5-second delay, with exponential jitter and exception filters available. Human-in-the-loop is [`InputRequiredEvent` and `HumanResponseEvent`](https://developers.llamaindex.ai/python/llamaagents/workflows/human_in_the_loop/), with the server accepting responses over HTTP. [Observability](https://developers.llamaindex.ai/python/llamaagents/workflows/observability/) is OpenTelemetry via a separate package, plus Phoenix and LlamaTrace, with a debugger UI on the server.

One thing to know before picking it for TypeScript: the [`workflows-ts`](https://github.com/run-llama/workflows-ts) repo is archived with a deprecation notice pointing at the Python packages, even though the docs site still lists it.

**Best fit:** Python teams building document-heavy pipelines, especially if they're already on LlamaIndex for retrieval. The snapshot-and-resume model plus a server that auto-resumes unfinished runs is one of the stronger open-source durability stories on this page.

**Limits:** durability is opt-in (a store or snapshots), steps are at-least-once, and the TypeScript package is deprecated per its own repo.

## Mastra

[Mastra](https://github.com/mastra-ai/mastra) is the TypeScript-native framework in the list (`@mastra/core` 1.75.0, Node 22.13+, Apache 2.0 with some `ee/` directories under a commercial licence). Workflows are composed with [`.then()`, `.parallel()`, `.branch()`, `.foreach()`, `.dowhile()`](https://mastra.ai/docs/workflows/control-flow) and so on. Multi-agent is done with [supervisor agents](https://mastra.ai/docs/agents/supervisor-agents) that delegate to sub-agents. Agent networks are deprecated.

Durability is native, and the docs are unusually clear about its edges. [`suspend()` and `resume()`](https://mastra.ai/docs/workflows/suspend-and-resume) snapshot a run to your storage adapter and survive deployments and restarts, and `Run.restart()` picks up from the last active step. The [workers docs](https://mastra.ai/docs/deployment/workers) then say it plainly: default workers run in-process and "don't survive crashes", and if the API crashes mid-step "that work can be lost and the workflow run can remain in a `running` state." For crash-safe execution the docs point to an adapter for a third-party durable runtime that skips completed steps on retry.

[Storage](https://mastra.ai/docs/memory/storage) is adapter-based: in-memory by default (lost on exit), LibSQL for local dev, Postgres suggested for production, plus MongoDB, Redis, DynamoDB and others. [Retries](https://mastra.ai/docs/workflows/error-handling) are `retryConfig: { attempts, delay }` on a workflow or `retries` on a step. Human-in-the-loop is suspend and resume with `resumeSchema`, plus `requireApproval: true` on agent tools. [Observability](https://mastra.ai/docs/observability/overview) has built-in exporters and works with Langfuse, Datadog, Arize and OpenTelemetry-compatible backends.

[Deployment](https://mastra.ai/docs/deployment/overview) is `mastra build` to a Node server for anywhere, with adapters for Vercel, Netlify, Cloudflare and others. One more doc-stated caveat: on Vercel, Netlify, Lambda and Cloudflare Workers, [scheduled workflows](https://mastra.ai/docs/workflows/scheduled-workflows) "don't fire with the built-in scheduler today"; the same adapter covers that. [Pricing](https://mastra.ai/pricing) is public: the framework is free, Mastra Cloud has a $0 Starter tier (24 CPU hours, then $0.35 an hour) and a $250 a month Teams tier.

**Best fit:** TypeScript teams who want agents, workflows, memory, evals and a local studio in one package. It's the most complete TypeScript framework on this list.

**Limits:** the default execution engine is in-process, so for crash recovery and for scheduling on serverless hosts, use the third-party runtime adapter or the split-worker setup with Redis. Both are documented, and both are a second system to run.

## Trigger.dev

[Trigger.dev](https://trigger.dev) sits underneath the frameworks above rather than beside them. It has no prompt abstraction, no tool registry, no handoffs. You write a TypeScript `task`, deploy it, and Trigger.dev runs it on long-running machines with retries, queues, checkpointed waits and a run dashboard. Apache 2.0, self-hostable, with a managed cloud.

A task is a function with a retry policy:

```ts
import { task } from "@trigger.dev/sdk";

export const summarise = task({
  id: "summarise",
  retry: { maxAttempts: 5, factor: 2, minTimeoutInMs: 1_000, maxTimeoutInMs: 30_000 },
  run: async (payload: { url: string }) => {
    // ...
  },
});
```

Here's how durability works, because it's different from the frameworks above. When an attempt throws, Trigger.dev starts a new attempt according to the [retry settings](https://trigger.dev/docs/errors-retrying) (default 3 attempts with exponential backoff), and that new attempt **runs `run()` again from the top**. There's no step-level replay. The units of durability are the run itself, child tasks (`triggerAndWait` gives each step its own retry budget and its own log), and waits.

Waits are where the cost saving comes from. A [`wait.for`](https://trigger.dev/docs/wait-for) longer than 60 seconds, a `triggerAndWait` on a child task, or a [`wait.forToken`](https://trigger.dev/docs/wait-for-token) on a human approval checkpoints the process (CRIU on Trigger.dev Cloud), releases the machine, and restores it when the wait completes. You're not paying for a box to sit idle for 3 days. On self-hosted deployments waits block instead of checkpointing; checkpoints, warm starts and autoscaling are listed as cloud-only in the [self-hosting docs](https://trigger.dev/docs/self-hosting/overview).

For chat-shaped agents there's also [`chat.agent()`](https://trigger.dev/docs/ai-chat/overview), which runs each conversation as one long-lived task. You still write the loop with the Vercel AI SDK (`streamText` in, stream out), and it adds the parts around it: a durable session per conversation, mid-stream resume, recovery from cancel, crash or OOM, sub-agents, stop and steering, and human-in-the-loop tool approvals. It still isn't a framework by this article's definition, since the model, tools and prompts come from the AI SDK, but it's the closest thing to one on this page that isn't one.

Other things worth knowing, all from the docs: [`maxDuration`](https://trigger.dev/docs/runs/max-duration) is required and counts compute time only, not time spent waiting. An OOM crash isn't retried by default; `retry.outOfMemory: { machine: "large-1x" }` retries it on a [bigger machine](https://trigger.dev/docs/machines). `idempotencyKey` on `trigger` dedupes a child task across parent retries. Observability is the run and trace view in the dashboard, plus OpenTelemetry `exporters` and `instrumentations` in [`trigger.config.ts`](https://trigger.dev/docs/config/config-file). Python is supported by [running scripts](https://trigger.dev/docs/guides/python/python-image-processing) from a TypeScript task via `@trigger.dev/python`; there's no Python SDK for defining tasks. [Pricing](https://trigger.dev/pricing) is public: Free ($5 of monthly credit), Hobby at $10, Pro at $50, and compute billed only while a task is executing.

**Best fit:** TypeScript teams who already have (or are choosing) an agent framework and need the runs to survive crashes, redeploys and multi-day waits without running their own workers.

**Limits:** retries restart `run()` from the top, so side effects need idempotency keys (the second example below shows how). There's no Python SDK, so Python-first teams are better served by LangGraph, LlamaIndex or ADK with a Python durable runtime underneath. And if you want step-level replay rather than run-level retries, a replay-based runtime is the closer fit.

## Decision matrix

Three tables, because 11 columns don't fit on a page. Labels are defined in "How this comparison was built" above, and every cell is backed by the product section above it.

### Building the agent

| Product | Orchestration | Human approval | TypeScript | Python |
| --- | --- | --- | --- | --- |
| Trigger.dev | External (bring a framework); chat sessions and sub-agents via `chat.agent()` | Native (wait tokens; tool approvals in `chat.agent()`) | Native | Integration (runs scripts) |
| LangGraph | Native (graph) | Native (interrupts) | Native | Native |
| CrewAI | Native (crews, flows) | Native (console); async is paid platform | None | Native |
| OpenAI Agents SDK | Native (handoffs) | Native (tool approval) | Native | Native |
| Microsoft Agent Framework | Native (graph workflows) | Native (request info) | None | Native (also .NET, Go preview) |
| Google ADK | Native (sequential, loop, parallel, graph) | Native (long-running tools); confirmation experimental | Native | Native (also Go, Java, Kotlin) |
| LlamaIndex Workflows | Native (event-driven steps) | Native (input events) | Deprecated | Native |
| Mastra | Native (workflows, supervisors) | Native (suspend and resume) | Native | None |

### Surviving failure

| Product | Durable execution | State persistence | Retries |
| --- | --- | --- | --- |
| Trigger.dev | Native, run-level; waits checkpoint on cloud | Native (managed) | Native, per task with backoff |
| LangGraph | Native with a durable checkpointer | Native (Postgres or SQLite you run) | Native, per node |
| CrewAI | Partial (opt-in persist and checkpoints; manual resume) | Native (SQLite default) | Partial (agent and guardrail level) |
| OpenAI Agents SDK | External (docs say to bring a runtime); paused approvals serialize | Native for conversation history only | Partial (model calls only) |
| Microsoft Agent Framework | Native checkpoints at supersteps; Azure Durable Task for crash recovery | Native (in-memory, file; Cosmos prerelease) | External (middleware) |
| Google ADK | Partial (Python and Kotlin; tools at-least-once) | Native (in-memory, database, Vertex) | Native (2.0) |
| LlamaIndex Workflows | Native (snapshots, server store) | Native (SQLite or Postgres store) | Native, per step |
| Mastra | Partial (suspend and resume; mid-step crash can lose work); third-party runtime adapter | Native (adapters; Postgres for prod) | Native |

### Operating it

| Product | Observability | Deployment | Self-hosting | Pricing |
| --- | --- | --- | --- | --- |
| Trigger.dev | Native (dashboard, OTel export) | Native (CLI deploy) | Native (Apache 2.0; Docker, Kubernetes) | Public |
| LangGraph | Integration (LangSmith) | Paid platform (LangSmith Deployment) or licensed server | Native library (MIT); server needs a licence | Public |
| CrewAI | Integration, or paid platform (AMP) | Paid platform (AMP) or your own | Native (MIT) | Partial (free tier public, rest custom) |
| OpenAI Agents SDK | Native (OpenAI Traces) | External | Native (MIT) | Public (model usage only) |
| Microsoft Agent Framework | Native (OTel) | Paid platform (Foundry) or prerelease self-host | Native (MIT) | Partial (Azure metered) |
| Google ADK | Native (OTel) | Paid platform (Agent Runtime), Cloud Run, GKE | Native (Apache 2.0) | Public |
| LlamaIndex Workflows | Integration (OTel, Phoenix) | Native server; LlamaCloud | Native (MIT) | Partial (hosting not priced) |
| Mastra | Native, plus integrations | Native (build to Node) or paid platform | Native (Apache 2.0, some enterprise code) | Public |

Read the Durable execution column with the caveats from each section. "Native" for LangGraph means "native once you've stood up Postgres for the checkpointer". "Partial" for Mastra means "works, until the API process dies mid-step". Neither is wrong, but they're different operational commitments.

## Example: a durable agent task with a human approval

This is a refund agent. It drafts a reply with an LLM, pauses for a human to approve it (for up to 3 days, without holding a machine), then issues the refund as a child task with its own retry budget.

Setup assumptions: a project created with `npx trigger.dev@latest init`, a `trigger.config.ts` with a `maxDuration` default, and `ai`, `@ai-sdk/openai` and `stripe` installed. All of this runs as written against `@trigger.dev/sdk` v4.

```ts
// trigger/refund-agent.ts
import { task, wait, logger } from "@trigger.dev/sdk";
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { issueRefund } from "./issue-refund";

type Payload = {
  ticketId: string;
  chargeId: string;
  amountCents: number;
  customerMessage: string;
};

type Review = { approved: boolean; reviewer: string };

export const refundAgent = task({
  id: "refund-agent",
  maxDuration: 300, // compute seconds; time spent waiting doesn't count
  retry: { maxAttempts: 3, minTimeoutInMs: 1_000, maxTimeoutInMs: 10_000, factor: 2 },
  run: async (payload: Payload, { ctx }) => {
    logger.info("Drafting reply", { attempt: ctx.attempt.number });

    const { text: draft } = await generateText({
      model: openai("gpt-4.1"),
      prompt: `Draft a short, polite reply to this refund request:\n\n${payload.customerMessage}`,
    });

    // Same key on every attempt, so a retry gets the same token back
    // instead of creating a second approval request.
    const token = await wait.createToken({
      timeout: "3d",
      idempotencyKey: `review-${payload.ticketId}`,
      idempotencyKeyTTL: "3d",
      tags: [`ticket:${payload.ticketId}`],
    });

    // Hand token.url (or token.id) to your reviewer UI, Slack bot, email, etc.
    logger.info("Waiting for review", { tokenId: token.id, url: token.url, draft });

    // The run is checkpointed here and the machine is released until the token completes.
    const review = await wait.forToken<Review>(token);

    if (!review.ok) {
      return { status: "timed-out" as const };
    }
    if (!review.output.approved) {
      return { status: "rejected" as const, by: review.output.reviewer };
    }

    // A child task: its own attempts, its own logs, and the parent is
    // checkpointed while it runs.
    const refund = await issueRefund.triggerAndWait({
      ticketId: payload.ticketId,
      chargeId: payload.chargeId,
      amountCents: payload.amountCents,
    });

    if (!refund.ok) {
      throw new Error(`Refund failed: ${refund.error}`);
    }

    return { status: "refunded" as const, draft, refundId: refund.output.refundId };
  },
});
```

The reviewer completes the token from anywhere, with the SDK or a POST to `token.url`:

```ts
import { wait } from "@trigger.dev/sdk";

await wait.completeToken<Review>(tokenId, { approved: true, reviewer: "sam@example.com" });
```

What "durable" means here, operationally: if the worker is killed while `generateText` is in flight, attempt 2 starts from the top, drafts again, and `wait.createToken` returns the existing token because the idempotency key matches. If the reviewer already approved before the retry landed, `wait.forToken` returns immediately. And while the run waits those 3 days, it's a checkpoint on disk, not a machine on your bill.

## Example: failure recovery and idempotency

The refund itself is the dangerous bit. Trigger.dev will happily retry `issueRefund` 5 times, and every attempt runs the function from the top. Without a safeguard, a network blip after Stripe processed the refund but before the response arrived means attempt 2 refunds the customer again.

The fix is to give the side effect a stable key, and to give any downstream trigger one too.

```ts
// trigger/issue-refund.ts
import { task, idempotencyKeys, logger } from "@trigger.dev/sdk";
import Stripe from "stripe";
import { sendRefundEmail } from "./send-refund-email";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const issueRefund = task({
  id: "issue-refund",
  retry: {
    maxAttempts: 5,
    minTimeoutInMs: 2_000,
    maxTimeoutInMs: 60_000,
    factor: 2,
    randomize: true,
  },
  run: async (
    payload: { ticketId: string; chargeId: string; amountCents: number },
    { ctx }
  ) => {
    logger.info("Refund attempt", { attempt: ctx.attempt.number });

    // Stripe dedupes requests that reuse an idempotency key, so a retry
    // after a timeout returns the original refund instead of a second one.
    const refund = await stripe.refunds.create(
      { charge: payload.chargeId, amount: payload.amountCents },
      { idempotencyKey: `refund-${payload.ticketId}` }
    );

    // The key is scoped to this run by default, so across all 5 attempts
    // the email task is only ever triggered once.
    await sendRefundEmail.trigger(
      { ticketId: payload.ticketId, refundId: refund.id },
      { idempotencyKey: await idempotencyKeys.create("refund-email") }
    );

    return { refundId: refund.id };
  },
});
```

Two patterns, both from the [idempotency docs](https://trigger.dev/docs/idempotency). For external APIs, pass the provider's own idempotency key (Stripe, and most payment and email APIs, support one). For child tasks, `idempotencyKeys.create` is scoped to the current run, so a retried parent can't fan out duplicate children. If the API you're calling has no idempotency support, write a unique operation ID to your database before the call and check it first; a read-before-write without a unique constraint can still race.

This is the honest trade-off of run-level durability versus step replay. A step-replay runtime would memoise the Stripe call and skip it on replay. Trigger.dev makes you say which calls are safe to repeat. In exchange your task is plain TypeScript with no determinism rules, and the retry and wait behaviour is visible in the run's trace.

## Running each framework on Trigger.dev

Every framework section above ends the same way: the framework handles the agent, something else handles crashes, retries and long waits. Here's what that pairing looks like for the TypeScript options. In each case the agent is defined exactly as the framework's own docs show. The task around it adds retries, a queue, a trace in the dashboard, and the ability to `wait` for days without holding a machine.

**Vercel AI SDK** is the refund example above: `generateText` inside `run()`. The docs have a longer [deep research agent](https://trigger.dev/docs/guides/example-projects/vercel-ai-sdk-deep-research) built the same way.

**OpenAI Agents SDK.** The `Agent` and `run` calls are straight from the [SDK's own hello world](https://github.com/openai/openai-agents-js). There's a [playground project](https://trigger.dev/docs/guides/example-projects/openai-agents-sdk-typescript-playground) in the docs with tools, handoffs and guardrails.

```ts
// trigger/ask-assistant.ts
import { task } from "@trigger.dev/sdk";
import { Agent, run } from "@openai/agents";

const assistant = new Agent({
  name: "Assistant",
  instructions: "You are a helpful assistant.",
});

export const askAssistant = task({
  id: "ask-assistant",
  retry: { maxAttempts: 3 },
  run: async (payload: { prompt: string }) => {
    const result = await run(assistant, payload.prompt);
    return { answer: result.finalOutput };
  },
});
```

**Mastra.** Register your agents on a Mastra instance as usual, then call them from a task. Mastra's own workflows keep their suspend and resume semantics; what the task adds is a process that survives, which is the gap Mastra's workers docs describe. The docs have a [multi-agent example with memory](https://trigger.dev/docs/guides/example-projects/mastra-agents-with-memory).

```ts
// trigger/support-reply.ts
import { task } from "@trigger.dev/sdk";
import { mastra } from "../mastra"; // your Mastra instance, agents registered

export const supportReply = task({
  id: "support-reply",
  retry: { maxAttempts: 3 },
  run: async (payload: { message: string }) => {
    const agent = mastra.getAgentById("support-agent");
    const response = await agent.generate(payload.message);
    return { reply: response.text };
  },
});
```

**LangGraph JS.** This is the pairing where both layers do real work. The graph is compiled with a `PostgresSaver`, so LangGraph owns the per-node checkpoints for the thread and they survive a Trigger.dev retry. Trigger.dev owns the process, the retry schedule and the wait. Decide which layer retries what: a flaky tool call belongs in a LangGraph `RetryPolicy`, a dead worker belongs to the task.

```ts
// trigger/run-graph.ts
import { task } from "@trigger.dev/sdk";
import { HumanMessage } from "@langchain/core/messages";
import { graph } from "../graph"; // a StateGraph compiled with PostgresSaver

export const runGraph = task({
  id: "run-graph",
  retry: { maxAttempts: 3 },
  run: async (payload: { threadId: string; input: string }) => {
    const result = await graph.invoke(
      { messages: [new HumanMessage(payload.input)] },
      { configurable: { thread_id: payload.threadId } }
    );

    return { messages: result.messages };
  },
});
```

None of these snippets changes what the framework does. They change where it runs and what happens when that stops.

## Choosing a stack

Pick the framework for the language and orchestration style you want. Then look at its "Durable execution" cell and decide whether that's enough for your failure modes. Three combinations that make sense:

**Python, graph control, you run infra.** LangGraph with `PostgresSaver`, LangSmith for tracing, and either the licensed standalone Agent Server or a durable runtime you wrap it in. You own Postgres and Redis either way.

**OpenAI-native, minimal framework.** The OpenAI Agents SDK, plus a durable runtime, which its own docs say to add. In TypeScript, `@openai/agents` inside a Trigger.dev task gets you retries, queues and checkpointed approvals without writing the runtime yourself.

**TypeScript end to end.** Mastra or the Vercel AI SDK for the agent, and Trigger.dev underneath for crash recovery and long waits. If your agent needs to sit on a human approval for days, a checkpointed wait is the feature to look for.

A fair way to compare setup cost is to count the steps to a run that survives a restart. With LangGraph you write the graph, run Postgres, wire up a `PostgresSaver`, and either license the standalone server or host the workers yourself. With Mastra you set a Postgres storage adapter and either split workers behind Redis or add its third-party runtime adapter. With Trigger.dev you run `npx trigger.dev@latest init`, write the task, and run `npx trigger.dev@latest deploy`; the workers, queue and checkpointing are the product. The trade is that your unit of durability is the run, not the step.

Trigger.dev is one option for that bottom layer, and several of the frameworks above ship adapters for others. Pick by language, by whether you want step replay or run-level retries, and by who you want running the workers.

## FAQs

- **Do I need both a framework and a runtime?** If your agent finishes inside one request and can safely restart from scratch, no. Once a run outlives a request, calls APIs with side effects, or waits on a human, you need something that persists progress and retries safely. That can be the framework's own checkpointing plus infrastructure you operate, or a separate runtime.

- **Can LangGraph, CrewAI or the others run in production without extra infrastructure?** Yes, but "extra infrastructure" tends to be the checkpoint store. LangGraph needs a Postgres or SQLite checkpointer for state to survive a restart. CrewAI needs `@persist` or checkpointing turned on, and you re-kickoff manually. Mastra needs Postgres and either split workers or its runtime adapter to survive a mid-step crash. The OpenAI Agents SDK docs tell you outright to bring a durable runtime.

- **What does "durable execution" actually mean?** A run records enough progress to recover after an interruption, instead of depending on one uninterrupted process. Implementations differ: some runtimes replay a log of completed steps, LangGraph and LlamaIndex snapshot state at step boundaries, and Trigger.dev retries the whole run and checkpoints the process during waits. None of them makes a non-idempotent API call safe to repeat; that's still your job.

- **Is Trigger.dev a replacement for an agent framework?** No. It has no prompts, tools or handoffs. It runs TypeScript tasks with retries, queues, checkpointed waits and a dashboard. You'd typically put the Vercel AI SDK, `@openai/agents` or Mastra inside a task. For chat agents, `chat.agent()` adds durable sessions, resume, sub-agents and tool approvals on top of the Vercel AI SDK, but the loop is still the AI SDK's.

- **Which of these support TypeScript natively?** LangGraph (`@langchain/langgraph`), the OpenAI Agents SDK (`@openai/agents`), Google ADK (`@google/adk`), Mastra and Trigger.dev. CrewAI and Microsoft Agent Framework have no TypeScript SDK. LlamaIndex's TypeScript workflows package is archived and deprecated per its repo.

## The takeaway

Framework choice and execution-infrastructure choice are two decisions, and they compound. A great framework on an in-process runtime loses runs on redeploy. A great runtime with no framework means writing your own tool loop. Evaluate them separately, test each with a worker kill and a 3-day wait before you commit, and read the "Durable execution" cell twice.
