Files
stroblmeandClaude Opus 5 15c3dd5838 Keep a flow's runs when the flow goes
A run record is a record of what ran, so deleting a flow no longer sweeps its
history: `_forget_runs` is gone from the flow-delete path, and `DELETE /runs/{id}`
is the only thing that removes a run one at a time. The in-flight guard stays —
that is about work, not history.

`DELETE /runs?flow=` is the counterpart to the list's flow filter and what a
reseed needs, sharing the four statements with the single-run delete and
refusing the same way while a run of that flow is still going.

A run whose flow is gone reads as one: `useFlowInputs` reports the 404 rather
than an empty declaration set, so the run page says "flow deleted", explains it,
and disables Retry, which the route would refuse anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CL9zvnnvcp1mvA8o7impxk
2026-09-06 14:53:23 +02:00

246 lines
11 KiB
Markdown

# The HTTP API
Everything the browser does, the API does first. The dashboard is a generated
client of this schema, not a privileged path into the engine, so anything you
can click, you can script.
Base URL: `https://api.${DOMAIN}/api/v1`, or `http://127.0.0.1:8000/api/v1` for
a `fluksio serve` instance.
```sh
export FLUKSIO=http://127.0.0.1:8000/api/v1
```
## Authenticating
```sh
export TOKEN=$(curl -s -X POST $FLUKSIO/login/access-token \
-d "username=admin@example.com&password=..." | jq -r .access_token)
curl -s $FLUKSIO/users/me -H "Authorization: Bearer $TOKEN"
```
A bearer token, valid for eight days. `POST /login/test-token` checks one.
Password recovery and reset are `POST /password-recovery/{email}` and
`POST /reset-password/`.
Agents authenticate differently; see [Agents over MCP](agents.md).
!!! tip "The interactive schema"
When `ENVIRONMENT` is not `production`, the full OpenAPI schema is at
`/docs` (Swagger) and `/redoc`, and the raw document at
`/api/v1/openapi.json`. That is the authoritative reference; this page is
the tour.
It is closed in production: the schema enumerates every endpoint the
instance serves, including paths webhook nodes mounted at runtime.
## Flows
| Method | Path | What |
|---|---|---|
| `GET` | `/flows/` | every flow, with node counts, draft and running state |
| `GET` | `/flows/{name}` | one flow — the draft if there is one — with node status and issues |
| `PUT` | `/flows/{name}` | save a draft (`version` must match, or 409) |
| `POST` | `/flows/{name}/publish` | put the draft live |
| `POST` | `/flows/{name}/discard-draft` | throw the draft away |
| `POST` | `/flows/{name}/rename` | rename it |
| `DELETE` | `/flows/{name}` | delete it. Its runs stay — a run is the record of what ran, and only `DELETE /runs` clears one. Refused with 409 while a run of it is `running` or `queued` |
| `GET` | `/flows/node-types` | every node type and its parameter schema |
| `GET` | `/flows/graph` | every flow as one graph — what Home draws |
Reading a flow returns the *draft* when one exists, along with a preview of how
it would run: node statuses and validation issues for the version you are
editing, not the one running underneath it.
### Node source
```sh
curl -s $FLUKSIO/flows/house/nodes/decide/source -H "Authorization: Bearer $TOKEN"
curl -X PUT $FLUKSIO/flows/house/nodes/decide/source -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"code": "def process(x):\n return {\"y\": x * 2}\n"}'
```
`POST .../share` promotes a node's source to the shared library;
`POST .../unshare` gives it a private copy back. `GET /flows/library` lists the
shared sources and which flows use each.
### Running and controlling
| Method | Path | What |
|---|---|---|
| `POST` | `/flows/{name}/start` · `/stop` | activate or tear down its subscriptions and schedules |
| `POST` | `/flows/{name}/pause` · `/resume` | hold messages instead of running them |
| `POST` | `/flows/{name}/step` | release exactly one held message |
| `POST` | `/flows/{name}/validate` | the issues, without saving |
| `POST` | `/flows/{name}/run` | run every node once, from the values you pass |
| `POST` | `/flows/{name}/nodes/{id}/trigger` | feed values into one node |
| `POST` | `/flows/{name}/nodes/{id}/cancel` | kill the worker running it right now |
| `POST` | `/flows/{name}/nodes/{id}/acknowledge` | clear the failure the canvas is marking |
`POST /flows/{name}/run` on a **batch** flow submits a run instead, because
that is what running one means: the parameters, the series and the result are
the point.
## Messages
```sh
curl -s $FLUKSIO/messages/ -H "Authorization: Bearer $TOKEN"
curl -X POST $FLUKSIO/messages/house.setpoint -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"value": 22.5}'
curl -s $FLUKSIO/messages/house.temperature/history -H "Authorization: Bearer $TOKEN"
```
`GET /messages/` is the catalogue: every message any published flow declares,
with its last value. Publishing puts a value into the graph exactly as a
dashboard control does, which means only a message some flow *declares* can be
published to. Flows own the namespace; everything else is a client of it.
## Runs
| Method | Path | What |
|---|---|---|
| `POST` | `/runs/flows/{name}` | queue one run — `{"params": {...}, "seed": 7, "draft": false, "no_cache": false}`. `"cause"` says where it came from — `api` (the default), `cli` or `sdk` |
| `POST` | `/runs/flows/{name}/sweep` | queue many, sharing a `group_id` |
| `GET` | `/runs` | the queryable history: `?flow=`, `?status=`, `?group=`, `?digest=`, `?since=`, `?before=`, `?limit=`, `?offset=` |
| `DELETE` | `/runs?flow=` | every run of one flow, and everything hanging off them. `flow` is required. Refused with 409 while one of them is still going |
| `GET` | `/runs/overview` | one row per flow that has runs, with how many are running or queued |
| `GET` | `/runs/export/metrics?…&name=&stride=&format=` | every selected run's series as one long table: `run, name, step, ts, value` |
| `GET` | `/runs/export/runs?…&params=&metrics=&format=` | one row per run: its inputs as columns, its final numbers, its status and provenance |
| `GET` | `/runs/metrics/names?…` | every metric name the selected runs recorded, distinct; takes the export's own filters |
| `GET` | `/runs/{id}` | one run in full: params, result, per-node record, artifacts |
| `POST` | `/runs/{id}/cancel` | stop it |
| `DELETE` | `/runs/{id}` | forget it, with its nodes, series and artifact rows. Cancel a live one first |
| `POST` | `/runs/{id}/retry` | run the same thing again, as a new run naming this one |
| `GET` | `/runs/{id}/metrics?name=&stride=` | one metric's series, in step order; every series of the run without `name` |
| `GET` | `/runs/series/compare?ids=a,b,c&metric=&x=` | that metric across several runs. `x` is what to plot against: nothing or `step`, `time` (seconds since each run's own first reading), or another metric's name, joined on the step the two share |
Submitting answers immediately with a `queued` run. Wrong parameters (an
undeclared name, a value of the wrong type) come back as a 422 naming the
problem, before anything executes.
`?digest=` filters by the hash of the parameters, which is how you find "every
run that used exactly this configuration".
`?before=` is how a long history is paged: rows come newest first, so handing
back the last row's `created_at` reads the next page whatever landed
meanwhile, which `?offset=` cannot, since a run submitted between two pages
shifts every row down one. `?since=` is inclusive and bounds the other end.
Both exports stream `csv` (the default) or `jsonl`, and take the selection the
history takes plus `?ids=a,b,c`, `?since=` and `?until=`. `export/runs` puts
the inputs that *vary* across the selection in `param.` columns (the sweep
axis) unless `?params=` names them, and the run's numbers in `metric.`
columns. Both are dotted paths into whatever a node returned, so
`?metrics=final_metrics.train_loss` selects a field of a record and the
defaults reach every number inside one. The run id is on every row of both, which is what makes an exported
file a join back to the run rather than a loose table.
`compare` answers in the same shape a chart widget draws, so three training
curves side by side is a widget binding rather than a screen of its own.
See [Runs: pipelines that finish](../concepts/runs.md).
## Artifacts
```sh
curl -X PUT $FLUKSIO/artifacts -H "Authorization: Bearer $TOKEN" \
--data-binary @model.pt
curl -s $FLUKSIO/artifacts/sha256:abc... -H "Authorization: Bearer $TOKEN" -o model.pt
```
Content-addressed, so uploading the same bytes twice stores them once. Node
code normally reaches these through `fluksio.save_artifact` /
`fluksio.load_artifact` rather than here.
## Dashboards and panels
| Method | Path | What |
|---|---|---|
| `GET` | `/dashboards/` | every dashboard, without its contents |
| `GET` | `/dashboards/{name}?draft=true` | the published document, or the editor's copy |
| `POST` | `/dashboards/{name}` | create one |
| `POST` | `/dashboards/from-flow/{flow}` | build `{flow}_results` from a batch flow's declared ports — a chart per streamed metric, a stat per output. 409 if it already exists |
| `PUT` | `/dashboards/{name}` | save a draft |
| `POST` | `/dashboards/{name}/publish` · `/discard` · `/rename` | |
| `GET` `PUT` | `/panels/` | which device shows which dashboards |
| `POST` | `/panels/pair` | start a pairing |
| `GET` | `/panels/pair/{code}` | what is holding a code |
| `POST` | `/panels/{id}/unpair` | drop this panel's credential, keep the panel |
## Secrets, modules, alerts
```sh
curl -s $FLUKSIO/secrets/ -H "Authorization: Bearer $TOKEN" # names only
curl -X PUT $FLUKSIO/secrets/influx-token -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"value": "..."}'
curl -s $FLUKSIO/modules/ -H "Authorization: Bearer $TOKEN"
curl -X POST $FLUKSIO/modules/apply -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"requirements": "numpy>=2\n"}'
curl -s $FLUKSIO/alerts/config -H "Authorization: Bearer $TOKEN"
curl -X POST $FLUKSIO/alerts/test/my-phone -H "Authorization: Bearer $TOKEN"
```
Secrets are write-only over the API: you can list names and set values, never
read one back.
## Observability
| Path | What |
|---|---|
| `/observability/summary` | engine health — always 200, degraded or not — and the `version` it is running |
| `/observability/timeseries` | executions and failures over a window |
| `/observability/flows` | per-flow rollups with a 60-slice trend |
| `/observability/runs` | recent cascades, with `?flow=`, `?since=`, `?until=` |
| `/observability/events?kind=failure\|audit` | what went wrong, or who changed what; narrows by `?flow=`, `?run=`, `?since=`, `?until=` |
| `/observability/dead-letter` | work the engine gave up on |
`GET /utils/health/` is the deep health check the container probe uses: it
fails when the event loop is wedged or the state backend is gone, not just when
the process is dead.
## Workers
| Method | Path | What |
|---|---|---|
| `GET` | `/workers` | what is attached, its labels and how busy it is |
| `POST` | `/workers/tokens` | mint a worker credential (superuser; shown once) |
| `GET` | `/workers/runtime` | the node runner's source, for a host without pip |
| `WS` | `/workers/attach` | where a worker dials in |
See [Remote workers](workers.md).
## Live events
```text
ws://127.0.0.1:8000/api/v1/flows/ws?token=<access token>
```
The websocket authenticates from its query string. It sends a snapshot on
connect and then every engine event: node executions, values published, health
changes, run started and finished. This is what the canvas and the dashboards
draw from.
## Errors
| Code | Means |
|---|---|
| `400` | the request was malformed, or the node refused it with a message |
| `401` / `403` | not signed in, or not allowed |
| `404` | no such flow, dashboard, run or message |
| `409` | someone else saved first — the body carries `current_version` |
| `422` | a parameter, port or binding did not typecheck |
| `503` | that subsystem is not available on this instance |
A 409 on a save or a publish is not an error to retry blindly: it means the
stored version moved past the one you were editing. Re-read, merge, save again.
## Generating a client
The frontend's TypeScript client is generated from the OpenAPI schema
(`make generate-client`). Any OpenAPI generator will do the same for your
language: point it at `/api/v1/openapi.json` on a non-production instance.