arun_ run ids: Format run or Action run? Store the family
A Sume arun_ id shows up under both /v1/format-runs and /v1/action-runs, and waitForRun requires a family. Store the surface next to the id and route reads.

Store the family next to every run id, because the id alone does not say where to read it. A Format run receipt and an Action run receipt can both carry an arun_ id, while the reads live at /v1/format-runs/{id} and /v1/action-runs/{id}. The SDK reflects this: waitForRun requires a family of format, action or agent, and the runs docs explain that it cannot be inferred because the three families sit behind three URL prefixes. A table with one id column and no family column is the bug.
Three id shapes, four read surfaces
Generation jobs use job_ ids and read at /v1/jobs/{id}. Agent Completions use agrun_ ids under /v1/agent-runs. Format runs and Scheduled Action runs both use arun_, under /v1/format-runs and /v1/action-runs. The public API page lists the routes, and each receipt includes status_url, result_url and cancel_url that already point at the right family.
That last fact is the easiest fix: store the status_url from the receipt instead of rebuilding it from the id. If you must rebuild, you need the family.
A router that refuses to guess
The function below maps an id to its status URL. A job_ or agrun_ id is unambiguous. An arun_ id without a family raises instead of picking one, which turns a silent 404 in production into a loud error in a test. It runs on any Python 3.10 or newer.
BASE = "https://api.sume.com/v1"
def status_url(run_or_job_id: str, family: str | None = None) -> str:
if run_or_job_id.startswith("job_"):
return f"{BASE}/jobs/{run_or_job_id}/status"
if run_or_job_id.startswith("agrun_"):
return f"{BASE}/agent-runs/{run_or_job_id}/status"
if run_or_job_id.startswith("arun_"):
if family not in ("format", "action"):
raise ValueError("arun_ ids exist under /format-runs and /action-runs: store the family")
return f"{BASE}/{family}-runs/{run_or_job_id}/status"
raise ValueError("unknown id prefix")
print(status_url("job_123"))
print(status_url("arun_abc", "format"))
print(status_url("agrun_xyz"))
try:
status_url("arun_abc")
except ValueError as e:
print("refused:", e)What goes wrong without it
A 404 is the usual symptom. Reading an Action run id from the Format surface returns format_run_not_found, and an engineer concludes the run was deleted. It was not; it is just on the other surface. Another symptom is a dashboard that polls the wrong family forever, reading a missing run on every cycle and spending read budget for nothing.
Mixed-source systems hit this first: a no-code tool that stores a bare id from a webhook, a scheduler that fires Actions and Formats, a support tool where someone pastes an id into a search box. Add a family column when you create the row, and fill it from the endpoint you called.
| Prefix | Kind | Read route | Routing |
|---|---|---|---|
| job_ | Generation job | /v1/jobs/{id} | Unambiguous |
| arun_ | Format run | /v1/format-runs/{id} | Needs family |
| arun_ | Scheduled Action run | /v1/action-runs/{id} | Needs family |
| agrun_ | Agent Completion | /v1/agent-runs/{id} | Unambiguous |
Webhooks carry the family in the event
On the webhook side the event name does the work: format.run.terminal, action.run.terminal and agent.run.terminal, and the request_id equals the run id. Route on event, store the family from it, and the same table question disappears for anything that arrived by push.
A migration note
If you already have a table of bare run ids, backfill the family from the first place you can: the endpoint that created the row, the event name on a stored webhook, or the status_url in a saved receipt. For rows with none of those, try the Format read first and fall back to the Action read on a 404, and write the answer back so you only guess once. Treat a 404 on both as an id you cannot see: the docs say a run owned by someone else reads the same as one that does not exist.
Also keep the id and family out of free-text fields. A column such as run_ref holding format:arun_abc works, but two columns work better, because you can index and filter on family, and a migration that renames a surface does not have to parse strings. Cancel is family-specific too: cancelFormatRun and the action-run cancel route are different calls, and cancel on a run that already ended is idempotent with a cancel_effect of no_op. If you wrap all of this in a small internal client, give it one method that takes the pair, so nobody calls a read with a bare id again.
Sources
Related posts
More in Developers
- Astro API route for Sume webhooks: export const prerender = false
An Astro endpoint can receive Sume job webhooks if it is rendered on demand. Set prerender false, read the raw body, and verify the sume-v1 signature.
- Async, webhook or sync: pick a Sume communication mode by job length
Use sync only for jobs that finish inside its 30 second cap, such as many images. Use async plus polling or webhook for video, and keep polling as the backup.
- attachment_too_large 413: 30 MB per image, 500 MB per run
A Format run 413 attachment_too_large means one image is over 30 MB or the set is over 500 MB. It is a different 413 from payload_too_large (4 MiB body).
- Audio detach in sync mode: 30 seconds, then a 202 you poll
Audio detach defaults to async. With mode sync it waits up to 30 seconds for a 200, or returns 202 to poll. Why a timeout is not a failure and how to retry.
Written by Sume