A job id is not a run id: poll image and video generation via /v1/jobs

Image and video generate routes create jobs, not runs. Poll GET /v1/jobs/:id/status until terminal is true. waitForRun is for run ids only.

5 min readSume
All posts

Runs and jobs are different surfaces with different ids, and you cannot swap them. POST /v1/image-1.0/generate, POST /v1/video-1.0/generate, the Avatar routes and the /v1/models/sume/…/runs aliases all create generation **jobs**, which live at /v1/jobs/:id. waitForRun and the /v1/format-runs/… routes are for Format, Action and Agent runs. Pass a job id to a run helper, or a run id to a job route, and the id will not resolve: the docs say you cannot use a job id as a run id or a run id as a job id.

Two surfaces, side by side

RunsGeneration jobs
Created byFormat, Action or Agent Completion callsImage, video and Avatar generation routes
Read at/v1/format-runs/…, /v1/action-runs/…, /v1/agent-runs/…/v1/jobs/:id, /status, /result, /events
Wait withwaitForRun with a required familywaitForJob, or your own poll of /jobs/:id/status
Done whenTerminal run statusterminal: true on the status route
Webhook events*.run.terminaljob.* events such as job.completed

The SDK docs describe waitForJob for the job surface. If the version you installed does not export it, poll the status route yourself; the loop is short, and the sample below does exactly that. Check what your installed package exports before you build on a helper.

A job poll that follows the docs

The recommended client behavior for jobs is: treat queued and processing as normal, use exponential backoff, and poll until terminal: true or your own application deadline. The status route also returns next_poll_after_seconds, which the sample uses as the next delay. On 429 or a 5xx it waits retry-after, since a failed read does not mean the job failed.

const key = process.env.SUME_API_KEY;
const base = process.env.SUME_API_BASE_URL ?? "https://api.sume.com/v1";
if (!key) throw new Error("set SUME_API_KEY");
const get = (path) => fetch(`${base}${path}`, { headers: { "x-api-key": key } });

// Generation jobs live at /v1/jobs/:id. A job id is not a run id.
export async function pollJob(jobId, deadlineMs = 15 * 60_000) {
  const end = Date.now() + deadlineMs;
  let wait = 3;
  while (Date.now() < end) {
    const res = await get(`/jobs/${jobId}/status`);
    if (res.status === 429 || res.status >= 500) {
      wait = Number(res.headers.get("retry-after")) || Math.min(wait * 2, 60);
    } else if (!res.ok) {
      throw new Error(`status ${res.status}`);
    } else {
      const raw = await res.json();
      const s = raw.data ?? raw;
      if (s.terminal) return (await get(`/jobs/${jobId}/result`)).json();
      wait = s.next_poll_after_seconds ?? Math.min(wait * 2, 60);
    }
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  throw new Error("your deadline passed; the job may still be running");
}

console.log(await pollJob(process.argv[2] ?? "job_123"));

Rules that apply to jobs

  • Do not submit a paid request again only because your local worker timed out. The job may still be running and billing.
  • Use an Idempotency-Key on each paid submit that a client can retry.
  • Store status_url, result_url, events_url and cancel_url when they are present on the response.
  • Read generation_limits on the response. When queue capacity is low, do not add more work.
  • Job reads are creator-only: an API key reads only the jobs its own member created, and any other read gives 404 not_found.
  • A job can be canceled only before generation starts. Later you get 409 job_generation_already_started.

Picking the right wait for your id

If you are unsure which surface an id came from, look at where it was created. An id you got from a format-runs URL, an action-runs URL or an agent-runs URL is a run, and waitForRun needs its family because the helper cannot infer it from the id. An id you got back from a generation route, or that you see under /v1/jobs, is a job. Store the surface next to the id when you create it, and your recovery code never has to guess.

A run can produce several jobs inside one agent turn, and the run webhook fires once for the turn. If you want one event per job, subscribe to the job.* events on the job layer instead.

From the CLI

The CLI follows the same model: sume jobs status job_123 --agent --json for the poll, sume jobs result when it is terminal, and sume jobs events for the audit trail. A job you lost track of can be recovered with sume jobs list and sume jobs get. See the SDK runs page for waitForRun and its family option, and keep the rule in mind: the three run families sit behind three URL prefixes, and a job id belongs to none of them.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume