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 + pathEffect
POST /runs/{id}/cancelMove a non-terminal run to cancelled and cancel its in-flight steps.
POST /runs/{id}/pauseMove a non-terminal run to paused. The scheduler will not step a paused run. Idempotent.
POST /runs/{id}/resumeMove a paused run back to queued and re-enqueue it.
POST /runs/{id}/replayStart 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-stepFork a finished run from a chosen step, carrying the steps before it. Body { "step": "<name>" }. Returns the new run.
POST /runs/bulk-replayReplay 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:

QueryEffect
actionOnly one kind (e.g. replay, bulk_replay).
runIdEntries targeting that run or forking into it - a run's full control history in one query.
limitPage 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

StatusWhen
400retry-from-step with no step; or bulk-replay with an unparseable body or no filter at all.
403The namespace is suspended (any replay path).
404The run id does not exist; or retry-from-step named a step the run does not have.
409pause/cancel on a terminal run; resume on a run that isn't paused; replay/retry-from-step on a run that isn't finished.

On this page