https://<project slug>.<project domain>/workflows/v1.
Keys
Send the key in the
apikey header. Never put a key in a URL.
Routes
Lists take
limit (1 to 100, default 20) and an opaque cursor; only
nextCursor: null ends a list. There is no total.
Every change takes an optional Idempotency-Key header: the same key and body
answers the first result again; another body under the same key is
422 idempotency_key_reused.
A body field this page does not name is ignored, not refused: it has no
effect, so never rely on one. The exceptions are on the bulk calls, so one
never reaches runs you did not name: input on POST /runs/bulk_replay is
422, and a GET /runs filter a bulk call cannot select by (eventId,
until and buildId on both, tags on a replay) is
422 workflow_bulk_filter_unsupported, its fields naming them.
Pause and resume a run
POST /runs/{id}/pause holds a queued, running or waiting run where it
is: it keeps every finished step, and nothing more of it runs until it is
resumed. A step already running finishes first. The run answers
status: "paused" and pausedBy: { reason: "manual", since }. Pausing a
paused run answers it again with a new since.
POST /runs/{id}/resume queues it again and clears pausedBy; it carries on
from the step it stopped at. A run waiting on an event stays queued until the
event arrives.
Run logs
GET /runs/{id}/logs lists what the run’s worker logged with the handler’s log, newest
first. Each line is { seq, ts, level, message, fields?, scope, attempt }:
scope is the step that logged it, or @root outside a step, and attempt is
that step’s attempt.
Steps
Each step carrieskind, what it does: run, sleep, wait, invoke,
emit, webhook or approval. A step recorded before kinds existed has
none.
Following runs live
GET /runs/stream?kinds=run_status,step_status&since=<time> answers
text/event-stream. Each change is one event:
since (or the Last-Event-ID header) set to the last id you
read; the boundary event may repeat, so skip any (runId, seq) you have
seen. A since older than the project’s look-back is 422 transition_cursor_expired: list the runs again and follow from now. A comment
line : keepalive arrives every workflows.stream_keepalive_seconds (15 by
default, at most 300; 0 sends none).
This stream and GET /runs/{id}/stream each stay open for the project’s
workflows.stream_max_seconds (an hour by default), then end with one last
event and close:
since set to the last id you
read here, from on a run’s timeline. The SDK’s
runs.watchTransitions returns at end; call it again to follow on.
Events, schedules, wait tokens, workers, limits and usage each have their own
page: Events, Schedules,
Wait tokens, Workers,
Limits, Usage.
On the console’s address
The console and your own tools reach the same routes on the console’s address, signed in or with an OAuth access token, each naming the project with?projectId=<id>. A project’s admins and developers read with duraton:read
and change with duraton:write; a viewer is refused with 403 forbidden.
GET /workflow_runs/{id}/stream?projectId=<id>&from=<seq> is the run’s
timeline: every status change of the run and its steps since from, then each
new one as it happens, as Server-Sent Events. It ends once the run has
finished, or after workflows.stream_max_seconds.
SDK reads
createClient({ url, apiKey }) from @duraton/sdk/client reads these routes:
A workflow carries
app, name, inputSchema (a JSON Schema, or null),
triggers (each an event with its if, or a cron with runOnStart) and
liveBuilds, the builds with a worker connected now. An answer of another
shape is refused with a TypeError, never passed on.
Errors
Every refusal is an RFC 9457 problem with a stablecode: