---
title: 'Mastra Durable Agents: A 2026 TypeScript Tutorial'
description: Mastra's beta Durable Agents survive dropped connections and replay missed events on reconnect. A hands-on TypeScript tutorial with real, executed code.
url: https://nerdleveltech.com/mastra-durable-agents-typescript-tutorial
language: en
date: '2026-07-17'
category: ai-ml
tags:
  - mastra
  - durable agents
  - ai agents
  - typescript
  - agent frameworks
  - resumable streams
markdown_url: https://nerdleveltech.com/mastra-durable-agents-typescript-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/mastra-durable-agents-typescript-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# Mastra Durable Agents: A 2026 TypeScript Tutorial

Mastra is a TypeScript framework for building AI agents, and its beta **Durable Agents** feature wraps a regular agent so its stream survives a dropped connection[^1]. This guide builds one from scratch, actually runs it, kills a client mid-stream, and reconnects — with the real terminal output to prove it works.

## TL;DR

Mastra's Durable Agents (beta, added in `@mastra/core@1.45.0`[^2]) wrap a regular `Agent` so its stream survives dropped connections: reconnect with `observe(runId)` and Mastra replays every missed event from cache before resuming live, without restarting the agent's turn from scratch. You'll build one, start a stream, simulate a disconnect after 3 events, reconnect from a second "client," and watch all 8 events — the 3 you missed plus 5 new ones — arrive in order. Runtime: Node.js 22.13+[^3], `@mastra/core@1.51.0`[^4]. Build time: 25-35 minutes.

## What You'll Learn

- What Durable Agents actually change versus a regular Mastra `Agent`, and when that matters
- How to wrap an agent with `createDurableAgent()` and read back its `runId`, `output`, and `cleanup()`
- How to simulate a dropped connection and reconnect with `observe(runId)` — with real captured output showing the replay
- How to run the whole tutorial without an API key, using Mastra's own internal mock-model test utility
- The difference between the three durable execution variants: `createDurableAgent()`, `createEventedAgent()`, and `createInngestAgent()`
- What's still missing if you only use the default in-memory cache, and what Redis buys you instead

## What Mastra Is

Mastra is an open-source TypeScript framework for building AI agents, workflows, and RAG pipelines, built by the team that previously built Gatsby[^5]. The company closed a $13M seed round in March 2025 (publicly announced October 8, 2025) after joining Y Combinator's Winter 2025 batch, then raised a $22M Series A led by Spark Capital on April 9, 2026 — bringing total funding to $35M — alongside the commercial launch of the Mastra platform (Studio, Server, and a Memory Gateway) built on top of the open-source framework[^6][^14].

In the October 2025 announcement, Mastra said the framework had grown from roughly 10,000 to a sustained 150,000 weekly npm downloads in its first year (October 2024 to October 2025) — a pace the company described as the third-fastest of any JavaScript framework it could find comparable growth data for[^6]. npm's own download-stats API put `@mastra/core` at 937,152 downloads for the week of May 19-25, 2026[^7], and the project's GitHub repository lists over 23,000 stars as of this writing — Mastra's own site showed noticeably higher, mutually inconsistent counts (25.3k to 26.2k, no two page fetches agreeing) across different pages during this post's research, so the direct GitHub repository page is used here as the more reliable source[^8].

Companies that have publicly described using Mastra include Replit, SoftBank, PayPal, and Adobe as of the October 2025 announcement[^6], and Brex, Sanity, Factorial, Indeed, MongoDB, Workday, and Salesforce as of the April 2026 Series A post[^14].

None of that is required to follow this tutorial — it's context for why a framework you may not have used yet is worth a look. The actual subject is one specific, fairly new feature.

## The Problem Durable Agents Solve

A regular Mastra `Agent` streams its response over the same HTTP connection that started the request. If that connection drops — a mobile client losing signal, a browser tab refreshing, a load balancer timing out a long request — the stream is gone. There's no resuming from where it left off; the caller has to start the turn over[^1].

Durable Agents change that. `createDurableAgent()` wraps an existing `Agent` so the agentic loop runs inside a Mastra workflow instead of being tied to the request. As the loop produces events, they're published to a PubSub topic keyed by a `runId` and cached. A caller can subscribe to that topic, disconnect, and reconnect later — from the same client or a different one — by calling `observe(runId)`, which replays every cached event before switching to live delivery[^1].

