Watch a Sume Format run by phase: preparing, running, finalizing
Pass timeline: true to subscribeFormatRun or waitForRun, or call getFormatRunTimeline, to show phases during a long run. It doubles the polling rate.

Pass timeline: true to subscribeFormatRun or to waitForRun for a Format run, and read the phase from snapshot.timeline in the onStatus callback. The phases are preparing, running and finalizing, each with a timestamp and a status. For a run that you do not wait on, call getFormatRunTimeline(runId, { client }) instead.
A phase gives a person something to read while a long run goes on. For a run that takes many minutes, a bare processing label looks stuck, and users cancel or resubmit, which costs money. A phase name and its timestamp usually settles that.
A concrete use is a customer facing status page. Show the phase name and the elapsed time since its timestamp, and keep the wording plain. A person who sees that a run is in the finalizing phase understands that the end is near, and is less likely to cancel it or ask for a refund.
What the timeline adds to status
The status field says processing, and for a run that lasts a quarter of an hour that is not much to tell a user. The timeline answers the question of what the run is doing right now, at the level of the three phases, without exposing anything inside the run.
The timeline is a phase record and not a log stream. The docs say that Sume does not publish agent output, tool calls or sandbox internals, so do not build a UI that promises a play by play. Show the three phases and their times, and nothing else.
Stay honest about what the three phases mean. They are coarse. A run can spend most of its time in running, and the timeline will not split that into smaller steps. Treat it as a progress hint for people, not as a measurement for dashboards.
Defaults and limits
Know these defaults before you turn it on.
Pick the watcher by the situation. Use subscribeFormatRun when your code also submits the run, because it handles the submit and the wait together. Use waitForRun when you already hold a run id, for instance after a restart. Use getFormatRunTimeline when you only want to show a progress strip and some other code owns the wait.
| Option or helper | Default | Note |
|---|---|---|
| subscribeFormatRun timeout | 20 minutes | Sends an automatic UUID idempotency key unless you pass null |
| waitForRun timeout | 10 minutes | family is required |
| waitForRun transient failures | 6 tolerated | Then it throws |
| timeline | false | Doubles the request rate when true |
Render the current phase
The function below turns the snapshot shape from the docs into one display line. It is plain TypeScript, with no SDK import, so you can unit test it with a canned timeline.
The cost of that extra read is real. Each poll reads the run and also reads the timeline, so the request rate doubles. With a read bucket of thousands of requests a minute that is rarely a problem for one watcher, but it adds up for a dashboard that follows many runs at once.
The sample keeps the status and the phase in one line, because that is what most UIs need. If the timeline is missing, for example when you did not pass timeline: true, the function falls back to the status alone, so the same code works with or without the option.
interface PhaseEntry {
at: string;
phase: "preparing" | "running" | "finalizing";
status: string;
duration_ms: number | null;
}
export function phaseLine(status: string, timeline?: PhaseEntry[]): string {
const last = timeline?.at(-1);
if (!last) return `${status}`;
return `${status} - ${last.phase} (${last.status})`;
}
console.log(phaseLine("processing"));
console.log(phaseLine("processing", [
{ at: "2026-10-05T10:00:00Z", phase: "preparing", status: "completed", duration_ms: 4000 },
{ at: "2026-10-05T10:00:04Z", phase: "running", status: "running", duration_ms: null },
]));
Deadlines and timeouts
The deadline logic is worth knowing. The helper checks the deadline before it sleeps, not after, so a caller who asks for a 5 second timeout gets the timeout in 5 seconds, not 5 seconds plus a poll interval. A timeout does not cancel the run. It ends your watching only, so resume with the same run id, and never submit the paid request again.
Do not branch your business logic on a phase. Phases are for display. Decisions belong to the terminal status and the outcome, which are the stable fields, and a phase name added later should not change what your code does with a finished run.
Sources
Related posts
More in Developers
- Webhook before your DB row? Insert, then submit the Sume video
A Sume job webhook can beat your own write of the job id. Insert a pending row keyed by Idempotency-Key first, then upsert by job_id when the event lands.
- Webhook events arrive out of order: Sume sends one event per job
Stripe does not guarantee event order. Sume job webhooks send terminal events only, so key on job_id, dedupe, and poll status when a callback never arrives.
- Webhook receiver that queues finished SKU videos for human approval
Verify the format.run.terminal signature, dedupe on run_id, and park each finished SKU video as pending review before anything is published. Runnable Python.
- Max text length for Gemini and OpenAI TTS: the docs name none
The OpenAI TTS guide and the Gemini speech page I read state no input limit. Sume publishes 20,000 characters and 1,200 s; here is a splitter.
Written by Sume