Temporal workflow
@ably/ai-transport/temporal/workflow provides helpers to manage runs from inside a Temporal workflow. You can open a run, then suspend it, end it, or clean it up through its handle.
1
import { openRun, withRun } from '@ably/ai-transport/temporal/workflow';Every call here schedules one of the run lifecycle 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 terminating the workflow.
- It no-ops when the run has already finished, or is parked suspended.
- It only fires on a throw. A
bodythat returns without publishing the run's terminal event leaves the run open.
On success withRun publishes nothing. Your application publishes the run's terminal event, and an activity that already has the run loaded can publish that event rather than adopting and loading the run again.
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 whose id its trigger carries, 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 run lifecycle 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 terminating the workflow. 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. Pass it to 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 leave that step unclosed.cleanup(errorMessage?: string) => Promise<void>error so a waiting client sees the run finish. 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.