Best AI agent frameworks for production: frameworks vs durable infrastructure

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 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, 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. The condition is the checkpointer. InMemorySaver stores checkpoints in RAM and loses them on restart. The production options are PostgresSaver and SqliteSaver, which you run. Keep thread_id under 255 characters on Postgres.

Retries are per node via RetryPolicy. The default policy skips ValueError, TypeError and friends, and for HTTP libraries retries only 5xx. Human-in-the-loop is interrupt(), resumed with Command(resume=...). The docs are explicit that the node re-runs from its start on resume.

Observability is via LangSmith, which has a free Developer tier (5k traces a month) and a Plus tier at $39 per seat. Deployment is LangSmith Deployment: managed Cloud on Plus and above, or a standalone Agent 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 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, and Flows, an event-driven pipeline built from @start(), @listen() and @router().

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 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 (default 2) and guardrail_max_retries on a task (default 3). We couldn't find a per-tool-call retry policy.

Human-in-the-loop is @human_feedback() 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 (Langfuse, Phoenix, MLflow, others) or CrewAI's own tracing, which you enable with crewai login. Hosting is the CrewAI AMP platform. 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 is deliberately small: agents, tools, handoffs (exposed to the model as transfer_to_<agent> tools), guardrails, sessions, tracing. There's no graph or workflow engine. The TypeScript SDK, @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 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, 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 persist conversation history, not execution progress. Python ships SQLite, Redis, SQLAlchemy and MongoDB backends plus OpenAI's server-side Conversations. The JS SDK 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 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, with processors for 25+ other vendors. There's no hosting product; it's a library.

The 2026 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 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 built with WorkflowBuilder from executors and edges, run in supersteps, plus prebuilt sequential, concurrent, handoff, group-chat and Magentic patterns.

Durability has two tiers. Native 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 on Azure Durable Task, via Azure Functions or your own compute.

There's no retry policy API. The exception-handling docs 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 is OpenTelemetry out of the box, exported to Azure Monitor, Aspire, Langfuse or MLflow.

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 is the most multilingual entry: Python (2.11.0), TypeScript (@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 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) 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 in 2.0 checkpoint automatically and skip completed sub-nodes.

Sessions 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 is native OpenTelemetry, with Cloud Trace, Phoenix, Langfuse and Datadog as integrations.

Human-in-the-loop uses LongRunningFunctionTool, where the tool starts something, the run pauses, and the client resumes with the answer. Tool confirmation (require_confirmation) exists but is marked experimental and, per its docs, isn't supported with the Database or Vertex session services.

Deployment 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 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 packages (llama-index-workflows for the core, plus server, client and llamactl). MIT. The model is event-driven: @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. Completed steps don't re-run; in-flight ones do, so steps are at-least-once. A WorkflowServer with SqliteWorkflowStore or PostgresWorkflowStore auto-resumes unfinished runs at startup (the default store is in-memory and lost on restart).

Retries 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, with the server accepting responses over HTTP. 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 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 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() and so on. Multi-agent is done with 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() 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 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 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 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 has built-in exporters and works with Langfuse, Datadog, Arize and OpenTelemetry-compatible backends.

Deployment 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 "don't fire with the built-in scheduler today"; the same adapter covers that. 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 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:

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 (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 longer than 60 seconds, a triggerAndWait on a child task, or a wait.forToken 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.

For chat-shaped agents there's also chat.agent(), 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 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. 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. Python is supported by running scripts from a TypeScript task via @trigger.dev/python; there's no Python SDK for defining tasks. 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

ProductOrchestrationHuman approvalTypeScriptPython
Trigger.devExternal (bring a framework); chat sessions and sub-agents via chat.agent()Native (wait tokens; tool approvals in chat.agent())NativeIntegration (runs scripts)
LangGraphNative (graph)Native (interrupts)NativeNative
CrewAINative (crews, flows)Native (console); async is paid platformNoneNative
OpenAI Agents SDKNative (handoffs)Native (tool approval)NativeNative
Microsoft Agent FrameworkNative (graph workflows)Native (request info)NoneNative (also .NET, Go preview)
Google ADKNative (sequential, loop, parallel, graph)Native (long-running tools); confirmation experimentalNativeNative (also Go, Java, Kotlin)
LlamaIndex WorkflowsNative (event-driven steps)Native (input events)DeprecatedNative
MastraNative (workflows, supervisors)Native (suspend and resume)NativeNone

Surviving failure

ProductDurable executionState persistenceRetries
Trigger.devNative, run-level; waits checkpoint on cloudNative (managed)Native, per task with backoff
LangGraphNative with a durable checkpointerNative (Postgres or SQLite you run)Native, per node
CrewAIPartial (opt-in persist and checkpoints; manual resume)Native (SQLite default)Partial (agent and guardrail level)
OpenAI Agents SDKExternal (docs say to bring a runtime); paused approvals serializeNative for conversation history onlyPartial (model calls only)
Microsoft Agent FrameworkNative checkpoints at supersteps; Azure Durable Task for crash recoveryNative (in-memory, file; Cosmos prerelease)External (middleware)
Google ADKPartial (Python and Kotlin; tools at-least-once)Native (in-memory, database, Vertex)Native (2.0)
LlamaIndex WorkflowsNative (snapshots, server store)Native (SQLite or Postgres store)Native, per step
MastraPartial (suspend and resume; mid-step crash can lose work); third-party runtime adapterNative (adapters; Postgres for prod)Native

Operating it

ProductObservabilityDeploymentSelf-hostingPricing
Trigger.devNative (dashboard, OTel export)Native (CLI deploy)Native (Apache 2.0; Docker, Kubernetes)Public
LangGraphIntegration (LangSmith)Paid platform (LangSmith Deployment) or licensed serverNative library (MIT); server needs a licencePublic
CrewAIIntegration, or paid platform (AMP)Paid platform (AMP) or your ownNative (MIT)Partial (free tier public, rest custom)
OpenAI Agents SDKNative (OpenAI Traces)ExternalNative (MIT)Public (model usage only)
Microsoft Agent FrameworkNative (OTel)Paid platform (Foundry) or prerelease self-hostNative (MIT)Partial (Azure metered)
Google ADKNative (OTel)Paid platform (Agent Runtime), Cloud Run, GKENative (Apache 2.0)Public
LlamaIndex WorkflowsIntegration (OTel, Phoenix)Native server; LlamaCloudNative (MIT)Partial (hosting not priced)
MastraNative, plus integrationsNative (build to Node) or paid platformNative (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.

// 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:

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

await wait.completeToken<Review>(tokenId, { approved: true, reviewer: "[email protected]" });

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.

// 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. 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 built the same way.

OpenAI Agents SDK. The Agent and run calls are straight from the SDK's own hello world. There's a playground project in the docs with tools, handoffs and guardrails.

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

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

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