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.
- 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.
- "Avatar does not have a usable TTS voice": the 400 and its fixes
Sume TTS with avatar_id or avatar_handle returns 400 when the avatar has no TTS voice, or when voice.id disagrees with it. What each message means and the fix.
Written by Sume