Execution Progress
Live progress of a job the agent is running, with controls to pause, cancel, skip, retry or unblock it.
≈ 2,355 tokens per use (tool 1,138 · guidance 245 · typical payload 972)How token costs are estimated
Example: Execution Progress
Migrate the 40 product docs to the new template.
Migrate the 40 product docs to the new template
Goal: Every doc on the new template, links checked, published.
Running7 of 8 steps done, 88%
1 needs attention
Output so far
When to use
- The agent is running a multi-step job right now that takes minutes: migrating docs, researching a list of companies, processing a batch of invoices.
- The user may need to approve, unblock, pause or stop the work while it runs.
When not to use
- The work is already finished. Show the Activity Trail, the record of what the agent did.
- It takes a few seconds. Do it and answer.
- The steps are for the user to carry out. Use a Checklist.
How the agent should use it
- Steps, states and outputs come from the runtime, never from model text. Send updated props as they change and keep step
ids stable, so rows animate in place instead of remounting. - Use
waitingonly when the run needs the user, and say what inwaitingFor("Your approval to publish the 38 converted docs"). Waiting steps get a Review button. Useblockedwhen something outside the run stops it ("Site requires login"). - Give failed steps a plain
error, and setcanSkiporcanRetryonly when the runtime can actually do it. - Treat every result as a command. Act on it, then send new props. The component never assumes a command worked.
- Set
cancelNotewhen cancelling leaves finished work in place ("12 docs are already migrated and stay migrated."). Without it, the confirm just says how many steps are done. - Lifecycle: this is live data. While the run is going, set
fetchedAtto the time of the latest update andstaleAfterMs(e.g. 2 minutes), so a run that stops reporting shows "may have changed" instead of a frozen spinner. Drop both oncerunStateiscompleted,cancelledorfailed: a finished run is a record. NoexpiresAt. Supersede the card when a new run of the same job replaces it, such as a restart after a cancel.
Intervening
| Control | What happens |
|---|---|
| Pause / Resume | Sends pause or resume. The button shows "Pausing…" until the props report the new runState |
| Cancel | Asks first, inline: "Cancel the run? …" with Keep running and Cancel run. Focus moves to Keep running, and back to Cancel after |
| Retry step / Skip step | On failed or blocked steps that allow it. Shows "Retrying…" or "Skipping…" until the step's state changes |
| Review | On waiting steps. Sends open-waiting with the step id; the host opens whatever the step waits for |
A command the runtime never reflects stops blocking its button after 10 seconds, so the card never locks. When the run is completed, cancelled or failed, the controls go away and a summary takes their place: "Completed in 6 min · 38 done, 2 skipped".
Live updates. Steps appear and change state with motion (icon cross-fade, rows sliding into place, bars easing), and reduced motion is respected. Local UI state, such as which substeps are open, survives every update. Past 6 steps the list scrolls inside the card, and the running step is kept in view within that box only. The page never scrolls, and the list holds still for a few seconds after the user scrolls it.
Layout. The header holds the title, the run state as a badge (dropped once a finished run's summary takes the footer), the count done in large type with "needs attention" and the time left (or elapsed) beside it, and the overall bar. Only live and stuck steps (running, waiting, blocked, failed) show their detail and substeps; finished and pending steps are one line, with their detail, duration and substep count read to screen readers. The goal is read to screen readers only. Outputs follow the steps.
Substeps are the one exception to "expanded by default". Only the running step's substeps are open; the other live steps start collapsed, because long runs would otherwise bury the current step. The user can open or close any of them.
Screen readers hear steps that start needing them ("Needs you: Publish to docs.acme.example. Your approval to publish the 38 converted docs."), failures, pause and resume, and the final summary. Progress ticks aren't announced; the overall bar has a text equivalent ("12 of 30 steps done, 40%").
Installation
npx shadcn@latest add https://your-site.com/r/execution-progress.json
Usage
The tool starts the job; the runtime then streams updates to the same card. With the AI SDK, write each update as a data part with the run's id, so it replaces the previous one:
import { createUIMessageStream, tool } from "ai"
import { executionProgressTool } from "@/components/genui/execution-progress.schema"
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const tools = {
[executionProgressTool.name]: tool({
description: executionProgressTool.description,
inputSchema: executionProgressTool.inputSchema,
execute: async (input) => {
const run = await jobs.start(input)
// Same id on every write: the card updates in place.
run.onUpdate((props) =>
writer.write({ type: "data-progress", id: run.id, data: props }),
)
return { runId: run.id }
},
}),
}
// …streamText({ model, messages, tools }) and merge it into the writer
},
})import { ExecutionProgress } from "@/components/genui/execution-progress"
if (part.type === "data-progress") {
return (
<ExecutionProgress
{...part.data}
onResult={(command) =>
fetch(`/api/runs/${part.id}/commands`, {
method: "POST",
body: JSON.stringify(command),
})
}
/>
)
}The runtime receives commands, one at a time:
{ action: "pause" }
{ action: "resume" }
{ action: "cancel" }
{ action: "retry", stepId: "harbor" }
{ action: "skip", stepId: "scanned" }
{ action: "open-waiting", stepId: "publish" }With LangGraph
Push it from a graph node with ui.push({ name: "show_progress", props }, { message }), then push again with the same UI message id as the run moves on (with merge: true for partial updates). Render the thread's UI messages with the Renderer, and send commands back to the graph as a resume or a new run input.
Restoring from history
The props are the truth. On reload, render the latest snapshot the runtime stored. A finished run renders as its summary; for a run still going, fetch the current snapshot.
result takes the last command if it may not have landed yet. It shows as pending ("Pausing…") until the props reflect it, and is ignored when they already do. It never calls onResult.
<ExecutionProgress {...latestSnapshot} result={lastCommand} onResult={sendCommand} />Schema
| Prop | Type | Notes |
|---|---|---|
title | string | The job: "Migrate the 40 product docs to the new template" |
goal | string? | What done looks like. Read to screen readers |
steps | ExecutionStep[] | 1–60. Data-bound |
runState | "running" | "paused" | "stopping" | "cancelled" | "completed" | "failed" | Data-bound |
startedAt | string? | ISO. Drives the elapsed clock and the summary's duration |
overall | { progress?, etaSeconds? }? | Data-bound. Without progress, it's derived: done and skipped steps count fully, running steps add their own progress |
controls | ("pause" | "resume" | "cancel" | "skip" | "retry")[]? | Default: pause, resume and cancel, plus skip and retry where steps allow. When set, it's the full list |
cancelNote | string? | What cancelling leaves behind, shown in the confirm |
outputs | { label, url? }[]? | Data-bound. Results so far, up to 20. http(s) links only |
ExecutionStep
| Field | Type | Notes |
|---|---|---|
id | string | Stable. Returned with step commands |
label | string | "Convert batch 2" |
state | "pending" | "running" | "waiting" | "blocked" | "done" | "failed" | "skipped" | waiting is on the user; blocked is on something else |
detail | string? | "12 of 40 docs" |
progress | number? | 0–1, for running steps |
startedAt, finishedAt | string? | ISO. A finished step's duration is read to screen readers |
waitingFor | string? | For waiting and blocked steps |
error | string? | For failed steps; the reason for blocked or skipped ones |
canSkip, canRetry | boolean? | Offer the buttons on failed or blocked steps |
substeps | { label, state }[]? | Up to 10 |
Host props: status (streaming or ready), onResult, result and className.