Format run on_active_run: allow, skip or reject with a 409?
on_active_run sets what a second Format run does while one is in flight: allow runs both, skip records a skipped run, reject answers 409 format_run_in_progress.

on_active_run on a Format run request chooses what happens when a run of the same Format is already in flight: allow (the default) runs both at once, skip records a run with status skipped, and reject answers 409 format_run_in_progress. Pick skip for cron-style triggers where a missed tick is fine, and reject when your caller should know it collided.
The rules below come from Calling a Format and Runs and results.
What do the three values do?
The default is allow: the run starts concurrently, and only your workspace generation concurrency limit can slow it down. The other two are opt-in.
Scheduled Actions default to skip, so do not copy a Scheduled body into a Format call and assume it behaves the same. A direct Format run behaves like allow unless you say otherwise.
| Value | What happens | What you get back |
|---|---|---|
| allow (default) | Runs concurrently; workspace concurrency still applies | A normal queued run |
| skip | Records a run that never executes | A terminal run with status skipped, skip_reason set, next_action retry_later |
| reject | Refuses the request | 409 format_run_in_progress |
Is skipped an error or a result?
A result. The run lifecycle is queued -> processing -> completed | failed | canceled | skipped, and skipped means it never ran because you sent skip and another run was in flight. next_action on that receipt is retry_later, and skip_reason says why.
Two details matter for integrations. A skipped run is already terminal, so cancel has nothing to stop. And a webhook is delivered once per run on completed or failed only, never on canceled or skipped, so a receiver that waits for a push on every run will wait forever for a skipped one. Branch on the create response instead.
How do I handle it with the SDK?
subscribeFormatRun and waitForRun resolve for any terminal status, so a skipped run comes back as a value, not an exception. Put skipped in your switch next to failed:
import { createSumeClient, subscribeFormatRun } from "@sume-com/sdk";
const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });
const run = await subscribeFormatRun({
client,
path: { handle: "acme", slug: "product-promo" },
idempotencyKey: "nightly-promo-2026-10-02",
body: {
input: { product_url: "https://shop.example.com/p/8823" },
on_active_run: "skip",
},
});
switch (run.status) {
case "completed": console.log(run.primary_output_url); break;
case "skipped": console.log("busy, try later:", run.skip_reason); break;
default: console.error(run.status, run.error);
}When should I choose reject instead?
Use reject when the caller is a person or a pipeline step that must not silently lose work. A 409 format_run_in_progress is an error at create, so nothing runs and nothing is charged, and your code decides whether to wait, queue or tell the user.
Use skip when the trigger repeats on its own and a later tick will cover the gap. Use allow when each run is a distinct unit of work, such as one render per order, and send an Idempotency-Key derived from the order so a retry cannot double-run it.
Do not confuse this with the idempotency conflicts. 409 idempotency_key_in_use is two requests with the same key at the same moment, and it is retryable after about a second. format_run_in_progress is two different requests against one Format, and only reject produces it.
For many items at once, a bulk queue is the better tool than firing creates with allow and hoping the concurrency limit sorts it out.
How should I test each branch?
Cheap tests beat guesses here. Start one long Format run, then send a second create for the same Format with each value of on_active_run and log what comes back. With allow you should see a second queued or processing run and a new run id. With skip you should see a terminal receipt whose status is skipped, with skip_reason populated and next_action set to retry_later. With reject you should see an HTTP 409 and the code format_run_in_progress, and no run id at all.
Check the spend side as well. A skipped run never executed, so there is nothing to bill, and a reject fails at create, where the errors page says nothing runs and nothing is charged. Only allow can add spend, and the workspace generation concurrency limit still applies to it, so a burst of allow creates can sit queued rather than run in parallel.
Keep your own bookkeeping honest too. If your system counts a trigger as handled once the create call returns, a skipped receipt means that tick produced nothing. Record the skip, and decide whether a later trigger or a retry with the same idempotency key from your side should cover it. Remember that a retry after a skip is a new decision: use a new key if you want a new attempt, because the same key with the same body replays the original receipt.
Keep this reference next to your trigger code.
- Default is
allow; scheduled Actions default toskip. skippedis terminal and carriesnext_action: retry_later.- No webhook is delivered for
skippedorcanceledruns. rejectis409 format_run_in_progress, raised before any run exists.- Always send an
Idempotency-Keyderived from what you are making.
Sources
Related posts
More in Developers
- Sume Idempotency-Key design: one key per order and revision
Build Idempotency-Key from your own order id plus a revision number, so retries return the same Sume job and a changed prompt is a deliberate new key.
- Image reference URL rejected on Sume: localhost, http, private hosts
Sume's Image API rejects localhost, private-network and non-HTTPS reference URLs before submission. A pre-flight check in Python and what to host instead.
- Sume job response: status_url, result_url, events_url, cancel_url
A Sume submit returns four URLs: status_url to poll, result_url once result_ready is true, events_url for the timeline, cancel_url while cancelable is true.
- Sume job status: queue.state, a null position, worker_heartbeat
Why queue.position is null on a Sume job status, what queue.state and worker_heartbeat report, and what a poller should do when a job sits in the queue.
Written by Sume