Skip to content

Background execution

Background execution lets a client submit work, disconnect, and return later. It requires server-managed state because the server needs a snapshot to track progress, liveness, completion, failure, and cancellation.

Use background execution for work that may outlive the request or the browser tab, such as report generation, long research tasks, multi-step planning, or tool-heavy workflows. Keep normal foreground streaming for short turns where the user is actively waiting and foreground cancellation is enough.

Store support matters for background work. See Session stores for which stores support snapshot status changes and aborting detached work.

Configure a store and expose companion endpoints when using a remote client:

const reportAgent = ai.defineAgent({
name: 'reportAgent',
system: 'Create detailed research reports.',
store,
});
app.post('/api/reportAgent', expressHandler(reportAgent));
app.post(
'/api/reportAgent/getSnapshot',
expressHandler(reportAgent.getSnapshotDataAction),
);
app.post(
'/api/reportAgent/abort',
expressHandler(reportAgent.abortAgentAction),
);

The runtime writes a pending snapshot and refreshes its heartbeat while work runs. If the heartbeat becomes stale, reads surface the snapshot as expired.

detach() submits a turn with detach: true. It returns after the server accepts the background work.

const chat = reportAgent.chat({ sessionId: 'report-123' });
const task = await chat.detach('Write the quarterly market report.');
savePendingSnapshot(task.snapshotId);

The chat updates its snapshotId to the pending snapshot ID. Store that ID so another process or browser session can inspect or abort the task.

poll() yields snapshots until the task reaches a terminal status.

for await (const snapshot of task.poll({ intervalMs: 1000 })) {
renderStatus(snapshot.status);
if (snapshot.status === 'completed') {
renderMessages(snapshot.state.messages);
}
}

Use wait() when the caller can block:

const finalSnapshot = await task.wait({ intervalMs: 1000 });
if (finalSnapshot.status === 'failed') {
showError(finalSnapshot.error);
}

Terminal statuses are completed, failed, aborted, and expired.

Use poll() for UI progress because it lets you render every status change. Use wait() for server code, tests, or short-lived command-line tools where blocking is acceptable. Store the pending snapshot ID before navigating away from the page so another client session can reconnect.

If the process that started the task no longer has the DetachedTask, read the stored snapshot ID and resume from it:

const snapshot = await reportAgent.getSnapshot({ snapshotId });
if (snapshot?.status === 'completed') {
const chat = await reportAgent.loadChat({ snapshotId });
await chat.send('Summarize the report in three bullets.');
}

Only completed snapshots can be resumed.

await task.abort();

Or abort directly from the agent:

await reportAgent.abort(snapshotId);

Abort flips a pending snapshot to aborted. The background worker observes the status change and cancels the work.