# Durable execution
Your agent turn survives a mid-flight process crash. A fresh process picks up the in-flight run and retries the failed step under the same stable identifier, so the retry replaces the failed attempt's output on the session.
An agent turn is often more than one HTTP request's worth of work. An LLM call, a tool execution, a follow-up call, sometimes a suspend and a later resume. Each stage can fail: a serverless cold start, a container redeploy, a spot instance eviction. Durable execution keeps the whole turn safe by pushing each stage into a workflow engine's activity and using AI Transport's steps to keep the session clean.
You keep everything [streaming](https://ably.com/docs/ai-transport/streaming.md) gives you, and gain one thing on top. A retried activity publishes under the same step id, so its output replaces the failed attempt on the channel instead of appearing beside it.
AI Transport works alongside any durable execution engine that gives each retryable activity a stable identifier, including [Temporal](https://ably.com/docs/ai-transport/durable-execution/temporal.md), [Vercel WDK](https://ably.com/docs/ai-transport/durable-execution/vercel-wdk.md), Inngest, and trigger.dev.

## How it works
Each retryable step or activity in the workflow engine maps to an SDK [step](https://ably.com/docs/ai-transport/streaming/runs-and-steps.md#steps) with a `stepId`. When the workflow engine retries a failed activity under the same `stepId`, the SDK supersedes the previous attempt's partial output and the conversation history stays clean.
On the agent, to create a step inside a run, the SDK needs two identifiers: the `runId` that identifies the run, and the `invocationId` that identifies the invocation which triggered the turn. It also needs the invocation itself. These identifiers allow different retryable activities, scheduled across different durable-execution workers, to contribute steps to an existing run, even if that run was started in a different activity:
### Javascript
```
import * as Ably from 'ably';
import { createAgentSession, Invocation } from '@ably/ai-transport';
import { createUIMessageSessionCodec } from '@ably/ai-transport/vercel';
async function runToolStep({ invocation, runId, invocationId, activityId, toolCall }) {
const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY });
const session = createAgentSession({
client: ably,
channelName: 'conversation-42',
codec: createUIMessageSessionCodec(),
});
await session.connect();
const run = session.adoptRun(Invocation.fromJSON(invocation), { runId, invocationId });
await run.load();
const output = await runYourTool(toolCall);
const step = run.createStep({ stepId: activityId });
await step.start();
await step.send({ type: 'tool-output-available', toolCallId: toolCall.id, output });
await step.end();
await session.detach();
ably.close();
}
```
`load` completes the adoption. `adoptRun` already registered the run for cancel routing, so a cancel published for this run fires `run.abortSignal` whether or not `load` has returned. `load` confirms the run is still open on the session. It rejects if the run has already been suspended or ended, because a run in either state can no longer accept new steps.
## Retry a step under a stable stepId
For a retry to supersede the failed attempt, both attempts must publish under the same `stepId`. Pass the workflow engine's own per-activity id as the `stepId` option to [`run.createStep()`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#create-step). Any id that the engine keeps stable across retries of the same activity is a valid source. In Temporal, that is the activity id. In [Vercel WDK](https://ably.com/docs/ai-transport/durable-execution/vercel-wdk.md), it is `getStepMetadata().stepId`.
Inside a single process, leave `stepId` out. The SDK picks one for you and reuses it if the previous step ended `failed`, so an in-process retry supersedes without any extra bookkeeping.
## Close the run only once
Every activity ends by detaching from the channel with [`session.detach()`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#detach). `session.detach()` unsubscribes and drops the channel without publishing anything, so the run stays open on the session for the next activity to adopt.
Exactly one process publishes the run's terminal event. On the happy path that is the activity holding the final step: it calls `run.end(...)` (or `run.suspend()` if the run is waiting on external input) before it detaches. Account for the failure path as well: if a turn exhausts its retries with the run still open, nothing has ended it and every observer's UI stays stuck on `streaming`. Your failure path needs to adopt the run and call `run.end({ reason: 'error' })` so every observer sees the run end.
Do not call [`session.end()`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#session-end) inside a still-running workflow. `session.end()` closes every open run this session owns as `'cancelled'`, which is the wrong outcome for a run whose next activity has not run yet. `session.end()` belongs to the final teardown of a turn that runs in a single process rather than one driven across workflow-engine activities.
## Route cancel messages through the session
Cancel messages arrive over the session rather than through a workflow signal. Every `AgentSession` subscribes on connect, so a cancel published by the client fires [`run.abortSignal`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#run) inside the activity that currently holds the run. Pass that signal into the LLM call as its `abortSignal` and the model call stops with the step ending `'cancelled'`.
Each activity constructs its own `Ably.Realtime` client and its own `AgentSession`. Sharing one `AgentSession` instance across activities is not supported; each activity should connect to the session through its own `AgentSession`. Sharing a Realtime client is only possible when activities run in the same process; workflow engines that isolate each activity in its own process, such as Temporal and Vercel WDK, give each activity a fresh client regardless. Where a client is shared, no activity may close a channel that another run still uses.
## Where durable execution sits
Runs and steps are part of the transport. They mark where a turn and each of its attempts start and end on a channel, and they pick which attempt's output counts. A durable agent that keeps its own conversation store can use runs and steps through the streaming layer's [`AgentTransport`](https://ably.com/docs/ai-transport/api/javascript/transport/agent-transport.md).
The integrations that ship today go further than that. Both the [Temporal](https://ably.com/docs/ai-transport/durable-execution/temporal.md) and [Vercel WDK](https://ably.com/docs/ai-transport/durable-execution/vercel-wdk.md) guides build their activities on `createAgentSession` with a full codec bound, and each guide's inference activity rebuilds the model's context by draining `run.view`. So both are [durable session](https://ably.com/docs/ai-transport/durable-sessions.md) integrations, and an application adopting either takes the conversation tree with it.
## Edge cases
- Cross-process retry with a fresh `stepId` appends instead of superseding. Two attempts persist in the conversation. Always source `stepId` from the workflow engine's stable per-activity id.
- `adoptRun` itself publishes nothing and rejects nothing; it returns an `AgentRun` synchronously. `load()` is what gates on the run's state, and it throws `InvalidArgument` for a suspended run, where you resume via `createRun().start()` with the continuation input event, and for a terminal run, where there is nothing left to publish because the run is already closed on the wire.
- `load` times out if the run's `ai-run-start` is not observed within 30 seconds (`options.timeoutMs`). This is a workflow-ordering error: the adopting activity ran before the activity that opened the run published it. Retry with backoff, or raise the timeout when history pages back a long way.
- A cancel fires `run.abortSignal` as soon as it arrives, because `adoptRun` registered the run for cancel routing. A cancel that lands before `load()`, or during its wait for the run-start, also makes `load()` reject with `OperationCancelled`, so treat that rejection as the cancel taking effect.
- A turn that exhausts its retries with the run still open leaves every observer's UI on `streaming`. Your failure path must adopt the run and publish `run.end({ reason: 'error' })` so every client sees the run end.
- A retry supersedes the previous attempt as soon as it calls `step.start()`, because supersede keys on the fresh, higher-serial `ai-step-start` published under the same `stepId` rather than on any output the retry later produces. A retry that starts and then emits nothing still replaces the earlier attempt's output with an empty step.
- `session.end()` on a still-active run publishes `ai-run-end` with reason `'cancelled'`. Reserve it for teardown rather than mid-workflow handoff.
## FAQ
### Does Ably retry steps automatically?
No. The workflow engine retries. AI Transport's role is to keep the session clean when the retry lands: the stable `stepId` causes the retry's `ai-step-start` to supersede the failed attempt.
### Which durable execution engines can I use?
Any engine that gives each retryable unit of work an id it keeps stable across retries of that unit and unique across different units. AI Transport passes that id to [`createStep({ stepId })`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#create-step), so a retry publishes under the same `stepId` and supersedes the failed attempt. What differs per engine is where that id comes from:
- Temporal: the activity id. The [`stepIdFor`](https://ably.com/docs/ai-transport/api/javascript/temporal.md) helper reads it from `Context.current().info.activityId` and prefixes it with the run's invocation id. Temporal is the reference integration, with a [framework page](https://ably.com/docs/ai-transport/durable-execution/temporal.md).
- Vercel WDK: `getStepMetadata().stepId`, already stable across retries and unique across workflow runs, so it passes straight to `createStep` with no helper. The [Vercel WDK guide](https://ably.com/docs/ai-transport/durable-execution/vercel-wdk.md) shows the integration.
- Inngest and trigger.dev: the stable id each exposes for a step or task. Dedicated helpers are [on the roadmap](https://ably.com/docs/ai-transport/roadmap.md); until then, read that id and pass it to `createStep({ stepId })` yourself.
The SDK supersedes on the `stepId` alone, so where an engine has no retry-stable id you must derive a stable one yourself, or the retry double-publishes.
### What happens if a step throws before `step.start()`?
Nothing is published on the session. `createStep` generates an id but does no I/O. `start()` is what emits `ai-step-start`. A throw before `start()` leaves the run unchanged; the retry starts clean.
### Can two steps run in parallel on one run?
No. Exactly one step at a time on a given run; `step.start()` rejects if another step is still open. Parallel work runs as [concurrent runs](https://ably.com/docs/ai-transport/streaming/concurrent-runs.md) on the same session instead.
### How is `createStep` different from `run.pipe`?
[`AgentRun.pipe`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#pipe) opens an implicit step lazily at the first output, streams, and closes on stream end. Each `pipe` call is a fresh step with a fresh id; retries never supersede. Use `pipe` when a turn runs in a single process. Use `createStep({ stepId })` when it runs across workflow-engine activities, or for any case where a re-attempt must replace the failed attempt's output.
## Related features
- [Reconnection and recovery](https://ably.com/docs/ai-transport/streaming/reconnection-and-recovery.md): what the client side does when a connection drops. Durable execution is the agent-side counterpart.
- [Tool calling](https://ably.com/docs/ai-transport/durable-sessions/tool-calling.md): each tool becomes its own step under a workflow engine.
- [Concurrent runs](https://ably.com/docs/ai-transport/streaming/concurrent-runs.md): parallel runs on the same session.
## Related Topics
- [Temporal](https://ably.com/docs/ai-transport/durable-execution/temporal.md): Run an AI Transport agent inside a Temporal workflow. One activity per step, activity ids as step ids, a retry that supersedes the failed attempt on the session, and cancel messages routed over the channel.
- [Vercel WDK](https://ably.com/docs/ai-transport/durable-execution/vercel-wdk.md): Run an AI Transport agent as a Vercel Workflow inside your Next.js app. Each model call and tool is its own retryable WDK step, a retry supersedes the failed attempt on the session, and cancel messages route over the channel.
## Documentation Index
To discover additional Ably documentation:
1. Fetch [llms.txt](https://ably.com/llms.txt) for the canonical list of available pages.
2. Identify relevant URLs from that index.
3. Fetch target pages as needed.
Avoid using assumed or outdated documentation paths.