# Temporal Temporal owns the durability of your agent's execution. AI Transport owns the durability of the conversation the user sees. The two compose without either owning the other's job. [Temporal](https://temporal.io/) runs your agent's loop as a workflow. Each stage of the loop is a Temporal activity that a worker executes and Temporal retries on failure. AI Transport publishes each activity's output as a [Step](https://ably.com/docs/ai-transport/concepts/steps.md) inside a [Run](https://ably.com/docs/ai-transport/concepts/runs.md). When Temporal retries an activity, the retry lands on the channel under the same `stepId` and supersedes the failed attempt. ![Diagram mapping Temporal activities on the left to AI Transport Steps on a single Run on the right. openRun opens the Run with no Step; runInferenceStep publishes Step A; runToolStep fails on attempt 1 and its attempt-2 retry supersedes it under the same stepId B; a follow-up runInferenceStep publishes Step C and ends the Run. Each activity maps to one Step, sharing the activityId as the stepId.](https://raw.githubusercontent.com/ably/docs/main/src/images/content/diagrams/ait-frameworks-temporal.png) ## What Temporal brings | Capability | Description | | --- | --- | | Workflow durability | The workflow's execution history persists in Temporal. A worker crash resumes the workflow on a different worker without losing progress. | | Activity retries | Failed activities retry under configurable policies. The retry gets the same `activityId`, so Ably can supersede its predecessor. | | Cancellation | A workflow cancel propagates as an `AbortSignal` inside each activity, which flows into the LLM call and stops it gracefully. | | Observability | The Temporal Web UI shows activity attempts, retry counts, error traces, and workflow history. Pair it with the channel to see the whole turn. | ## What AI Transport adds | Capability | Description | | --- | --- | | Durable sessions | Tokens flow through an Ably channel that outlives any single connection. A client reconnects and resumes from where it left off. | | Multi-device sync | Every device subscribed to the session sees the same conversation in realtime. | | Bidirectional control | Cancel, steer, and interrupt the agent from any client. No separate control channel. | | Retry supersede | When Temporal retries an activity, [`AgentRun.createStep({ stepId })`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#create-step) causes the retry's channel output to supersede the failed attempt's rather than append beside it. | | Cancel routing on the channel | Cancels published to the Ably channel reach every activity's session directly, so a stop button in the browser aborts the LLM call inside the in-flight activity without a Temporal signal. | | History and replay | Load the full conversation on reconnect, page refresh, or new device join. | ## Where they connect Each activity publishes its output as a single Step, using the Temporal activity id as that Step's `stepId`. That is the join between the two systems: Temporal's retryable unit and AI Transport's supersedable unit share one identifier. Use [`stepIdFor`](https://ably.com/docs/ai-transport/api/javascript/temporal.md) from `@ably/ai-transport/temporal` to derive the `stepId`. It reads the activity id from the Temporal activity context and prefixes it with the Run's invocation id. Two workflows both have `activityId === '1'` for their first activity; the invocation-id prefix keeps their Steps distinct on the channel. An inference activity: ### Javascript ``` import { Context } from '@temporalio/activity'; import Ably from 'ably'; import { streamText, convertToModelMessages, stepCountIs } from 'ai'; import { createAgentSession, vercelRunOutcome } from '@ably/ai-transport/vercel'; import { stepIdFor } from '@ably/ai-transport/temporal'; import { Invocation } from '@ably/ai-transport'; export async function runInferenceStep({ ids, invocation }) { const ably = new Ably.Realtime({ key: process.env.ABLY_API_KEY }); const session = createAgentSession({ client: ably, channelName: invocation.sessionName }); await session.connect(); const run = session.adoptRun( { runId: ids.runId, invocationId: ids.invocationId, triggerEventId: ids.triggerEventId }, { signal: Context.current().cancellationSignal }, ); await run.load(); while (run.view.hasOlder()) await run.view.loadOlder(); const step = run.createStep({ stepId: stepIdFor(ids.invocationId) }); await step.start(); const result = streamText({ model: myModel, messages: await convertToModelMessages(run.view.getMessages().map((m) => m.message)), abortSignal: run.abortSignal, stopWhen: stepCountIs(1), }); const pipeResult = await step.pipe(result.toUIMessageStream()); const outcome = await vercelRunOutcome(pipeResult, result.finishReason); await step.end(); await session.detach(); ably.close(); return outcome; } ``` `stopWhen: stepCountIs(1)` prevents the Vercel AI SDK from running its own multi-step tool loop inside the activity. The workflow drives the loop instead: a first activity opens the Run, then one activity per inference (the first and every follow-up) and one activity per server tool call. This is what makes each unit retryable in isolation. Open the Run in its own activity, separate from the first inference. That activity only calls `createRun` and `run.start()`; it publishes `ai-run-start` and returns the Run's ids without running the model. Two things follow from the split. An inference failure retries the inference alone and never re-opens the Run. And the Run's ids reach the workflow before any inference runs, so if the very first inference exhausts its retries your failure path still has the ids to end the Run rather than leaving it open. Pin the Run id to the Temporal `workflowId` in that opening activity so a fresh-process retry of it re-enters the same Run instead of opening a parallel one; the republished `ai-run-start` folds idempotently. ## Route cancels through the channel Cancels do not need a Temporal signal. A `clientRun.cancel()` in the browser publishes `ai-cancel` on the Ably channel. The activity's own `AgentSession` subscribes to that channel and routes the cancel to [`run.abortSignal`](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md#run) via the SDK's built-in cancel routing: ```text Browser client │ clientRun.cancel() ▼ Ably channel │ ai-cancel published ▼ Activity's AgentSession (subscribes on adoptRun/createRun) │ matches by run-id ▼ run.abortSignal fires │ passed to streamText as abortSignal ▼ LLM call aborts · step.end() run.end({ reason: 'cancelled' }) ``` Because each activity constructs its own session and routes its own cancels, no long-running listener activity is needed for cancels. Temporal-level cancellation (a workflow cancel) still propagates via the activity's `Context.current().cancellationSignal`, which the code above passes into `adoptRun` as the runtime signal. ## Suspend and resume across workflows A [suspend](https://ably.com/docs/ai-transport/features/human-in-the-loop.md) does not resume the current workflow. The suspending activity calls `run.suspend()`, publishes `ai-run-suspend`, and returns. The workflow ends. When the client posts a continuation invocation (a tool result or an approval response), the HTTP handler starts a fresh workflow. That workflow's first activity calls `session.createRun(invocation, { invocationId })` with the new invocation id; the SDK sees the existing `runId` on the continuation input event and publishes `ai-run-resume` rather than `ai-run-start`. `workflowId = invocationId` for every workflow: one workflow per HTTP POST. Continuations are new workflows on the same `runId`, not resumes of the original. ## Scope and trade-offs Temporal is intentionally focused on execution durability. By design, it does not model conversation state or route cancels between clients and the agent. AI Transport adds those without changing how you write workflows and activities. The workflow decides what runs and when; AI Transport decides what appears in the conversation and to whom. Two boundaries to keep in mind: - Durability applies at the Step boundary, not mid-chunk of an LLM stream. Temporal retries the whole activity. The activity's Step opens fresh under the same `stepId` and the retry's output supersedes the failed attempt. - Publish `run.end({ reason: 'error' })` from your workflow's outermost catch when activity retries are truly exhausted. Otherwise the Run stays open on the channel and every observer's UI stays on `streaming`. ## Read next - [Durable execution](https://ably.com/docs/ai-transport/features/durable-execution.md): the framework-agnostic pattern. - [Steps](https://ably.com/docs/ai-transport/concepts/steps.md): the retry unit Temporal activities map to. - [Vercel WDK](https://ably.com/docs/ai-transport/frameworks/vercel-wdk.md): the same composition inside your Next.js app. - [`stepIdFor` API reference](https://ably.com/docs/ai-transport/api/javascript/temporal.md): the helper that produces a Temporal-safe `stepId`. - [AgentSession API reference](https://ably.com/docs/ai-transport/api/javascript/core/agent-session.md): `adoptRun`, `createStep`, `detach`, and `end`. ## Related Topics - [Vercel AI SDK UI](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-ui.md): How Ably AI Transport integrates with Vercel AI SDK UI (@ai-sdk/react) to add durable sessions and multi-device sync to useChat. - [Vercel AI SDK Core](https://ably.com/docs/ai-transport/frameworks/vercel-ai-sdk-core.md): How Ably AI Transport integrates with Vercel AI SDK Core (the `ai` library) on the server. Codec, streamText, and the UI message stream. - [Vercel WDK](https://ably.com/docs/ai-transport/frameworks/vercel-wdk.md): How Ably AI Transport composes with Vercel Workflow Development Kit. Open the Run and each model call as their own WDK steps, WDK step ids as AI Transport Step ids, retries supersede on the channel, and cancels route through Ably rather than workflow signals. ## 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.