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.

5 min readSume
All posts

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.

read 2026-10-03
PrefixKindRead routeRouting
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

All Developers posts

Written by Sume