The feature is in **beta** as of this writing; Mastra's own docs carry an explicit warning that "APIs may change in future releases"[^9], so treat the exact method signatures here as a snapshot of `@mastra/core@1.51.0`, not a permanent contract.

## Setup

This tutorial only needs `@mastra/core` — no database, no Redis, no Docker, and (as you'll see in a moment) no LLM API key.

```bash
mkdir mastra-durable-demo && cd mastra-durable-demo
npm init -y
npm install @mastra/core@1.51.0 typescript tsx zod
npm install --save-dev @types/node
```

`@mastra/core` requires Node.js `>=22.13.0`[^3] and accepts `zod@^3.25.0` or `^4.0.0` as a peer dependency[^4]. Add a `tsconfig.json`:

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["*.ts"]
}
```

That `skipLibCheck: true` isn't boilerplate — see the Gotchas section below for why it's load-bearing. Set `"type": "module"` in `package.json` too, since the examples use top-level `await`.

## Build a Regular Agent

Start with a plain `Agent`. This is the same primitive every Mastra tutorial starts with — nothing durable about it yet:

```typescript
import { Agent } from '@mastra/core/agent';

const agent = new Agent({
  id: 'virtualDr',
  name: 'Virtual Dr',
  instructions: 'You are a careful medical research assistant.',
  model: 'anthropic/claude-sonnet-4-6', // any model string Mastra's router supports
});
```

Called directly, `agent.stream(...)` would return a response tied to this process and this call. If you `await` it and the process dies, or the caller's connection drops, that response is gone. That's the exact gap Durable Agents close.

## Wrap It with createDurableAgent()

`createDurableAgent()`, from `@mastra/core/agent/durable`, takes an existing `Agent` and returns a `DurableAgent` with the same `.stream()` shape plus two new methods: `.observe()` and `.resume()`[^10].

```typescript
import { createDurableAgent } from '@mastra/core/agent/durable';

const durableAgent = createDurableAgent({ agent });
```

That's the whole wrapping step. Notably, this does **not** require constructing a full `Mastra({...})` app instance — Mastra's own docs example wires one up, but `durableAgent.stream()` works standalone, which is what the code below actually does. You only need a `Mastra` instance if you're also running the HTTP server layer.

## Run It, Then Kill the Connection on Purpose

Here's the part that's more interesting to prove than to read about. The script below plays two roles: "Client A" starts a stream and deliberately stops reading after 3 events (simulating a dropped connection, including *not* calling `cleanup()` — a real disconnect wouldn't get the chance to). "Client B" then reconnects using the `runId` Client A captured before it "dropped."

```typescript
import { Agent } from '@mastra/core/agent';
import { createDurableAgent } from '@mastra/core/agent/durable';
import { createMockModel } from '@mastra/core/test-utils/llm-mock';

async function main() {
  const model = createMockModel({
    mockText: 'The first-line treatment for adult migraine is usually a triptan or an NSAID.',
  });

  const agent = new Agent({
    id: 'virtualDr',
    name: 'Virtual Dr',
    instructions: 'You are a careful medical research assistant.',
    model,
  });

  const durableAgent = createDurableAgent({ agent });

  console.log('--- Client A: starting stream() ---');
  const clientA = await durableAgent.stream(
    "What's the first-line treatment for adult migraine?",
  );
  console.log('runId:', clientA.runId);

  const seenByA: string[] = [];
  let chunkCount = 0;
  for await (const chunk of clientA.output.fullStream) {
    chunkCount++;
    seenByA.push(chunk.type);
    // Simulate client A's connection dropping after the 3rd event
    if (chunkCount === 3) {
      console.log('Client A: simulating a dropped connection after', chunkCount, 'chunks');
      break;
    }
  }
  console.log('Client A saw chunk types:', seenByA);

  console.log('\n--- Client B: reconnecting with observe(runId) ---');
  const clientB = await durableAgent.observe(clientA.runId);
  const seenByB: string[] = [];
  for await (const chunk of clientB.output.fullStream) {
    seenByB.push(chunk.type);
  }
  console.log('Client B saw chunk types (replayed + live):', seenByB);

  const finalText = await clientB.output.text;
  console.log('\nFinal text (via reconnected client):', finalText);

  clientB.cleanup();
}

