200 from an Action run call: replay or skipped? Read the receipt

A 200 on POST /v1/actions/:id/runs means an idempotency replay or a skipped run. Branch on status, idempotency_hit and skip_reason, not on the HTTP code.

4 min readSume
All posts

A 200 from POST /v1/actions/:id/runs is not a success signal by itself. The API docs say it covers two cases: an idempotency replay, and a run the API skipped because another run was already active. A fresh run is 202. So read the receipt: status, idempotency_hit and skip_reason tell you which case you are in, and the docs say to branch on the status field, not on the HTTP status. A 200 also does not mean the work finished.

The three outcomes

The API-trigger page gives these responses. A render that takes a while makes overlap likely, because only one run of an Action is active at a time.

Outcomes of an Action run call (read 2026-10-05)
HTTPReceipt showsMeaningYour code does
202status: queued, poll status_urlNew run accepted and startedPoll until next_action stops saying poll_status
200idempotency_hit: trueSame key and payload seen beforeUse the original run; do not count a new one
200status: skipped, skip_reason: previous_run_activeAnother run was active and on_active_run is skipDecide whether to try later; the API records a run row
409action_run_in_progressActive run and on_active_run was rejectRetry later; no run recorded

Telling them apart in code

Check status first. If it is skipped, log skip_reason and stop: no work is running for this trigger, and nothing will be sent to a webhook for it. If idempotency_hit is true, take the run id from the receipt and keep polling the original. Otherwise it is a new run on a 202.

  • 202 plus status_url: new run, poll it.
  • 200 plus idempotency_hit: true: the earlier run is the one to track.
  • 200 plus status: skipped: no run was started for this request.
  • 409 action_run_in_progress: you asked for reject, so your caller must handle the error.

Choosing skip or reject

skip is the default. Use it when overlapping triggers are harmless, such as a nightly job where one missed night is fine. Use reject when a dropped trigger has to appear as an error in your caller, for example when an order system must know that a render was not started. With reject, no run row is written.

One more caution on the replay case: if you reuse a key with a different body, you do not get a 200; you get 409 idempotency_conflict. And without any key there is no replay protection at all, so each request starts a new run. Pair a stable key with the on_active_run choice, and the receipt will always tell you which of the paths happened.

A receipt handler

Write one function that takes the parsed receipt and returns one of four labels: started, replay, skipped or error. Call it right after the HTTP response, whatever the status. Everything after that point branches on the label, never on the HTTP code. This avoids the common mistake of treating every 200 as a replay and every 202 as the only success.

Log the label with the key you sent. When someone asks why a week's run did not happen, the log shows skipped with previous_run_active, or replay with the earlier run id, and the question is answered.

A skipped run does not send a webhook, so do not wait for one.

  • Return a label from the receipt.
  • Log label plus key.
  • No webhook for a skipped run.

Sources

Related posts

More in Agents

All Agents posts

Written by Sume