Control API

Take control of a run in flight: cancel, pause, resume, replay it, or retry from a step - plain HTTP, with replay and retry forking a new run.

Duraton 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/{id}/forkFork a finished run with one declared change on an AI step, as a shadow run. See the evals API.
POST /runs/bulk-replayReplay every finished run matching a filter. Body { app?, workflow?, status?, runType?, since? }. Returns outcome counts.
POST /runs/bulk-cancelCancel every non-terminal run matching a filter. Body { app?, workflow?, status?, runType?, tags?, since? }. Returns outcome counts.
import { createClient } from "@duraton/sdk/client";

const duraton = createClient({ url: process.env.DURATON_URL! });
await duraton.runs.pause("01HXYZ...");
await duraton.runs.resume("01HXYZ...");
const replayed = await duraton.runs.replay("01HXYZ..."); // the new run
const resumed = await duraton.runs.retryFromStep("01HXYZ...", "charge"); // the new run
const bulk = await duraton.runs.bulkReplay({ status: "failed", since: "2026-06-01T00:00:00Z" });

replay and retryFromStep return the new run, e.g. { "id": "01HABC...", "status": "queued" }.

Which status accepts which call

The run statuses split into non-terminal (queued, running, waiting, paused, needs_attention) and terminal (succeeded, failed, cancelled). Every control call is gated on that split:

curl -X POST "$DURATON_URL/runs/01HXYZ.../pause"   # 200 while non-terminal, 409 once terminal
curl -X POST "$DURATON_URL/runs/01HXYZ.../replay"  # 200 once terminal, 409 while non-terminal
CallAccepted when the run isRejected with
cancel, pausenon-terminal409 on a terminal run
resumepaused409 on any other status
replay, retry-from-step, forkterminal409 on a non-terminal run
bulk-replay-non-terminal matches are counted in skipped, not replayed
bulk-cancel-terminal matches are counted in skipped, not cancelled

paused is non-terminal: a paused run holds no worker but stays cancellable and resumable. 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, which continues from the next step and replays the already-completed steps from their stored results, so no prior work runs twice.

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 (project-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 project. The response is { matched, replayed, skipped, failed, capped }. A suspended project refuses the whole call (403).

Bulk cancel

bulk-cancel is the mirror of bulk replay: it cancels many non-terminal runs at once. It selects runs by the same axes as the runs listing - app, workflow, status, runType, and since - plus tags, so you can cancel, say, every queued run for one customer. It cancels each match newest first, up to a per-call ceiling (the response sets capped: true when more matched than were cancelled; narrow the filter and call again). Already-terminal matches are counted in skipped, not cancelled, and a run that finishes between the match and the cancel is skipped too, so the call is idempotent. A request with no filter at all is rejected (400) - a cancel is always scoped to at least one of status/since/app/workflow/runType/tags, so an empty body never cancels the whole project. The response is { matched, cancelled, skipped, failed, capped }. A suspended project refuses the whole call (403). Each cancelled run fires its own run.cancelled webhook.

// cancel every queued run for one customer
const res = await duraton.runs.bulkCancel({ status: "queued", tags: { customerId: "A1" } });
// { matched, cancelled, skipped, failed, capped }
// cancel every queued run for one customer
res, _ := dx.Runs.BulkCancel(ctx, client.BulkCancelFilter{
	Status: client.RunQueued, Tags: map[string]string{"customerId": "A1"},
})
// { matched, cancelled, skipped, failed, capped }
curl -X POST $DURATON_URL/runs/bulk-cancel -d '{"status":"queued","tags":{"customerId":"A1"}}'

bulkCancel is in the TypeScript and Go SDKs and the REST/MCP surface today; the Python SDK method is on the roadmap. The tags filter depends on run tags.

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 state-changing call is recorded to a per-project audit log - the control actions above plus the eval and approval writes. Read it with GET /control-actions, newest first:

curl "$DURATON_URL/control-actions?runId=01HXYZ...&limit=100"
QueryEffectDefault
actionOne kind: cancel, pause, resume, replay, retry_from_step, bulk_replay, bulk_cancel, fork_compare, run_eval, approve, deny. An unknown value returns 400.all kinds
runIdEntries targeting that run or forking into it - a run's full control history in one query.all runs
limitPage size, 1-1000. A non-integer returns 400.100

Each entry is { id, action, runId?, newRunId?, actor?, detail?, createdAt }. actor is the name of the API key the call authenticated with - so naming your keys per surface (ci, support-tool) is what makes the log attributable. 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; bulk-replay/bulk-cancel with an unparseable body or no filter at all; or a bulk-cancel tags filter that exceeds the tag limits.
403The project 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