Three things the first export pass got wrong for a real study. **Dotted paths.** A node returns a record, not a scalar — the numbers arrive inside `final_metrics` — so `--metrics final_metrics.train_loss` yielded an empty column and `--metrics final_metrics` yielded the whole record in one cell. Both sides of the wide table now take dotted paths, and the defaults reach the same depth: every number a result carries is a column named by its path, and inputs are compared leaf by leaf, so two configurations differing in one field give that field as the axis rather than two blobs that are merely not equal. Lists stay whole — a curve belongs in the long table. **`--list`.** Metric names are flow-qualified, so `--name train_loss` matched nothing and said only that. `fluksio export metrics --list` prints the names the selection carries, and an empty export made with `--name` points at it. **A version to compare.** The CLI ships ahead of the engine and a stale one answered a flat 404 with nothing anywhere in the API to tell how old it was. The engine reports `version` on `/observability/summary`, `fluksio status` prints it, and a 404 from export now names both versions — or says "older" when the field itself predates the engine. Bumped to 0.1.5, which is what makes the number worth reading. Also formats `flow/metrics.py`, which had been committed unformatted and was the last `ruff format --check` failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A9Hdrmf2cwNABCnE5x9UJa
11 KiB
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 installation.
export FLUKSIO=http://127.0.0.1:8000/api/v1
Authenticating
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.
!!! 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 on purpose: the schema enumerates every endpoint
the installation serves, including the 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 |
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
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, and a call that quietly did something else would be a trap.
Messages
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= |
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?…¶ms=&metrics=&format= |
one row per run: its inputs as columns, its final numbers, its status and provenance |
GET |
/runs/{id} |
one run in full: params, result, per-node record, artifacts |
POST |
/runs/{id}/cancel |
stop it |
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.
Artifacts
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
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.
Live events
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 installation |
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 installation.