Execution Progress

Live progress of a job the agent is running, with controls to pause, cancel, skip, retry or unblock it.

Pattern

≈ 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.

Running

7 of 8 steps done, 88%

1 needs attention

  1. Done: Inventory the product docs. 40 docs found. Took 34s
  2. Done: Convert batch 1. 10 of 10 docs. Took 1m 25s
    10 of 10 substeps done
  3. Done: Convert batch 2. 10 of 10 docs. Took 1m 24s
    10 of 10 substeps done
  4. Done: Convert batch 3. 10 of 10 docs. Took 2m 5s
    10 of 10 substeps done
  5. Done: Convert batch 4. 8 of 8 docs. Took 1m 11s
    8 of 8 substeps done
  6. Skipped: Convert the 2 scanned spec sheets. 2 PDFs. Took 13s

    Scanned images, no text to extract.

  7. Done: Check links. 214 links checked, 0 broken. Took 1m 7s
    3 of 3 substeps done
  8. Waiting for you: Publish to docs.acme.example
    38 docs ready

    Approve publishing the 38 docs

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 waiting only when the run needs the user, and say what in waitingFor ("Your approval to publish the 38 converted docs"). Waiting steps get a Review button. Use blocked when something outside the run stops it ("Site requires login").
  • Give failed steps a plain error, and set canSkip or canRetry only 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 cancelNote when 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 fetchedAt to the time of the latest update and staleAfterMs (e.g. 2 minutes), so a run that stops reporting shows "may have changed" instead of a frozen spinner. Drop both once runState is completed, cancelled or failed: a finished run is a record. No expiresAt. Supersede the card when a new run of the same job replaces it, such as a restart after a cancel.

Intervening

ControlWhat happens
Pause / ResumeSends pause or resume. The button shows "Pausing…" until the props report the new runState
CancelAsks first, inline: "Cancel the run? …" with Keep running and Cancel run. Focus moves to Keep running, and back to Cancel after
Retry step / Skip stepOn failed or blocked steps that allow it. Shows "Retrying…" or "Skipping…" until the step's state changes
ReviewOn 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:

app/api/chat/route.ts
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
  },
})
components/message.tsx
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

PropTypeNotes
titlestringThe job: "Migrate the 40 product docs to the new template"
goalstring?What done looks like. Read to screen readers
stepsExecutionStep[]1–60. Data-bound
runState"running" | "paused" | "stopping" | "cancelled" | "completed" | "failed"Data-bound
startedAtstring?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
cancelNotestring?What cancelling leaves behind, shown in the confirm
outputs{ label, url? }[]?Data-bound. Results so far, up to 20. http(s) links only

ExecutionStep

FieldTypeNotes
idstringStable. Returned with step commands
labelstring"Convert batch 2"
state"pending" | "running" | "waiting" | "blocked" | "done" | "failed" | "skipped"waiting is on the user; blocked is on something else
detailstring?"12 of 40 docs"
progressnumber?0–1, for running steps
startedAt, finishedAtstring?ISO. A finished step's duration is read to screen readers
waitingForstring?For waiting and blocked steps
errorstring?For failed steps; the reason for blocked or skipped ones
canSkip, canRetryboolean?Offer the buttons on failed or blocked steps
substeps{ label, state }[]?Up to 10

Host props: status (streaming or ready), onResult, result and className.

Source

On this page