Cancel a Sume bulk run: no queue endpoint, so cancel each child

Sume has no public cancel for a bulk queue. Poll the queue, then POST /v1/format-runs/{run_id}/cancel for each running child. fal cancels one request with PUT.

4 min readSume
All posts

There is no public endpoint to cancel a Sume bulk queue, and no public list of queues. To stop a batch, read the queue with GET /v1/format-run-queues/{id}, then call POST /v1/format-runs/{run_id}/cancel for each item that has a run_id and is not terminal. Queued items only get a run_id when a slot frees, so repeat the loop until the queue drains.

What the queue gives you

The queue object lists items with index, status, run_id and error, plus counts for total, queued, running, completed, failed and canceled. When you cancel a child, the queue marks that item canceled and frees its slot for the next queued item.

That last rule is the trap: freeing a slot starts the next queued item. To really stop the batch, cancel the queued ones as well as soon as they get a run_id, or accept that a small tail will start.

Cancel paths, read 2026-10-08
ServiceCancel callScope
Sume bulk queueNone publicNot available
Sume child runPOST /v1/format-runs/{run_id}/cancelformats:write
Sume single jobCancel URL on the envelopeOnly before generation starts
fal requestPUT on the cancel endpointOne request

A loop that drains the window

The loop below cancels every child that has a run id and is not finished, then re-reads the queue until nothing is running or queued. It stops after a bounded number of rounds.

import json, os, time, urllib.request

H = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"]}

def call(url: str, method: str = "GET") -> dict:
    req = urllib.request.Request(url, headers=H, method=method)
    with urllib.request.urlopen(req, timeout=20) as r:
        return json.load(r)

def stop(queue_id: str, rounds: int = 20) -> dict:
    base = "https://api.sume.com/v1"
    for _ in range(rounds):
        q = call(f"{base}/format-run-queues/{queue_id}")["data"]
        live = [i for i in q["items"] if i["run_id"] and i["status"] in ("queued", "running")]
        if not live and q["counts"]["queued"] == 0:
            return q
        for i in live:
            call(f"{base}/format-runs/{i['run_id']}/cancel", "POST")
        time.sleep(2)
    return q

What it costs you

A run that already started generation may not be cancelable; the job rule is that cancel works only before generation starts, otherwise 409 job_generation_already_started. Treat that 409 as normal in the loop and keep going. Check counts.failed and counts.canceled at the end before you reconcile spend.

Reading the end state

After the loop returns, the queue status is completed, which means every item is terminal. It does not mean success. Read counts for completed, failed and canceled, and keep the three numbers with the batch record. A child that finished before your cancel arrived stays completed, and its cost is spent.

A cancel on a run that has begun generation can return a conflict. Handle that as expected, and let the run finish; the counts.completed field tells you how many ended that way.

Compared with fal

The fal queue page, read today, lists a cancel endpoint that uses PUT for one request, and it states that requests in the queue are never dropped, so a cancel is the only way to stop work there. Sume is the same at the single-run level and has no queue-level call, which is why the per-child loop exists.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume