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 or served. 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 below.

Method + pathReturns
GET /workflowsEvery workflow registered in the project.
POST /workflows/{app}/{name}/triggerStarts 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:

[
  {
    "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"
  }
]
FieldMeaning
nameThe registered workflow name - the dispatch key a run records and a runWorkflow targets.
appThe app the workflow belongs to.
maxAttemptsThe workflow-wide retry attempt budget (1 = no retry).
backoffThe retry backoff shape: fixed, linear, or exponential. The finer bounds (initialDelayMs / maxDelayMs) are applied at runtime and are not re-emitted here.
triggersThe workflow's 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).
scheduledtrue when the workflow has at least one cron schedule. Always present.
schedulesThe resolved cron schedules, each { cron, nextFireAt, lastFiredAt?, isStale } - see detecting a dead schedule. Absent when the workflow has no cron trigger.
flowControlThe configured 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).
stepsThe advisory step manifest in declaration ticket. Absent when the workflow declared none.
registeredAtWhen the workflow was first registered (RFC3339).
updatedAtWhen its definition was last updated by a re-registration (RFC3339).

Flow control

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

Sub-fieldShape
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
budget{ maxCost?, maxTokens?, windowMs, warnAtPct? } - a rolling-window 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 ticket. Each entry is:

FieldMeaning
nameThe declared step id - the id the run view diffs against the run's actually-executed steps.
descriptionAn optional human label for the step. Absent when none was declared.
hiddentrue 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.

import { createClient } from "@duraton/sdk/client";

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

The typed workflows read is currently available in the TypeScript and Go SDKs; the Python read helper is on the roadmap. The wire contract (GET /workflows) is stable, so any language can read it over REST today.

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 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:

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" },
});

Request body

FieldTypeMeaning
inputany JSONBecomes ctx.event.data on the run. Defaults to {} (never null) when omitted.
eventNamestringOverrides 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.
dedupeIdstringDrop a repeat of the same id (per workflow) within the dedupe window. Absent means no dedupe: a second call is a second run.
runnerstringPin the run to a specific runner id. Anycast (any capable runner) when absent.
tagsobject of string->stringCustomer-defined key/value metadata attached to the run - same limits 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} or uses runs.wait.

{
  "workflow": "nightly-report",
  "app": "support-app",
  "runId": "01HXYZ...",
  "eventName": "nightly-report"
}
FieldMeaning
workflow / appThe workflow the request addressed.
runIdThe run started. Absent when a flow-control gate short-circuited the request - not an error.
eventNameThe resolved ctx.event.name on the run: your eventName override, or the workflow's own name.
skipped / dropped / debounced / batched / dedupedFlow-control outcomes - set (and runId absent) when the request was held back rather than run immediately. At most one is set. See 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.

On this page