`PUT /api/workflows/{id}` — replace the workflow's steps (and optionally its name). A clean full-replace: the service re-validates the DAG, deletes the workflow's steps and re-inserts the request's steps. An invalid DAG -> 400.
const url = 'https://example.com/api/workflows/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0';const options = { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: '{"name":"example","nodes":[{"config":"example","depends_on":[{"handle":"example","key":"example"}],"key":"example","kind":"example","parent_node_key":"example","position":{"x":1,"y":1}}],"steps":[{"agent_type":"example","depends_on":["example"],"key":"example","on_failure":"example","prompt":"example"}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request PUT \ --url https://example.com/api/workflows/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0 \ --header 'Content-Type: application/json' \ --data '{ "name": "example", "nodes": [ { "config": "example", "depends_on": [ { "handle": "example", "key": "example" } ], "key": "example", "kind": "example", "parent_node_key": "example", "position": { "x": 1, "y": 1 } } ], "steps": [ { "agent_type": "example", "depends_on": [ "example" ], "key": "example", "on_failure": "example", "prompt": "example" } ] }'Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Workflow id
Request Bodyrequired
Section titled “Request Bodyrequired”PUT /api/workflows/{id} body — full-replace the workflow’s graph. Carries
either the v1 steps shape or the typed nodes array (the V8 builder).
object
Optional rename; the existing name is kept when omitted.
One typed node in a create/update request body (ADR 0020 V8 builder). The
builder authors all four kinds; config is the kind-specific blob the spec
layer persists verbatim, position is the canvas placement.
object
The kind-specific config blob; defaults to {}.
This node’s incoming edges.
One incoming-edge declaration of a node in the typed-node wire shape — the
source node_key plus the handle it leaves the source by (out for an
agent node, true/false for a gate, approved/rejected for a human).
object
The source’s output handle; defaults to out.
agent / gate / human / end / code / loop / notify_*.
WP V16: when set, this node lives INSIDE the named loop frame’s
sub-graph. Omitted (None) means top-level.
A node’s canvas position in a create/update request (ADR 0020 V8).
object
One step in a create/update request body — the v1 agent-only wire shape,
kept so existing callers keep working. The V8 builder sends nodes instead.
object
stop (default) or continue; unknown values fall back to stop.
Examplegenerated
{ "name": "example", "nodes": [ { "config": "example", "depends_on": [ { "handle": "example", "key": "example" } ], "key": "example", "kind": "example", "parent_node_key": "example", "position": { "x": 1, "y": 1 } } ], "steps": [ { "agent_type": "example", "depends_on": [ "example" ], "key": "example", "on_failure": "example", "prompt": "example" } ]}Responses
Section titled “Responses”A workflow plus its graph — the GET/PUT /api/workflows/{id} body.
steps keeps the v1 agent-only wire shape (every agent node in ordinal
order with depends_on rebuilt from the edge set) so older callers keep
working. nodes + edges is the typed-node graph the V8 builder consumes:
every node kind with its config, position and handle-carrying edges.
object
One directed edge as returned to the client (ADR 0020 V8) — a connection
from source’s source_handle output into target.
object
Issue #110: per-workflow concurrency cap; None = unlimited.
One typed node as returned to the client (ADR 0020 V8). Carries the node’s
kind, its kind-specific config blob and its canvas position — the
shape the Svelte Flow builder renders directly.
object
The node kind (node.kind, workflow_nodes.kind CHECK). Runtime stays
String; the spec narrows it to the closed [workflow_svc::NodeKindSpec].
WP V16: when set, this node lives INSIDE the named loop frame’s
sub-graph. The V8 builder wires up parentId / extent: 'parent'
on Svelte Flow nodes carrying this field.
A node’s canvas position in a response body.
object
A workflow step (an agent node) as returned to the client.
ADR 0020 reshapes the schema to typed nodes + edges; this DTO keeps the v1
agent-step wire shape (step_key + depends_on) so the existing web canvas
stays functional — depends_on is rebuilt from the node’s incoming edges.
object
The node kind (node.kind, workflow_nodes.kind CHECK). Runtime stays
String; the spec narrows it to the closed [workflow_svc::NodeKindSpec].
Example
{ "nodes": [ { "kind": "agent" } ], "steps": [ { "kind": "agent" } ]}Structured client error
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Authentication required
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Permission denied
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Workflow not found
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Structured server error
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}