Poll a Sume Action run in Python: branch on next_action, not status
A small Python loop for a Sume Scheduled run that sleeps on poll_status, retries on retry_later and stops on none, then fetches the result.

Poll GET /v1/action-runs/{run_id}/status and branch on next_action: keep waiting on poll_status, try the trigger again later on retry_later, and stop on none. Then call /result once for the full receipt.
The three values are the whole set, per Runs and results. Branching on them is simpler than listing every status, and it keeps working if a status is ever added.
What do the next_action values mean?
The loop treats skipped as terminal for polling and leaves the decision to the caller. retry_later is a signal, not an instruction to sleep here: it means the run never started because another run was active, so the right move is usually to re-call the trigger later, ideally with the same Idempotency-Key only if you want the original skipped receipt back, or a new key if you want a fresh attempt.
Use exponential backoff in production rather than a fixed sleep, as the docs advise. The sample doubles from 2 seconds to a 30 second ceiling. If you hit 429, back off further and see Errors and rate limits.
| next_action | When | Do |
|---|---|---|
| poll_status | queued or processing | Keep polling with backoff |
| retry_later | skipped | Another run was active; try the trigger again |
| none | completed, failed or canceled | Nothing left to poll; read the receipt |
What does the loop look like?
This uses only the standard library. Set SUME_API_KEY and pass a run id from the 202 receipt. It backs off from 2 seconds up to 30 and gives up after 20 minutes.
import json, os, time, urllib.request
BASE = "https://api.sume.com/v1/action-runs"
KEY = os.environ["SUME_API_KEY"]
def get(path):
req = urllib.request.Request(BASE + path,
headers={"Authorization": "Bearer " + KEY})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["data"]
def wait(run_id, limit=1200):
delay, start = 2, time.time()
while time.time() - start < limit:
s = get(f"/{run_id}/status")
if s["next_action"] != "poll_status":
return s
time.sleep(delay)
delay = min(delay * 2, 30)
raise TimeoutError(run_id)
if __name__ == "__main__":
rid = os.environ["RUN_ID"]
s = wait(rid)
if s["status"] == "completed":
print(get(f"/{rid}/result")["primary_output_url"])
else:
print("ended as", s["status"], s["next_action"])Why not call /result in the loop?
GET /v1/action-runs/{run_id}/result returns the receipt only once the run is terminal. While the run is queued or processing it returns 409 run_not_completed with the current status in details.status. Polling /status is the cheaper call built for loops; fetch /result once at the end.
Cancel is a separate call and is idempotent. POST /v1/action-runs/{run_id}/cancel needs actions:write, and canceling an already-terminal run returns that terminal receipt with 200. After you cancel, trust the response and poll status until it reads canceled; do not wait for a webhook, because a canceled run does not deliver one.
When should I use a webhook instead?
If you start many runs, one loop each is wasteful. Set communication.webhook_url and Sume sends one signed POST when the run completes or fails, carrying the same receipt. Keep the loop as a backup: canceled and skipped runs never deliver a webhook, so for those you must read the status. See Run webhooks.
A final point on the receipt: read output_error when a run is completed but output is empty. A run can finish, bill, and produce real media while failing to project that media into your schema, and the receipt tells you why. The loop above prints only the primary output URL, so add a branch for output_error before you ship it.
Sources
Related posts
More in Agents
- Cron or API call: the trigger type of a Sume schedule is fixed
A Sume schedule's trigger_type is set at creation. An API-only schedule can never gain a cadence, and a cron one can never become API-only. What to pick first.
- Passing data to a scheduled agent run: input is data, not instructions
What the input field on a Sume Scheduled run does, its 64-property and 2 MiB limits, and why caller text cannot rewrite the saved instructions.
- Storyboard to finished video over Sume MCP: the tool order
The order a Sume MCP client should follow: stills, inspect, wordless clips, probe, then a timeline dry run and render. Where each step bills and what to skip.
- Sume Action run 403 insufficient_scope: old keys and service accounts
A 403 on POST /v1/actions/{id}/runs means the key lacks actions:write, predates the scope, or is a service-account key. Why scopes cannot be added and the fix.
Written by Sume