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/waitForSnapshot',
expressHandler(reportAgent.waitForSnapshotAction),
);
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.

The waitForSnapshot route is optional. With it, a remoteAgent client follows a background task in one request that holds until the task settles. Without it, the client falls back to polling getSnapshot. One request holds for at most maxSnapshotWaitMs (set on defineAgent) and then answers with the snapshot as it stands, and the client asks again.

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. The wait happens next to the store, so there is no polling loop on your side:

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

Bound the wait with an abort signal. When it fires, wait() rejects with the signal’s reason and the task keeps running:

const snap = await task.wait({ abortSignal: AbortSignal.timeout(30_000) });

Without a DetachedTask handle, reportAgent.waitForSnapshot(snapshotId) does the same from a snapshot ID. It resolves undefined for a snapshot that does not exist, where task.wait() throws.

Terminal statuses are completed, failed, aborted, and expired. A failed, aborted, or expired task still resolves with its snapshot, so inspect status and error.

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.');
}

completed, failed, and aborted snapshots can be resumed. A failed or aborted snapshot holds the conversation up to the last completed tool round, so chat.send({}) re-runs the turn from there without repeating finished tool calls. See Resume after a failure or a stop. pending and aborting snapshots are rejected until they settle.

To read status without the conversation, for example for a task list, send metadataOnly: true in the body of the getSnapshot route. The response has no state.

await task.abort();

Or abort directly from the agent:

await reportAgent.abort(snapshotId);

Abort flips a pending snapshot to aborting and resolves with the status the snapshot had before, so pending for a task that was still running. The background worker observes the status change, stops, and settles the snapshot as aborted with the state it had committed. task.wait() resolves once it settles, and the aborted snapshot can be resumed.