Flows and dashboards are created by POST under an issued or superuser-pinned id; PUT only updates; deleted flows leave a tombstone with their key; the controller stores by id and runs by key, hooks live under /hooks/<flow id>; a key with state or queued work behind it is refused until cleared

This commit is contained in:
2026-11-02 17:41:23 +01:00
parent fae85f5ecb
commit 08a6376836
63 changed files with 2832 additions and 936 deletions
+55 -31
View File
@@ -52,29 +52,47 @@ a token. Password recovery uses `POST /password-recovery/{email}` and
| Method | Path | Action |
|---|---|---|
| `GET` | `/flows/` | list flows and their state |
| `GET` | `/flows/{name}` | read the draft when present, with status and issues |
| `PUT` | `/flows/{name}` | save a draft; send the current `version` |
| `POST` | `/flows/{name}/publish` | publish; send `{"version": <current-version>}` |
| `POST` | `/flows/{name}/discard-draft` | discard unpublished edits |
| `POST` | `/flows/{name}/duplicate` | copy into an unpublished `{name}_copy`; code nodes need a superuser |
| `DELETE` | `/flows/{name}` | delete; refused while a run is queued or running |
| `GET` | `/flows/` | list flows and their state; `?name=<key>` lists the one with that key |
| `POST` | `/flows/` | create a flow from a document; it starts as a draft |
| `GET` | `/flows/{id}` | read the draft when present, with status and issues |
| `PUT` | `/flows/{id}` | save a draft of an existing flow; send the current `version` |
| `POST` | `/flows/{id}/publish` | publish; send `{"version": <current-version>}` |
| `POST` | `/flows/{id}/discard-draft` | discard unpublished edits |
| `POST` | `/flows/{id}/duplicate` | copy into an unpublished `<key>_copy` with an id of its own; code nodes need a superuser |
| `DELETE` | `/flows/{id}` | delete; refused while a run is queued or running |
| `GET` | `/flows/node-types` | list types and parameter schemas |
| `GET` | `/flows/graph` | read the graph shown on Home |
Deleting a flow keeps its run history. Delete runs separately.
A flow has an `id` and a key, its `name`. The id says which flow it is and
never changes. Every path takes the id, never the key. The key is the
namespace of the flow's messages (`house.temperature`) and is unique among
flows. A flow created before ids existed has its key as its id.
A flow document carries an `id` beside its `name`. The id never changes, and
for now it equals the name, which is the `{name}` in these paths. A save may
leave the id out, but cannot change the id or the name.
`POST /flows/` takes the same document as a save, without `version`, and
answers 201 with the new flow. The server issues an opaque id such as
`f_k3q7m2xa`. A superuser may pin one instead by sending `id`, which the Demo
and restores do. A pinned id has to be free and cannot be a path of this API
like `graph`. If runs or metrics of a deleted flow are still filed under it,
the new flow takes that history back and has to use the key it was recorded
under. Creating a flow with code nodes needs a superuser, as saving one does.
A key that still has values or queued work from an earlier flow is refused
with 409 until that is gone.
`PUT /flows/{id}` only updates. It answers 404 for an id nobody holds, and 409
for a body whose `name` differs from the stored key. Its body may leave `id`
out.
Deleting a flow keeps its run history, which is shown under the key the flow
last had. Delete runs separately. The flow's values and queued work go with
it, so its key is free for a new flow, which starts with an empty history.
### Node source
```sh
curl "$FLUKSIO/flows/house/nodes/decide/source" \
curl "$FLUKSIO/flows/f_k3q7m2xa/nodes/decide/source" \
-H "Authorization: Bearer $TOKEN"
curl -X PUT "$FLUKSIO/flows/house/nodes/decide/source" \
curl -X PUT "$FLUKSIO/flows/f_k3q7m2xa/nodes/decide/source" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"code": "def process(x):\n return {\"y\": x * 2}\n"}'
@@ -87,7 +105,7 @@ Function and Command nodes, changing Command settings, or changing
Ordinary source edits stay in the draft until publishing. Shared library
source changes affect its users immediately, and the audit trail records them
like a publish.
`POST /flows/{name}/nodes/{id}/share` promotes a source to the library;
`POST /flows/{id}/nodes/{node}/share` promotes a source to the library;
`/unshare` makes it private again. `GET /flows/library` lists shared sources.
`DELETE /flows/library/{name}` retires one, and answers 409 while nodes still
use it. With `?detach=true` each of those nodes keeps a copy of the code, in
@@ -97,14 +115,18 @@ the published flow and the draft, and the shared source is deleted.
| Method | Path | Action |
|---|---|---|
| `POST` | `/flows/{name}/start`, `/flows/{name}/stop` | activate or stop a live flow |
| `POST` | `/flows/{name}/pause`, `/flows/{name}/resume` | hold or release incoming work |
| `POST` | `/flows/{name}/step` | release one held message |
| `POST` | `/flows/{name}/validate` | validate the current draft |
| `POST` | `/flows/{name}/run` | run the current draft once with `{"inputs": {...}}` |
| `POST` | `/flows/{name}/nodes/{id}/trigger` | feed a node with `{"values": {...}}` |
| `POST` | `/flows/{name}/nodes/{id}/cancel` | stop active node code |
| `POST` | `/flows/{name}/nodes/{id}/acknowledge` | clear its failure marker |
| `POST` | `/flows/{id}/start`, `/flows/{id}/stop` | activate or stop a live flow |
| `POST` | `/flows/{id}/pause`, `/flows/{id}/resume` | hold or release incoming work |
| `POST` | `/flows/{id}/step` | release one held message |
| `POST` | `/flows/{id}/validate` | validate the current draft |
| `POST` | `/flows/{id}/run` | run the current draft once with `{"inputs": {...}}` |
| `POST` | `/flows/{id}/nodes/{node}/trigger` | feed a node with `{"values": {...}}` |
| `POST` | `/flows/{id}/nodes/{node}/cancel` | stop active node code |
| `POST` | `/flows/{id}/nodes/{node}/acknowledge` | clear its failure marker |
Values, states and node names in the answers are qualified with the key, as
in `house.temperature` and `house.decide`. Node ids in the paths are the
flow's own, unqualified.
A preview run can have real side effects. On a batch flow, `/run` submits a
recorded run; use the run endpoints below for parameters and tracking.
@@ -192,13 +214,13 @@ reclaiming space sooner.
| Method | Path | Action |
|---|---|---|
| `GET` | `/dashboards/` | list dashboards |
| `GET` | `/dashboards/{name}?draft=true` | read a draft; omit the query for published content |
| `POST` | `/dashboards/{name}` | create |
| `POST` | `/dashboards/from-flow/{flow}` | create a batch flow's results dashboard |
| `PUT` | `/dashboards/{name}` | save a draft |
| `POST` | `/dashboards/{name}/publish`, `/dashboards/{name}/discard` | publish or discard |
| `POST` | `/dashboards/{name}/duplicate` | copy into an unpublished `{name}_copy` |
| `GET` | `/dashboards/` | list dashboards; `?name=<key>` lists the one with that key |
| `POST` | `/dashboards/` | create an empty draft from `{"name": ..., "title": ...}` |
| `GET` | `/dashboards/{id}?draft=true` | read a draft; omit the query for published content |
| `POST` | `/dashboards/from-flow/{flow id}` | create and publish a batch flow's results dashboard, `<key>_results` |
| `PUT` | `/dashboards/{id}` | save a draft of an existing dashboard |
| `POST` | `/dashboards/{id}/publish`, `/dashboards/{id}/discard` | publish or discard |
| `POST` | `/dashboards/{id}/duplicate` | copy into an unpublished `<key>_copy` with an id of its own |
| `GET` | `/devices/` | list devices, with `online`, `last_seen`, `report` and `reported_at` |
| `POST`, `GET`, `PUT`, `DELETE` | `/devices/{id}` | create, read, save or delete one; a save sends the `version` it read and gets 409 when someone saved first. A read adds `io` and `bindings`, its ports as the device nodes of published flows wire them |
| `POST` | `/devices/pair` | start device pairing, optionally with `{name, board, firmware}`; answers `code`, `secret` and the two-character `check` the device shows beside its code; unauthenticated, five a minute per address (the portal limits its own), the oldest of 50 waiting requests from the same side gives way |
@@ -210,8 +232,10 @@ reclaiming space sooner.
| `PUT` | `/devices/{id}/report` | what the device has, from its own credential only; at most 16 KB, and 64 KB once its named `config_schemas` are put in place (413 above either), replaces the last report, read leniently (see [What a device reports](../interface/dashboards.md#what-a-device-reports)) |
| `GET` | `/devices/{id}/ice` | STUN and TURN servers for a live view of the device, as `{ice_servers: [{urls, username?, credential?}], ttl}`, with TURN credentials of the caller's own; 403 for whoever may not watch its camera, and for a device credential |
A dashboard document carries an `id` the way a flow does. Dashboards have ids
of their own, so a flow and a dashboard may share one.
A dashboard has an `id` and a key the way a flow does, and its paths take the
id. A new one gets an opaque id such as `d_k3q7m2xa`, or one a superuser pins
with `id`. Dashboards have ids of their own, so a flow and a dashboard may
share one. `PUT` only updates, as for a flow.
`/panels/pair`, `/panels/pair/{code}` and `GET /panels/{id}` answer as their
`/devices/` counterparts, for firmware that has not updated yet.
+1 -1
View File
@@ -69,7 +69,7 @@ without an inbound port. Replace `portal.example.com` with your portal's API
address in these examples:
- A webhook or a served WebSocket:
`https://portal.example.com/i/<instance-id>/hooks/<flow>/<url>/<secret>`,
`https://portal.example.com/i/<instance-id>/hooks/<flow-id>/<url>/<secret>`,
with `wss://` for a socket.
- An agent: `https://portal.example.com/i/<instance-id>/mcp`, see
[Agents over MCP](../code/agents.md).
+5 -2
View File
@@ -61,7 +61,9 @@ unwrapped only if it contains the port's name as a key.
The ports pick the direction: outputs only make a **listener**, inputs a
**sender**. A listener is a **webhook**: the engine mounts a route at
`/hooks/{flow}/{url}/{secret}` while the flow runs. A sender requests `url`
`/hooks/{flow id}/{url}/{secret}` while the flow runs. The flow's id is in the
URL rather than its key, so the URL stays the same when the key changes, and
for a flow made before ids existed it reads as the key. A sender requests `url`
and publishes a JSON object response on its outputs, one key per output port.
Keys no port declares are dropped, and a response that is not an object
publishes nothing.
@@ -93,7 +95,8 @@ one), while a reverse proxy in front of the engine logs the path as it sees it.
The URL picks the role: `ws://…` or `wss://…` connects to a server and
reconnects when it drops; a path such as `/ticker` is served at
`/hooks/{flow}/{url}/{secret}` while the flow runs. The ports pick the
`/hooks/{flow id}/{url}/{secret}` while the flow runs, under the flow's id like
a webhook. The ports pick the
direction: outputs only make a **listener**, inputs a **sender**. A served
sender sends to every connected client.