main().catch((err) => {
  console.error('FAILED:', err);
  process.exit(1);
});
```

Ignore `createMockModel` for a moment — it's explained in the next section. Run it:

```bash
npx tsx durable-demo.ts
```

Real output from this exact script, unedited:

```
--- Client A: starting stream() ---
runId: 09345c55-61da-4eb8-8238-6f4709b15e2d
Client A: simulating a dropped connection after 3 chunks
Client A saw chunk types: [ 'start', 'step-start', 'response-metadata' ]

--- Client B: reconnecting with observe(runId) ---
Client B saw chunk types (replayed + live): [
  'start',
  'step-start',
  'response-metadata',
  'text-start',
  'text-delta',
  'text-end',
  'step-finish',
  'finish'
]

Final text (via reconnected client): The first-line treatment for adult migraine is usually a triptan or an NSAID.
```

That's the feature working end to end. Client A's connection drops after 3 events. Client B reconnects with nothing but the `runId` string and sees all 8 — the same 3 Client A already got, replayed from cache, followed by the 5 that happened after Client A "left." Nobody re-ran the agent's turn from the beginning, and Client B never touched the original `clientA` object.

## Testing Without an API Key

The demo above never calls a real model provider. `createMockModel()`, imported from `@mastra/core/test-utils/llm-mock`, is a utility Mastra ships internally for its own test suite. It doesn't appear anywhere in the public docs site's navigation or content, and it has a genuinely broken types setup: `@mastra/core`'s own `package.json` declares a `types` path for this export pointing at `./dist/test-utils/llm-mock.d.ts`, but that file does not actually exist in the published package — we checked with a direct filesystem test (`test -f`), not just by reading the `exports` map.

Under the hood, `createMockModel()` constructs a real object that satisfies the AI SDK's `LanguageModelV2` interface (`doGenerate`/`doStream`), confirmed by reading the compiled `dist/test-utils/llm-mock.js` directly, so Mastra's `Agent` accepts it exactly like it would accept a real provider model — which is why the durable-execution machinery above runs for real, with no network calls and no billing.

Because the declared types file is missing, TypeScript needs a small ambient module declaration to stop complaining:

```typescript
// durable-agent-mock.d.ts
declare module '@mastra/core/test-utils/llm-mock' {
  import type { Agent } from '@mastra/core/agent';
  export function createMockModel(
    config: { mockText: string },
  ): ConstructorParameters<typeof Agent>[0]['model'];
}
```

This mirrors a pattern already covered on this blog for a different framework — Vercel's AI SDK ships its own public, documented mock model for the same purpose[^11] (that post covers `MockLanguageModelV3`; Vercel's AI SDK has since shipped v7, whose current equivalent is `MockLanguageModelV4`, confirmed against the live AI SDK docs while fact-checking this post[^15]).

Mastra's equivalent is real and works, but because it's undocumented and untyped, treat it as a way to verify your *own* code's plumbing during development, not as a stable public API to build production tests against. For a real deployment, swap `model` back to a provider string like `'anthropic/claude-sonnet-4-6'` or a real provider SDK instance — nothing else in the durable-agent code changes.

## The Three Execution Variants

Mastra actually offers three factory functions for building a durable agent, not one — and they don't all live in the same package. They differ in where the workflow actually runs[^1][^10]:

| Factory | Package | Runs where | Best for |
|---|---|---|---|
| `createDurableAgent()` | `@mastra/core` | In-process; the workflow runs to completion before `.stream()` hands back a result | Local dev, single-process servers — the one used above |
| `createEventedAgent()` | `@mastra/core` | In-process, but non-blocking (`startAsync`) | Fire-and-forget background runs the caller doesn't wait on |
| `createInngestAgent()` | `@mastra/inngest` | On the Inngest platform | Production — adds per-step memoization, automatic retries, and a monitoring dashboard |

All three return an object with the same `Agent`-shaped surface, so switching between them later is mostly a constructor swap, not a rewrite[^10].

## Gotchas We Hit

**`skipLibCheck: true` is required, not optional.** Running `tsc --strict --noUncheckedIndexedAccess --noEmit` against the code above without it produces 454 type errors — every single one inside Mastra's own shipped `.d.ts` files, not this tutorial's code. The package vendors multiple internally-conflicting copies of `@ai-sdk/provider` types (v5, v6, and v7 live side by side under `dist/_types/`), and TypeScript's strict mode trips over the overlap. With `skipLibCheck: true`, the same command exits clean. This is a library-internal issue, not a sign your own code is wrong — the same kind of shipped-types gotcha has shown up before in this corpus against an unrelated Anthropic SDK version[^12].

**The default cache is in-memory and single-process.** `createDurableAgent()` and `createEventedAgent()` default to an `InMemoryServerCache` (the concrete class behind the `MastraServerCache` interface that the `cache` option accepts) unless you configure otherwise, which is exactly why the demo above works with zero setup — but it also means the "reconnect" only really proves itself within one running process. Surviving an actual process crash or restart needs a persistent cache backend, like `RedisServerCache` from `@mastra/redis`[^1]:

```typescript
import { createDurableAgent } from '@mastra/core/agent/durable';
import { RedisServerCache } from '@mastra/redis';

