# Steps A Step is a bracket around one publishable unit of agent output inside a Run. It has its own start and end lifecycle on the channel, an addressable stepId, and a terminal reason of complete, failed, or cancelled. A [Run](https://ably.com/docs/ai-transport/concepts/runs.md) does not publish output directly. It publishes through Steps. A Step is the inner bracket a Run uses to group one contiguous unit of output: the tokens for one LLM inference, the payload for one tool result, or any other burst of channel writes the agent code frames as a single unit. Every output message an agent publishes carries a `step-id` that names the Step it belongs to. A Step has its own lifecycle events on the channel (`ai-step-start` and `ai-step-end`), its own addressable identity (`stepId`), and its own terminal reason (`'complete'`, `'failed'`, or `'cancelled'`). It is to the Run what the Run is to the Session: a nested bracket with a stable identity and a defined start and end, carrying a portion of the outer bracket's output. Two properties of the Step layer sit on top of that structural role. Every Step in a Run is ordered relative to the others (exactly one Step is active on a Run at a time). And two `ai-step-start` events under the same `stepId` coalesce: the later one supersedes the earlier one's output rather than appending beside it, which is what makes a Step the safe boundary for a retry. ![Diagram of a Run on the channel between an agent that publishes and a client that subscribes. The Run contains three Steps: Step s1 attempt A ends failed, Step s1 attempt B retries and supersedes attempt A to end complete, then Step s2 publishes a tool result and ends complete, before the Run ends complete.](https://raw.githubusercontent.com/ably/docs/main/src/images/content/diagrams/ait-concepts-steps.png) ## Why Steps exist A Run is rarely one continuous stream. A single turn typically produces several discrete pieces of output: the tokens of an initial LLM response, the payload of a tool result, the tokens of a follow-up LLM response, and so on. The channel needs to represent each of those as a distinct, addressable unit rather than an undifferentiated slab of `ai-output` messages between `ai-run-start` and `ai-run-end`. The Step is the SDK's answer. Every output message carries a `step-id` naming the Step it was produced in, and every Step is bracketed by `ai-step-start` and `ai-step-end` events so an observer can tell one unit from the next. The Run becomes a container of Steps rather than a container of raw output. That structure is what makes a retry safe. Because a Step has stable identity, two `ai-step-start` events under the same `stepId` are the same Step re-attempting rather than two distinct pieces of output. Between the two attempts, the message that was published later wins, so a retry's output supersedes the failed attempt on the channel rather than appending beside it. Without a sub-Run unit with its own identity, there would be no safe place to draw a retry boundary within a Run. ## Understand the Step model Every Step within a Run has four invariants: - A `stepId` that is stable across retry attempts of the same Step. - A bracketing pair of lifecycle events on the channel: `ai-step-start` and `ai-step-end`. - A terminal `StepEndReason` once it closes: `'complete'`, `'failed'`, or `'cancelled'`. - One Step active at a time on a given Run. The next Step opens after the previous one ends. The Run remains open across Steps. A Step ending is not a Run ending; the agent code drives the Run to [`suspend()`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#run-suspend) or [`end()`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#run-end) after the last Step closes. Two kinds of Steps sit on a Run: - Implicit Steps opened by [`AgentRun.pipe`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#pipe). Each `pipe` call opens its own fresh Step lazily at the first output, stamps the stream, and closes on stream end. Two `pipe` calls produce two independent Steps. Implicit Steps never supersede; each is a fresh Step with a fresh id. - Explicit Steps opened by [`AgentRun.createStep`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#create-step). Returns a `RunStep` handle. Pass `{ stepId }` to make the id stable across cross-process retries and gain the supersede semantics. ## What the Step layer requires | Property | Why it matters | | --- | --- | | Stable stepId | A retry's `ai-step-start` must land under the same `stepId` as the failed attempt. Otherwise the retry's output publishes under a fresh id and both attempts remain in the conversation. Supply the workflow engine's own stable id ([Temporal](https://ably.com/docs/ai-transport/frameworks/temporal.md) activity id, [Vercel WDK](https://ably.com/docs/ai-transport/frameworks/vercel-wdk.md) step id) on cross-process retries. | | Supersede on retry | When two attempts share a `stepId`, the message that was published later wins. The client's View surfaces only the winning attempt. | | One Step at a time | Exactly one Step may be active on a Run. [`RunStep.start`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#step-start) rejects if another Step is still open. | | Terminal auto-close | If agent code forgets to close a Step, [`AgentRun.end`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#run-end) auto-closes the open Step before publishing the Run terminal. No observer's UI is stranded on `streaming`. | | Independent from Run terminal | A Step ending as `'failed'` does not end the Run. The Run stays active for the next Step; the agent code decides whether to retry, suspend, or end the Run itself. | ## Publish Steps across processes The Steps of one Run are not always published from the same process. A workflow engine runs each Step as a separate activity, a retry lands in a fresh invocation, a suspended Run resumes its next Step in a background worker. Steps compose across those boundaries because a Step's output and lifecycle events live on the channel, not in the process that published them. Any process that attaches a session to the channel and enters the Run can publish the next Step. A process publishes its Steps through an [`AgentSession`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md) bound to the channel: construct it with `createAgentSession`, attach with `connect`. Every `ai-step-start`, `ai-step-end`, and `ai-output` inside a Step goes out through that session. The session keeps no state of its own; the open Run and its Steps live on the channel, which is what makes them visible to the next process. A Step always belongs to a Run, so a process must be inside that Run before it can publish one. The process that publishes the first Step opens the Run with `session.createRun(invocation)` and `start()`. A later process enters the already-open Run with `session.adoptRun(...)` and `load()`, which recovers the Run's state from channel history so it can publish further Steps without re-opening the Run. Adoption also routes channel cancels to this process, so a cancel ends the Step it is currently publishing as `'cancelled'`. See [Runs](https://ably.com/docs/ai-transport/concepts/runs.md) for the identifiers this threads across the boundary. Once it has published its Step, the process releases the channel. `session.detach()` releases it without publishing anything, leaving the Run open so the next process can adopt it and publish the next Step. `session.end()` closes any Step and Run this session still holds open as `'cancelled'`, then detaches. A process handling one Step mid-Run uses `detach`; the process that publishes the final Step, and a failure-catch cleanup, uses `end`. See [Detach or end a session](https://ably.com/docs/ai-transport/concepts/sessions.md#detach-vs-end). ## Coalesce retries under a stable stepId When a Run's Steps run across workflow-engine activities, each activity follows the same shape: adopt the Run, create the Step under the workflow's activity id, pipe the LLM stream, close the Step. A retry re-enters the same code with the same activity id, so the retry's Step lands under the same `stepId` and supersedes the earlier attempt. ### Javascript ``` import { createAgentSession } from '@ably/ai-transport'; import { stepIdFor } from '@ably/ai-transport/temporal'; async function runInferenceStep({ runId, invocationId, triggerEventId }) { const session = createAgentSession({ /* ... */ }); await session.connect(); const run = session.adoptRun({ runId, invocationId, triggerEventId }); await run.load(); const step = run.createStep({ stepId: stepIdFor(invocationId) }); await step.start(); await step.pipe(llmStream); await step.end(); await session.detach(); } ``` `stepIdFor(invocationId)` reads the Temporal activity id from the activity context and prefixes it with the Run's invocation id so it stays unique across workflows. Other durable execution frameworks pass their own stable id in the same slot. Omit `stepId` for the common in-process case. The SDK assigns an invocation-scoped id, and an in-process retry after a `'failed'` close reuses that id automatically. ## Read next - [Runs](https://ably.com/docs/ai-transport/concepts/runs.md): the outer bracket a Step lives in. - [Durable execution](https://ably.com/docs/ai-transport/features/durable-execution.md): the pattern that makes cross-process Step retries safe. - [Temporal](https://ably.com/docs/ai-transport/frameworks/temporal.md): the reference workflow-engine integration. - [Vercel WDK](https://ably.com/docs/ai-transport/frameworks/vercel-wdk.md): the same pattern inside your Next.js app. - [RunStep API reference](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#step): the surface `createStep` returns. ## Related Topics - [Overview](https://ably.com/docs/ai-transport/concepts.md): Understand the building blocks of AI Transport: sessions, connections, Runs, Invocations, codecs, the conversation tree, authentication, and infrastructure, and how they relate. - [Sessions](https://ably.com/docs/ai-transport/concepts/sessions.md): Understand sessions in AI Transport: persistent, shared conversation state that exists independently of any participant's connection. - [Connections](https://ably.com/docs/ai-transport/concepts/connections.md): Understand connections in AI Transport: ClientSession and AgentSession instances connect each participant to the shared conversation. Lifecycle, responsibilities, and how multiple participants coexist. - [Runs](https://ably.com/docs/ai-transport/concepts/runs.md): Understand Runs in AI Transport: the SDK primitive that implements a conversation turn with explicit identity, lifecycle, cancel routing, and durable execution semantics. - [Invocations](https://ably.com/docs/ai-transport/concepts/invocations.md): Understand Invocations in AI Transport: the trigger that mediates between a client publishing input and the agent creating a Run. - [Codecs](https://ably.com/docs/ai-transport/concepts/codecs.md): Understand codecs in AI Transport: the translation layer between your AI framework's events and Ably's messages. - [Conversation tree](https://ably.com/docs/ai-transport/concepts/conversation-tree.md): Understand how AI Transport organises messages into a branching conversation tree, and how views provide each participant with their own linear perspective. - [Authentication](https://ably.com/docs/ai-transport/concepts/authentication.md): Understand how authentication works in Ably AI Transport: Ably token auth for channel access, HTTP headers for server endpoints, and cancel authorization. - [Infrastructure](https://ably.com/docs/ai-transport/concepts/infrastructure.md): How Ably's platform provides the integrity, reliability, performance, and availability that a durable AI session layer requires. ## 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.