Temporal workflow
@ably/ai-transport/temporal/workflow provides helpers to manage runs from inside a Temporal workflow. You can interact with runs, adopt open runs, and suspend, end and clean up runs when you're done.
1
import { openRun, withRun } from '@ably/ai-transport/temporal/workflow';Every call here schedules one of the framing activities that createAblyTransportPlugin registers on your worker. The Ably client and the session both live on the worker side, which is why Temporal's workflow sandbox can load this module. Register the plugin on your worker before a workflow calls anything here, or the activities will not resolve.
The subpath declares @temporalio/workflow as an optional peer dependency. Install it alongside the SDK.
Open a run and close it on failure
withRun<T>(invocation: InvocationData, body: (run: RunHandle) => Promise<T>): Promise<T>withRun<T>(invocation: InvocationData, options: OpenRunOptions, body: (run: RunHandle) => Promise<T>): Promise<T>Open a run, run body against it, and make a best-effort attempt to close the run if body throws. An unclosed run leaves every subscribed client waiting on a stream that never ends, so withRun is the entry point to reach for by default.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { proxyActivities } from '@temporalio/workflow';
import { withRun } from '@ably/ai-transport/temporal/workflow';
import type * as activities from './activities.js';
const { runInferenceStep, runToolStep } = proxyActivities<typeof activities>({
startToCloseTimeout: '5 minutes',
retry: { maximumAttempts: 3 },
});
export async function chatWorkflow(input) {
await withRun(input.invocation, async (run) => {
let outcome = await runInferenceStep({ ids: run.ids, invocation: input.invocation });
while (outcome.kind === 'server-tools') {
for (const toolCall of outcome.toolCalls) {
await runToolStep({ ids: run.ids, invocation: input.invocation, toolCall });
}
outcome = await runInferenceStep({ ids: run.ids, invocation: input.invocation });
}
});
}The cleanup runs in a non-cancellable scope, so a cancelled or terminated workflow still closes its run, and cleanup's own failure is swallowed so body's error reaches Temporal unmasked.
Cleanup is best-effort by design, and there are three cases where nothing is published:
- Cleanup gets one attempt with a short timeout. Retrying would let a hanging cleanup hold up a terminate.
- It no-ops when the run has already finished, or is parked suspended.
- It only fires on a throw. A
bodythat returns without publishing a terminal leaves the run open.
On success withRun publishes nothing. Your application publishes its own terminal, and doing that inside an activity that already has the run loaded costs no extra adopt or load.
Parameters
invocationrequiredInvocationDataoptionsoptionalOpenRunOptionsOpenRunOptions.bodyrequired(run: RunHandle) => Promise<T>RunHandle.Returns
Promise<T>, resolving to whatever body returns. Rejects with body's error when body throws.
Open a run
openRun(invocation: InvocationData, options?: OpenRunOptions): Promise<RunHandle>Create the run, locate its trigger event, publish the opening event, and return a handle. Use this when the workflow needs the handle outside a single lexical scope. You are then responsible for the failure-path cleanup, through run.cleanup.
"Open" covers two cases. A fresh turn creates a run and publishes ai-run-start. A continuation resumes the run its trigger names and publishes ai-run-resume, so opening does not always mean a new run. The SDK tells the two apart from the trigger's run-id header, which is also why the run-id pinning below applies only to a fresh run.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { openRun } from '@ably/ai-transport/temporal/workflow';
export async function chatWorkflow(input) {
const run = await openRun(input.invocation, {
activityOptions: {
default: { startToCloseTimeout: '5 minutes' },
},
});
try {
await runInferenceStep({ ids: run.ids, invocation: input.invocation });
await run.end({ reason: 'complete' });
} catch (error) {
await run.cleanup(error instanceof Error ? error.message : 'workflow failed');
throw error;
}
}Parameters
invocationrequiredInvocationDataoptionsoptionalOpenRunOptionsOpenRunOptions.Returns
Promise<RunHandle>. See RunHandle.
OpenRunOptions
invocationIdoptionalStringactivityOptionsoptionalRunActivityOptionsRunActivityOptions.RunActivityOptions
Temporal ActivityOptions per framing activity. Each one is merged over default, which is in turn merged over the SDK's own defaults.
defaultoptionalActivityOptionsopenRunoptionalActivityOptionsendRunoptionalActivityOptionssuspendRunoptionalActivityOptionscleanupRunoptionalActivityOptionsThe SDK's own defaults:
| Activity | startToCloseTimeout | retry.maximumAttempts |
|---|---|---|
openRun | 2 minutes | 3 |
endRun | 2 minutes | 3 |
suspendRun | 2 minutes | 3 |
cleanupRun | 30 seconds | 1 |
cleanupRun gets one attempt and a tight timeout so its own failure cannot hold up a terminate. Raise openRun's startToCloseTimeout for a long conversation, because openRun pages channel history to locate the trigger event.
RunHandle
A handle on an open run, held in workflow state. It carries plain data plus calls that schedule activities, and it never holds a live Ably session.
idsRunIdentityrunId and invocationId. Thread it through your own activities so they can adopt the run.end(params) => Promise<void>{ reason, errorMessage? }, where reason is one of complete, cancelled, error, and errorMessage is used only when reason is error.suspend() => Promise<void>ai-run-suspend. Fails if a step is still open, because suspending mid-step would strand the step bracket.cleanup(errorMessage?: string) => Promise<void>error so a waiting client unsticks. Takes an optional errorMessage. Does nothing if the run already finished or is parked suspended.Read next
- Temporal worker:
createAblyTransportPluginandstepIdFor. - Get started with Temporal: the workflow and worker built out in full inside a Next.js chat app.
- Runs: runs, steps, and invocations.