const durableAgent = createDurableAgent({
  agent,
  cache: new RedisServerCache({ url: 'redis://localhost:6379' }),
});
```

We did not execute this Redis-backed path — this sandbox has no Redis server available and no way to install one — so treat it as documented-but-unverified, distinct from the in-process reconnect demonstrated above, which was actually run.

**Nobody called `cleanup()` for Client A, on purpose.** Every `stream()`/`observe()` call returns a `cleanup()` function that immediately frees the run's registry entry; if you don't call it, an automatic timer does after `cleanupTimeoutMs` (30 seconds by default)[^10]. The demo script never calls it for Client A, because a real dropped connection wouldn't get the chance to either — and the run's cached events were still there for Client B regardless.

## Tool Approval Works the Same Way

Durable Agents also support `requireToolApproval`, which suspends the run and calls `onSuspended` when a tool call needs a human sign-off, resumable later with `.resume(runId, { approved: true })`[^1]. The suspend/resume shape is different from — but philosophically related to — the hooks-and-`canUseTool` permission layer covered in this blog's Claude Agent SDK human-in-the-loop tutorial[^13]. Both exist to put a person between an agent and an action it shouldn't take unsupervised, just implemented at different layers of two different frameworks.

## When to Use This (and When Not To)

Mastra's own guidance is direct: reach for a durable agent when a client might drop and reconnect mid-stream, when the agentic loop might outlive a single HTTP request, when you need a second client to observe a run the first one started, or when you want Inngest-backed retries and monitoring. For a short-lived, request-scoped call where the client just stays connected, a regular `Agent` with `.stream()` or `.generate()` is simpler and has one less moving part[^1].

## FAQ

**Is Mastra's Durable Agents feature stable?** No — Mastra's own docs mark it as beta and explicitly warn that APIs may change in future releases[^9]. It requires `@mastra/core@1.45.0` or later[^2].

**Do I need Redis to use Durable Agents?** No. The default in-memory cache works out of the box and is enough to reconnect within the same running process, which is what this tutorial demonstrates. You only need a persistent cache like Redis if you need the run to survive an actual process restart[^1].

**Can I follow this tutorial without an LLM API key?** Yes. Every code sample here runs against Mastra's internal `createMockModel()` test utility instead of a real provider, so there's no signup and no API cost required to reproduce the output shown[^1].

**What's the difference between `createDurableAgent()` and `createEventedAgent()`?** `createDurableAgent()` runs the workflow to completion in-process before handing back a result to consume; `createEventedAgent()` starts the workflow in the background (non-blocking, via `startAsync`) and lets you consume events through PubSub without the caller waiting on it[^10].

## Verification

What was actually executed in a real Node.js process for this post: `npm install` of the real, registry-published `@mastra/core@1.51.0`[^4]; a `tsc --strict --noUncheckedIndexedAccess --noEmit` pass (clean only with `skipLibCheck: true`, for the reason explained above); and a live `tsx` run of the full Client A / Client B script, with the terminal output shown reproduced verbatim from that run.

What was not executed: any call to a real external model provider (this automated pipeline has no `ANTHROPIC_API_KEY` or equivalent available, so `createMockModel()` stands in, as explained above) and the Redis-backed persistent-cache path (no Redis server available in this environment). Both gaps are disclosed inline where they're relevant rather than presented as tested.

---

*Related reading on NerdLevelTech: [Test Vercel AI SDK Code with MockLanguageModelV3 (2026)](https://nerdleveltech.com/test-vercel-ai-sdk-mock-language-model-tutorial) for the equivalent no-API-key testing pattern in a different framework, [Add Human-in-the-Loop Approval to Claude Agent SDK (2026)](https://nerdleveltech.com/human-in-the-loop-claude-agent-sdk-typescript-tutorial) for a deeper look at approval gates, and [Claude Tool Use in TypeScript: Agentic Loop Tutorial (2026)](https://nerdleveltech.com/claude-sonnet-4-6-tool-use-typescript-agentic-loop-tutorial) if you want to see the raw agentic loop a framework like Mastra is abstracting over.*

[^1]: [Durable agents | Long-running agents | Mastra Docs](https://mastra.ai/docs/long-running-agents/durable-agents), fetched July 17, 2026.
[^2]: [Durable agents | Long-running agents | Mastra Docs](https://mastra.ai/docs/long-running-agents/durable-agents) — "Added in: `@mastra/core@1.45.0`," fetched July 17, 2026. (Corrected during a post-publish adversarial sweep: this line lives on the guide page, not the reference page originally cited here.)
[^3]: `npm view @mastra/core engines --json` against the npm registry, run July 17, 2026 — `{"node": ">=22.13.0"}`.
[^4]: `npm view @mastra/core dist-tags --json` and `npm view @mastra/core time --json` against the npm registry, run July 17, 2026 — `latest` is `1.51.0`, published 2026-07-15.
[^5]: [GitHub - mastra-ai/mastra](https://github.com/mastra-ai/mastra), fetched July 17, 2026.
[^6]: [Announcing our $13m seed round from YC, pg, Gradient, Amjad, Guillermo, Balaji, and 120+ others](https://mastra.ai/blog/seed-round), Mastra Blog, published October 8, 2025, fetched July 17, 2026.
[^7]: [npm downloads API](https://api.npmjs.org/downloads/point/last-week/@mastra/core), queried July 17, 2026 — returned 937,152 downloads for the week of 2026-05-19 to 2026-05-25.
[^8]: [GitHub - mastra-ai/mastra](https://github.com/mastra-ai/mastra/stargazers), fetched directly July 17, 2026 (23.9k stars shown on the repository page itself; cited here as "over 23,000" since Mastra's own site displayed inconsistent, non-matching star counts — 25.3k, 25.5k, 25.7k, and 26.2k, no two page fetches agreeing — across different pages during this post's research and a subsequent fact-check sweep).
[^9]: [Durable agents | Long-running agents | Mastra Docs](https://mastra.ai/docs/long-running-agents/durable-agents) — beta warning banner, fetched July 17, 2026.
[^10]: [Reference: DurableAgent | Agents | Mastra Docs](https://mastra.ai/reference/agents/durable-agent), fetched July 17, 2026.
[^11]: [Test Vercel AI SDK Code with MockLanguageModelV3 (2026)](https://nerdleveltech.com/test-vercel-ai-sdk-mock-language-model-tutorial), NerdLevelTech.
[^12]: [Add Human-in-the-Loop Approval to Claude Agent SDK (2026)](https://nerdleveltech.com/human-in-the-loop-claude-agent-sdk-typescript-tutorial), NerdLevelTech — documents the same `skipLibCheck` requirement against a shipped Claude Agent SDK version.
[^13]: [Add Human-in-the-Loop Approval to Claude Agent SDK (2026)](https://nerdleveltech.com/human-in-the-loop-claude-agent-sdk-typescript-tutorial), NerdLevelTech.
[^14]: [Mastra raises $22M Series A led by Spark Capital](https://mastra.ai/blog/series-a), Mastra Blog, published April 9, 2026, fetched July 17, 2026 — total funding to date $35M; customers named include Brex, Sanity, Factorial, Indeed, Marsh McLennan, MongoDB, Workday, and Salesforce.
[^15]: [Testing | AI SDK Core](https://ai-sdk.dev/docs/ai-sdk-core/testing), Vercel AI SDK Docs, fetched July 17, 2026 — current version is AI SDK 7; documented mock classes are `MockLanguageModelV4` and `MockEmbeddingModelV4`, with no mention of a V3 variant.
