> ## Documentation Index
> Fetch the complete documentation index at: https://docs.duraton.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](/reference/api/runs). 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.                                              |
| `POST /runs/bulk-cancel`          | Cancel every non-terminal run matching a filter. Body `{ app?, workflow?, status?, runType?, tags?, since? }`. Returns outcome counts.                                   |

<Tabs>
  <Tab title="TypeScript">
    ```ts theme={null}
    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" });
    ```
  </Tab>

  <Tab title="REST API">
    ```sh theme={null}
    curl -X POST $DURATON_URL/runs/01HXYZ.../pause
    curl -X POST $DURATON_URL/runs/01HXYZ.../resume
    curl -X POST $DURATON_URL/runs/01HXYZ.../replay
    curl -X POST $DURATON_URL/runs/01HXYZ.../replay -d '{"input":{"orderId":"A1"}}'  # replay with an edited payload
    curl -X POST $DURATON_URL/runs/01HXYZ.../retry-from-step -d '{"step":"charge"}'
    curl -X POST $DURATON_URL/runs/bulk-replay -d '{"status":"failed","since":"2026-06-01T00:00:00Z"}'
    ```
  </Tab>
</Tabs>

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

## Which status accepts which call

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

```sh theme={null}
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
```

| Call                        | Accepted when the run is | Rejected with                                               |
| --------------------------- | ------------------------ | ----------------------------------------------------------- |
| `cancel`, `pause`           | non-terminal             | `409` on a terminal run                                     |
| `resume`                    | `paused`                 | `409` on any other status                                   |
| `replay`, `retry-from-step` | terminal                 | `409` 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](/reference/api/runs).

## Bulk replay

`bulk-replay` redrives many runs at once. It selects finished runs by the same axes as the
[runs listing](/reference/api/runs) - `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](/reference/api/runs) - `app`, `workflow`, `status`, `runType`, and
`since` - plus [`tags`](/reference/api/runs#run-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.

```ts theme={null}
// cancel every queued run for one customer
const res = await duraton.runs.bulkCancel({ status: "queued", tags: { customerId: "A1" } });
// { matched, cancelled, skipped, failed, capped }
```

```sh theme={null}
curl -X POST $DURATON_URL/runs/bulk-cancel -d '{"status":"queued","tags":{"customerId":"A1"}}'
```

<Note>
  The `tags` filter depends on [run tags](/reference/api/runs#run-tags).
</Note>

## 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
approval decisions. Read it with `GET /control-actions`, newest first:

```sh theme={null}
curl "$DURATON_URL/control-actions?runId=01HXYZ...&limit=100"
```

| Query    | Effect                                                                                                                                               | Default   |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
| `action` | One kind: `cancel`, `pause`, `resume`, `replay`, `retry_from_step`, `bulk_replay`, `bulk_cancel`, `approve`, `deny`. An unknown value returns `400`. | all kinds |
| `runId`  | Entries targeting that run **or** forking into it - a run's full control history in one query.                                                       | all runs  |
| `limit`  | Page 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

| Status | When                                                                                                                                                                                                      |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `retry-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](/reference/api/runs#run-tags). |
| `403`  | The project 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.                                                                         |
