> ## 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.

# Workflows API

> See what your runners actually registered - triggers, schedules, retry policy, flow control, and the step manifest - and start a run by hand.

`GET /workflows` returns the workflow **definitions** currently registered in the project - the shape a
runner declared when it [connected](/reference/sdk/connect). It is the read model behind the console's
Workflows view. Definitions are registered by your runners; there is no write endpoint for them.

`POST /workflows/{app}/{name}/trigger` starts one off-schedule run of a registered workflow by identity -
see [trigger a run manually](#trigger-a-run-manually) below.

| Method + path                          | Returns                                          |
| -------------------------------------- | ------------------------------------------------ |
| `GET /workflows`                       | Every workflow registered in the project.        |
| `POST /workflows/{app}/{name}/trigger` | Starts one run of that workflow. `202 Accepted`. |

`GET /workflows` takes no query parameters - it returns the whole set. Filter client-side by `app` when
you only want one app's workflows.

## Response

An array of workflow definitions:

```json theme={null}
[
  {
    "name": "fulfillment",
    "app": "support-app",
    "maxAttempts": 3,
    "backoff": "exponential",
    "triggers": [
      { "event": "ticket.created" },
      { "event": "ticket.updated", "if": "event.data.priority == 'high'" }
    ],
    "scheduled": false,
    "flowControl": {
      "concurrency": { "limit": 5, "key": "customerId" },
      "idempotency": { "key": "ticketId", "periodMs": 86400000 }
    },
    "steps": [
      { "name": "validate", "description": "Validate the ticket" },
      { "name": "triage" },
      { "name": "audit", "hidden": true }
    ],
    "registeredAt": "2026-06-01T09:00:00Z",
    "updatedAt": "2026-06-22T10:00:00Z"
  },
  {
    "name": "nightly-report",
    "app": "support-app",
    "maxAttempts": 1,
    "backoff": "fixed",
    "scheduled": true,
    "schedules": [
      {
        "cron": "0 2 * * *",
        "nextFireAt": "2026-06-23T02:00:00Z",
        "lastFiredAt": "2026-06-22T02:00:00Z",
        "isStale": false
      }
    ],
    "registeredAt": "2026-06-01T09:00:00Z",
    "updatedAt": "2026-06-01T09:00:00Z"
  }
]
```

| Field          | Meaning                                                                                                                                                                                                                                  |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`         | The registered workflow name - the dispatch key a run records and a `runWorkflow` targets.                                                                                                                                               |
| `app`          | The app the workflow belongs to.                                                                                                                                                                                                         |
| `maxAttempts`  | The workflow-wide [retry](/core/retries) attempt budget (`1` = no retry).                                                                                                                                                                |
| `backoff`      | The retry backoff shape: `fixed`, `linear`, or `exponential`. The finer bounds (`initialDelayMs` / `maxDelayMs`) are applied at runtime and are not re-emitted here.                                                                     |
| `triggers`     | The workflow's [triggers](/core/triggers). Each entry sets exactly one of `event` (with an optional `if` CEL guard) or `cron`. Absent when the workflow declared none (it is then implicitly triggered by an event matching its `name`). |
| `scheduled`    | `true` when the workflow has at least one cron schedule. Always present.                                                                                                                                                                 |
| `schedules`    | The resolved cron schedules, each `{ cron, nextFireAt, lastFiredAt?, isStale }` - see [detecting a dead schedule](/core/triggers#detecting-a-dead-schedule). Absent when the workflow has no cron trigger.                               |
| `flowControl`  | The configured [flow-control](/core/flow-control) policies, in the same millisecond-based shape they were registered with. A present sub-field is an active policy; the whole object is absent when none are set (see below).            |
| `steps`        | The advisory [step manifest](#step-manifest) in declaration order. Absent when the workflow declared none.                                                                                                                               |
| `registeredAt` | When the workflow was first registered (RFC3339).                                                                                                                                                                                        |
| `updatedAt`    | When its definition was last updated by a re-registration (RFC3339).                                                                                                                                                                     |

### Flow control

`flowControl` re-emits whatever [flow-control policies](/core/flow-control) the workflow registered, each
as an optional sub-field. A field is present only when that policy is active; all durations are
milliseconds:

| Sub-field       | Shape                                                       |
| --------------- | ----------------------------------------------------------- |
| `concurrency`   | `{ limit, key? }`                                           |
| `throttle`      | `{ limit, perMs, key? }`                                    |
| `rateLimit`     | `{ limit, perMs, key? }`                                    |
| `debounce`      | `{ periodMs, key? }`                                        |
| `batch`         | `{ maxSize, timeoutMs, key? }`                              |
| `priority`      | `{ shiftMs }`                                               |
| `singleton`     | `{ key?, mode? }`                                           |
| `idempotency`   | `{ key?, periodMs? }`                                       |
| `cap`           | `{ maxCost?, maxTokens? }` - a per-run AI spend ceiling     |
| `tokenThrottle` | `{ tokens, perMs, key? }` - a token-denominated AI throttle |

### Step manifest

`steps` is the workflow's **advisory** manifest of declared steps, in declaration order. Each entry is:

| Field         | Meaning                                                                                               |
| ------------- | ----------------------------------------------------------------------------------------------------- |
| `name`        | The declared step id - the id the run view diffs against the run's actually-executed steps.           |
| `description` | An optional human label for the step. Absent when none was declared.                                  |
| `hidden`      | `true` to exclude a bookkeeping step from a customer-facing progress view. Absent (falsey) otherwise. |

The manifest is rendering metadata only: it never gates execution, never fails a run for drift, and
never matches emitted opcodes. When a run's executed steps disagree with the manifest, **discovery
wins** - the run view shows what actually ran. A workflow that declares no manifest behaves exactly the
same; the field is simply absent.

<Tabs>
  <Tab title="TypeScript">
    ```ts theme={null}
    import { createClient } from "@duraton/sdk/client";

    const duraton = createClient({ url: process.env.DURATON_URL! });
    const workflows = await duraton.workflows.list(); // WorkflowDef[]
    ```
  </Tab>

  <Tab title="REST API">
    ```sh theme={null}
    curl "$DURATON_URL/workflows"
    ```
  </Tab>
</Tabs>

<Note>
  The wire contract (`GET /workflows`) is stable, so any language can read it over REST today.
</Note>

## Trigger a run manually

`POST /workflows/{app}/{name}/trigger` starts one run of one workflow by identity, independent of its
declared triggers. It works even for a workflow with only a `cron` trigger, which `POST /events` cannot
reach - there is no event to send it. See the [manual trigger guide](/core/triggers#trigger-a-run-manually)
for the cron-only case, how this differs from `POST /events`, and why flow control still applies.

Every field is optional, and a request with no body at all is valid - that's the cron-only case:

<Tabs>
  <Tab title="TypeScript">
    ```ts theme={null}
    import { createClient } from "@duraton/sdk/client";

    const duraton = createClient({ url: process.env.DURATON_URL! });

    // Just fire it.
    const res = await duraton.workflows.trigger("support-app", "nightly-report");
    res.runId;       // absent if a flow-control gate short-circuited it

    // With custom input, an event-name label, and tags.
    await duraton.workflows.trigger("support-app", "fulfillment", {
      input: { ticketId: "T-421" },
      eventName: "ticket.created",
      tags: { team: "support" },
    });
    ```
  </Tab>

  <Tab title="REST API">
    ```sh theme={null}
    # Just fire it - no body needed.
    curl -X POST "$DURATON_URL/workflows/support-app/nightly-report/trigger" \
      -H "Authorization: Bearer $DURATON_API_KEY"

    # With custom input, an event-name label, and tags.
    curl -X POST "$DURATON_URL/workflows/support-app/fulfillment/trigger" \
      -H "Authorization: Bearer $DURATON_API_KEY" \
      -d '{"input":{"ticketId":"T-421"},"eventName":"ticket.created","tags":{"team":"support"}}'
    ```
  </Tab>
</Tabs>

### Request body

| Field       | Type                     | Meaning                                                                                                                                                    |
| ----------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input`     | any JSON                 | Becomes `ctx.event.data` on the run. Defaults to `{}` (never `null`) when omitted.                                                                         |
| `eventName` | string                   | Overrides `ctx.event.name`. Defaults to the workflow's own name. **Label only** - it never fires an event, causes fan-out, or wakes a `waitForEvent` step. |
| `dedupeId`  | string                   | Drop a repeat of the same id (per workflow) within the dedupe window. Absent means no dedupe: a second call is a second run.                               |
| `runner`    | string                   | Pin the run to a specific runner id. Anycast (any capable runner) when absent.                                                                             |
| `tags`      | object of string->string | Customer-defined key/value metadata attached to the run - same [limits](/reference/api/runs#run-tags) as event tags.                                       |

`eventName`, `dedupeId`, and `runner` are each bounded at 256 characters; a longer value or unparseable
`input` returns `400`.

### Response

`202 Accepted`. The run is created and enqueued, not executed - a caller that wants the finished result
polls [`GET /runs/{id}`](/reference/api/runs) or uses [`runs.wait`](/reference/sdk/client#trigger-and-await).

```json theme={null}
{
  "workflow": "nightly-report",
  "app": "support-app",
  "runId": "01HXYZ...",
  "eventName": "nightly-report"
}
```

| Field                                                       | Meaning                                                                                                                                                                  |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `workflow` / `app`                                          | The workflow the request addressed.                                                                                                                                      |
| `runId`                                                     | The run started. **Absent** when a flow-control gate short-circuited the request - not an error.                                                                         |
| `eventName`                                                 | The resolved `ctx.event.name` on the run: your `eventName` override, or the workflow's own name.                                                                         |
| `skipped` / `dropped` / `debounced` / `batched` / `deduped` | Flow-control outcomes - set (and `runId` absent) when the request was held back rather than run immediately. At most one is set. See [flow control](/core/flow-control). |

A workflow not registered in the project (or registered in another project) returns `404`. No live
runner capable of serving the workflow returns `502` - the run was never queued, so there's nothing to
poll.
