Mastra Durable Agents: A 2026 TypeScript Tutorial
July 17, 2026

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 connection1. 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.02) 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.04. 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 itsrunId,output, andcleanup() - 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(), andcreateInngestAgent() - 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 Gatsby5. 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 framework67.
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 for6. npm's own download-stats API put @mastra/core at 937,152 downloads for the week of May 19-25, 20268, 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 source9.
Companies that have publicly described using Mastra include Replit, SoftBank, PayPal, and Adobe as of the October 2025 announcement6, and Brex, Sanity, Factorial, Indeed, MongoDB, Workday, and Salesforce as of the April 2026 Series A post7.
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 over1.
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 delivery1.
The feature is in beta as of this writing; Mastra's own docs carry an explicit warning that "APIs may change in future releases"10, 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.
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.03 and accepts zod@^3.25.0 or ^4.0.0 as a peer dependency4. Add a tsconfig.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:
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()11.
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."
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:
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:
// 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 purpose12 (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 post13).
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 runs111:
| 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 rewrite11.
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 version14.
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/redis1:
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)11. 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 tutorial15. 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 part1.
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.04; 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) for the equivalent no-API-key testing pattern in a different framework, Add Human-in-the-Loop Approval to Claude Agent SDK (2026) for a deeper look at approval gates, and Claude Tool Use in TypeScript: Agentic Loop Tutorial (2026) if you want to see the raw agentic loop a framework like Mastra is abstracting over.
Footnotes
-
Durable agents | Long-running agents | Mastra Docs, fetched July 17, 2026. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Durable agents | Long-running agents | Mastra Docs — "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.) ↩ ↩2 -
npm view @mastra/core engines --jsonagainst the npm registry, run July 17, 2026 —{"node": ">=22.13.0"}. ↩ ↩2 -
npm view @mastra/core dist-tags --jsonandnpm view @mastra/core time --jsonagainst the npm registry, run July 17, 2026 —latestis1.51.0, published 2026-07-15. ↩ ↩2 ↩3 -
GitHub - mastra-ai/mastra, fetched July 17, 2026. ↩
-
Announcing our $13m seed round from YC, pg, Gradient, Amjad, Guillermo, Balaji, and 120+ others, Mastra Blog, published October 8, 2025, fetched July 17, 2026. ↩ ↩2 ↩3
-
Mastra raises $22M Series A led by Spark Capital, 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. ↩ ↩2
-
npm downloads API, queried July 17, 2026 — returned 937,152 downloads for the week of 2026-05-19 to 2026-05-25. ↩
-
GitHub - mastra-ai/mastra, 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). ↩
-
Durable agents | Long-running agents | Mastra Docs — beta warning banner, fetched July 17, 2026. ↩ ↩2
-
Reference: DurableAgent | Agents | Mastra Docs, fetched July 17, 2026. ↩ ↩2 ↩3 ↩4 ↩5
-
Test Vercel AI SDK Code with MockLanguageModelV3 (2026), NerdLevelTech. ↩
-
Testing | AI SDK Core, Vercel AI SDK Docs, fetched July 17, 2026 — current version is AI SDK 7; documented mock classes are
MockLanguageModelV4andMockEmbeddingModelV4, with no mention of a V3 variant. ↩ -
Add Human-in-the-Loop Approval to Claude Agent SDK (2026), NerdLevelTech — documents the same
skipLibCheckrequirement against a shipped Claude Agent SDK version. ↩ -
Add Human-in-the-Loop Approval to Claude Agent SDK (2026), NerdLevelTech. ↩



