Control API
HTTP endpoints to cancel, pause, resume, replay, and retry runs.
The engine exposes mutating control endpoints alongside the read-only runs API. Each returns the
affected run as JSON (replay and retry-from-step return the new run); misuse returns 409 Conflict, an unknown run
404 Not Found.
Endpoints
| Method + path | Effect |
|---|---|
POST /runs/{id}/cancel | Move a non-terminal run to cancelled and cancel its in-flight steps. |
POST /runs/{id}/pause | Move a non-terminal run to paused. The scheduler will not step a paused run. Idempotent. |
POST /runs/{id}/resume | Move a paused run back to queued and re-enqueue it. |
POST /runs/{id}/replay | Start a fresh run from a finished run's trigger. Optional body { "input": <json> } overrides the forked run's payload (absent = replay verbatim). Returns the new run. |
POST /runs/{id}/retry-from-step | Fork a finished run from a chosen step, carrying the steps before it. Body { "step": "<name>" }. Returns the new run. |
POST /runs/bulk-replay | Replay every finished run matching a filter. Body { app?, workflow?, status?, runType?, since? }. Returns outcome counts. |
curl -X POST $DURABLEX_ENGINE_URL/runs/01HXYZ.../pause
curl -X POST $DURABLEX_ENGINE_URL/runs/01HXYZ.../resume
curl -X POST $DURABLEX_ENGINE_URL/runs/01HXYZ.../replay
curl -X POST $DURABLEX_ENGINE_URL/runs/01HXYZ.../replay -d '{"input":{"orderId":"A1"}}' # replay with an edited payload
curl -X POST $DURABLEX_ENGINE_URL/runs/01HXYZ.../retry-from-step -d '{"step":"charge"}'
curl -X POST $DURABLEX_ENGINE_URL/runs/bulk-replay -d '{"status":"failed","since":"2026-06-01T00:00:00Z"}'import { createClient } from "@durablex/sdk/client";
const dx = createClient({ engineUrl: process.env.DURABLEX_ENGINE_URL! });
await dx.runs.pause("01HXYZ...");
await dx.runs.resume("01HXYZ...");
const replayed = await dx.runs.replay("01HXYZ..."); // the new run
const resumed = await dx.runs.retryFromStep("01HXYZ...", "charge"); // the new run
const bulk = await dx.runs.bulkReplay({ status: "failed", since: "2026-06-01T00:00:00Z" });replay and retryFromStep return the new run, e.g. { "id": "01HABC...", "status": "queued" }.
Statuses
paused is a non-terminal status: a paused run is skipped by the scheduler but remains cancellable and
resumable. The terminal statuses (succeeded, failed, cancelled) are unchanged.
From the console
The run inspector mirrors these endpoints. A live run (queued / running / waiting) shows
Pause alongside Cancel; a paused run swaps Pause for Resume; a finished run shows
Replay. Pause and resume fire on a single click - no confirm, since both are reversible. The
consequential controls - Cancel and Retry from step (on a finished run's steps) - take a
two-click confirm: the first click arms the button, a second click within a few seconds runs it.
Pause takes effect at the next step boundary: the step in flight finishes and is checkpointed, then the run stops before its next step - it is not a mid-step interrupt. Resume re-queues the run and it continues from the next step, replaying the already-completed steps from their stored results, so no prior work runs twice. Each click is recorded in the run's Control history (the same audit log above).
Replay on a finished run opens a payload editor pre-filled with the original input. Replay it as-is
to fork verbatim, or edit the JSON first to fork from a corrected payload (the input override above) -
useful for re-driving a run that failed on bad input without re-triggering the source event.
Replay semantics
replay acts only on a finished run (succeeded / failed / cancelled); replaying a still-active
run returns 409. It does not mutate the original - that stays as history. Instead it creates a new
run with its own id, copying the original's workflow, app, input, and runner, and enqueues it from the
start.
The new run records the id of the source run it was forked from in replayOf (set the same way by
retry-from-step and bulk-replay), so the lineage is traceable both ways: a run links back to its
origin, and GET /runs?replayOf=<id> lists every run forked from one source. See
replay->run lineage.
Bulk replay
bulk-replay redrives many runs at once. It selects finished runs by the same axes as the
runs listing - app, workflow, status, runType, and since (namespace-scoped) - and
forks each, newest first, up to a per-call ceiling (the response sets capped: true when more matched
than were replayed; narrow the filter and call again). Non-terminal matches are counted in skipped,
not replayed. A request with no filter at all is rejected (400), so a replay is always scoped to at
least one of status/since/app/workflow/runType - an empty body never redrives the whole namespace.
The response is { matched, replayed, skipped, failed, capped }. A suspended namespace refuses the whole call
(403).
Retry from a step
retry-from-step is replay with a checkpoint. Like replay it acts only on a finished run and forks a
new run (the original stays as history), but it carries over the completed steps before the named
step and resumes execution from that step. The carried steps replay from their stored results - durable
execution skips them - so an expensive earlier step (a charge, an email) is not run twice. Any step of the
run is a valid boundary, letting you rewind to any point; picking the first step carries nothing and is
equivalent to a full replay. An unknown step name returns 404.
Audit log
Every control action above (cancel, pause, resume, replay, retry_from_step, bulk_replay) is
recorded to a per-namespace audit log. Read it with GET /control-actions, newest first:
| Query | Effect |
|---|---|
action | Only one kind (e.g. replay, bulk_replay). |
runId | Entries targeting that run or forking into it - a run's full control history in one query. |
limit | Page size (default 100, max 1000). |
Each entry is { id, action, runId?, newRunId?, actor?, detail?, createdAt }. actor is the label of the
API key behind the action (e.g. console, static, or a managed-issued key's name). detail carries
action-specific context: the step a retry resumed from, or a bulk replay's filter and outcome counts.
Recording is best-effort - a failed audit write is logged but never fails the action it describes, since
the action has already happened.
Error codes
| Status | When |
|---|---|
400 | retry-from-step with no step; or bulk-replay with an unparseable body or no filter at all. |
403 | The namespace is suspended (any replay path). |
404 | The run id does not exist; or retry-from-step named a step the run does not have. |
409 | pause/cancel on a terminal run; resume on a run that isn't paused; replay/retry-from-step on a run that isn't finished. |