DurableAgent
DurableAgent wraps an existing Agent with durable execution and resumable streams. It runs the agentic loop so that a client can disconnect and reconnect without missing events, and it streams those events over PubSub. Use it when a run must outlive a single request or survive a dropped connection.
Create one with the createDurableAgent factory, or use createEventedAgent for fire-and-forget execution on the built-in workflow engine. For Inngest-powered execution, use createInngestAgent from @mastra/inngest.
Usage exampleDirect link to Usage example
import { Mastra } from '@mastra/core'
import { Agent } from '@mastra/core/agent'
import { createDurableAgent } from '@mastra/core/agent/durable'
const agent = new Agent({
id: 'my-agent',
name: 'My Agent',
instructions: 'You are a helpful assistant',
model: 'openai/gpt-5.6-sol',
})
const durableAgent = createDurableAgent({ agent })
export const mastra = new Mastra({
agents: { myAgent: durableAgent },
})
Stream a response and read the result. The cleanup function unsubscribes from PubSub when you are done with the run:
const { output, runId, cleanup } = await durableAgent.stream('Hello!')
const text = await output.text
cleanup()
Using the durable config flagDirect link to using-the-durable-config-flag
Set durable: true on AgentConfig and the agent is automatically wrapped with createDurableAgent when it's attached to a Mastra instance. Use an object to forward advanced options such as cache, pubsub, maxSteps, or cleanupTimeoutMs.
import { Mastra } from '@mastra/core'
import { Agent } from '@mastra/core/agent'
const myAgent = new Agent({
id: 'my-agent',
name: 'My Agent',
instructions: 'You are a helpful assistant',
model: 'openai/gpt-5.6-sol',
durable: true, // or: { maxSteps: 10, cleanupTimeoutMs: 60_000 }
})
export const mastra = new Mastra({
agents: { myAgent },
})
mastra.getAgent('myAgent') returns the wrapped DurableAgent. Standalone agents (constructed but never registered on a Mastra instance) don't become durable. The wrapping is applied at registration.
createDurableAgent(options)Direct link to createdurableagentoptions
Wraps an Agent with durable execution and resumable streams. This is the recommended way to create a DurableAgent.
import { createDurableAgent } from '@mastra/core/agent/durable'
const durableAgent = createDurableAgent({ agent })
Returns: DurableAgent
ParametersDirect link to Parameters
agent:
id?:
name?:
cache?:
false to disable caching, which makes streams non-resumable.pubsub?:
maxSteps?:
createEventedAgent(options)Direct link to createeventedagentoptions
Wraps an Agent with fire-and-forget durable execution on the built-in workflow engine. Like createDurableAgent, it returns a result you stream from, but the underlying workflow runs non-blocking (via startAsync) instead of running to completion before the stream is wired up. Use it when you want the run to progress independently of the caller. It doesn't accept id or name overrides.
import { createEventedAgent } from '@mastra/core/agent/durable'
const eventedAgent = createEventedAgent({ agent })
Returns: EventedAgent (a subclass of DurableAgent)
ParametersDirect link to Parameters
agent:
cache?:
false to disable caching.pubsub?:
maxSteps?:
Constructor parametersDirect link to Constructor parameters
The DurableAgent class accepts the same options as createDurableAgent, plus cleanupTimeoutMs. Prefer the factory unless you need to subclass.
agent:
id?:
name?:
cache?:
false to disable caching.pubsub?:
maxSteps?:
cleanupTimeoutMs?:
cleanup() call. Auto-cleanup does not fire on suspended events.MethodsDirect link to Methods
ExecutionDirect link to Execution
stream(messages, options?)Direct link to streammessages-options
Streams a response using durable execution. Returns immediately with a result whose output produces events as the run progresses.
const { output, runId, cleanup } = await durableAgent.stream('Hello!', {
onChunk: chunk => console.log(chunk),
onFinish: result => console.log('done', result),
})
const text = await output.text
cleanup()
Returns: Promise<DurableAgentStreamResult>
resume(runId, resumeData, options?)Direct link to resumerunid-resumedata-options
Resumes a suspended run, for example after a tool approval. Pass the runId from the original stream and the data the run was waiting on. Throws if no registry entry exists for the run.
const { output, cleanup } = await durableAgent.resume(runId, {
approved: true,
})
await output.text
cleanup()
Returns: Promise<DurableAgentStreamResult>
observe(runId, options?)Direct link to observerunid-options
Reconnects to an existing run, replaying cached events before delivering live ones. Use this after a network disconnection. Pass offset to start replay from a known position.
const { output, cleanup } = await durableAgent.observe(runId, {
offset: 0,
onChunk: chunk => console.log(chunk),
})
await output.text
By default observe() waits indefinitely for events. If the process running the run stops unexpectedly, the run stops producing events but never emits a completion event, so the observed stream would wait forever. Pass idleTimeoutMs to bound that wait: after that many milliseconds of silence the stream ends. An optional isAlive check is consulted first. Return true while the run is still being worked on (for example a long-running tool call, or a run paused waiting for human input) to keep waiting. Returning false, or omitting isAlive, ends the stream with an error. A transient throw from isAlive is treated as "still alive", so a momentary check failure never ends a live stream.
const { output } = await durableAgent.observe(runId, {
idleTimeoutMs: 30_000,
isAlive: () => runHeartbeat.isFresh(runId),
})
Ending a run on idle timeout runs the same cleanup as a run that errors (see the warning below), so its cached state is released rather than retained. Both options are opt-in. Omit them for the previous wait-indefinitely behavior.
Returns: Promise<DurableAgentStreamResult>
The cleanup() returned by observe() destroys the run's registry entries and cached events. Only call it when you are done with the run. If the run is suspended and you intend to resume later, don't call cleanup(). Let the auto-cleanup timer handle it after the run finishes or errors. Auto-cleanup doesn't fire on suspended events.
prepare(messages, options?)Direct link to preparemessages-options
Prepares a run for durable execution without starting it. Registers the run in the internal registry and returns the serialized workflow input. Use this when you need to control when and how the workflow is triggered.
const { runId, messageId, workflowInput, threadId, resourceId } = await durableAgent.prepare(
'Summarize the document',
{
memory: { threadId: 'thread-1', resourceId: 'user-1' },
},
)
Returns:
interface PrepareResult {
runId: string
messageId: string
workflowInput: any
registryEntry: object
threadId?: string
resourceId?: string
}
RecoveryDirect link to Recovery
recoverActiveRuns(options?)Direct link to recoveractiverunsoptions
Discovers runs stuck in running status for this agent and re-drives them from the last persisted snapshot. Recovers up to options.limit runs (default: 100). Returns a summary of what was recovered.
const result = await durableAgent.recoverActiveRuns()
// { recovered: [{ runId, status }], succeeded: 2, failed: 0 }
Pass a runId to recover a single known run:
await durableAgent.recoverActiveRuns({ runId: 'run-abc-123' })
Returns:
interface DurableAgentRecoverActiveRunsResult {
recovered: Array<{ runId: string; status: 'success' | 'failed'; error?: Error }>
succeeded: number
failed: number
}
options.runId?:
options.limit?:
options.createdBefore?:
recover(runId, options?)Direct link to recoverrunid-options
Recovers a single run by ID. Returns a streamable result with the same shape as stream(). Use this when you need to observe the recovery stream in real time.
const { output, cleanup } = await durableAgent.recover('run-abc-123', {
onChunk: chunk => console.log(chunk),
onError: ({ error }) => console.error(error),
})
await output.text
cleanup()
Returns: Promise<DurableAgentStreamResult>
Stream optionsDirect link to Stream options
stream() accepts a DurableAgentStreamOptions object. It supports the agent execution options below, plus lifecycle callbacks.
runId?:
resume() or observe().instructions?:
context?:
memory?:
requestContext?:
maxSteps?:
toolsets?:
clientTools?:
toolChoice?:
activeTools?:
modelSettings?:
Authorization, X-Api-Key, and similar) are stripped from the serialized snapshot before it crosses process boundaries.stopWhen?:
maxSteps only.system?:
requireToolApproval?:
true or false to gate all or none, or a function for per-call policy. Function-form policies live on the in-process run registry; cross-process resumes fall back to a true shadow.autoResumeSuspendedTools?:
resume() call.toolCallConcurrency?:
includeRawChunks?:
maxProcessorRetries?:
structuredOutput?:
untilIdle?:
true for the default 5-minute idle timeout, or { maxIdleMs } to customise. Equivalent to the deprecated streamUntilIdle() method. Also supported on resume().disableBackgroundTasks?:
tracingOptions?:
requestContextKeys forwarded to the agent and model spans. Fully JSON-serializable.actor?:
transform?:
transformToolPayload closure lives on the in-process run registry; only the JSON-safe targets shadow is serialized.prepareStep?:
PrepareStepProcessor at the start of every iteration. Closure-only — stored on the in-process run registry. Cross-process resumes lose the hook.isTaskComplete?:
onComplete live on the in-process run registry; the JSON-safe primitives (strategy, timeout, parallel, suppressFeedback, scorerNames) are serialized for cross-process observability.delegation?:
onDelegationStart, onDelegationComplete, messageFilter). Callbacks are baked into the sub-agent tool wrappers at prepare time. Cross-process resumes lose the callbacks.versions?:
abortSignal?:
AbortController, so either source can cancel the run. Cross-process resumes cannot recover the signal — pass a fresh one to resume() if you need post-resume abortability.onChunk?:
onStepFinish?:
onFinish?:
onError?:
onSuspended?:
onAbort?:
abortSignal or result.abort().onIterationComplete?:
messageList, finishReason, and isFinal flag. Observation-only on durable agents: returning continue: false or feedback does not influence the loop.resume() and observe() accept the same lifecycle callbacks (onChunk, onStepFinish, onFinish, onError, onSuspended). observe() also accepts an offset to control where replay starts.
DurableAgentStreamResultDirect link to DurableAgentStreamResult
The object returned by stream(), resume(), observe(), and recover().
interface DurableAgentStreamResult<OUTPUT = undefined> {
output: MastraModelOutput<OUTPUT>
readonly fullStream: ReadableStream<any>
runId: string
threadId?: string
resourceId?: string
cleanup: () => void
abort: () => void
}
output:
output.text for the full text, or consume output.fullStream.fullStream:
output.fullStream.runId:
resume() or observe() to reconnect.threadId?:
resourceId?:
cleanup:
abort:
AbortController. Surfaces as an AbortError inside the durable LLM-execution step and fires the onAbort callback. Safe to call after the run has finished — a no-op in that case.