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.
Server requirements
Section titled “Server requirements”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 from a turn
Section titled “Detach from a turn”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 or wait
Section titled “Poll or wait”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.
Reconnect by snapshot ID
Section titled “Reconnect by snapshot ID”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.
Abort work
Section titled “Abort work”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.
Requirements
Section titled “Requirements”Background execution requires a server-managed agent and a store that implements status subscriptions. localstore.FileSessionStore supports this.
Choose background execution when the caller should receive a pending snapshot immediately and let the agent continue on the server. For local command-line tools or services that can keep the connection open, a normal streaming Connect call is often simpler.
import ( aix "github.com/firebase/genkit/go/ai/exp" "github.com/firebase/genkit/go/ai/exp/localstore" genkitx "github.com/firebase/genkit/go/genkit/exp")store, err := localstore.NewFileSessionStore[ReportState]("./.genkit/snapshots/reports")if err != nil { // Fails if the snapshot directory cannot be created or is not writable. log.Fatalf("open report store: %v", err)}
agent := genkitx.DefineAgent(g, "report", aix.InlinePrompt{ ai.WithModelName("googleai/gemini-flash-latest"), ai.WithSystem("Create detailed research reports."), }, aix.WithSessionStore(store),)The default HTTP abort route is /agents/{name}/abort.
Detach locally
Section titled “Detach locally”With a local AgentConnection, send an input that has Detach: true.
conn, err := agent.Connect(ctx)if err != nil { // Connect fails if the init payload is rejected, such as a sessionId on a // client-managed agent. return fmt.Errorf("connect to agent: %w", err)}
if err := conn.Send(&aix.AgentInput{ Message: ai.NewUserTextMessage("Write the quarterly report."), Detach: true,}); err != nil { return fmt.Errorf("send detached input: %w", err)}
out, err := conn.Output()if err != nil { // The detached turn could not be started; a started one resolves in-band. return fmt.Errorf("read detached output: %w", err)}
fmt.Println(out.FinishReason)fmt.Println(out.SnapshotID)A detached output has FinishReason set to detached and SnapshotID set to the pending snapshot.
Wait for completion
Section titled “Wait for completion”When the store implements aix.SnapshotSubscriber, subscribe to status changes and then read the final snapshot.
subscriber := agent.Store().(aix.SnapshotSubscriber)statusCh := subscriber.OnSnapshotStatusChange(ctx, snapshotID)
for status := range statusCh { if status != aix.SnapshotStatusPending { break }}
snap, err := agent.GetSnapshot(ctx, snapshotID)If the store does not support subscriptions, poll agent.GetSnapshot(ctx, snapshotID) until Status leaves pending.
Abort work
Section titled “Abort work”status, err := agent.Abort(ctx, snapshotID)if err != nil { // A non-nil error is an abort the store could not attempt, not a snapshot // that was already terminal (that returns the terminal status, nil). return fmt.Errorf("abort snapshot: %w", err)}
fmt.Println(status)Abort is a no-op for missing snapshots and for snapshots that already reached a terminal status. Client-managed agents return FAILED_PRECONDITION because there is no server snapshot to cancel.