Files
app/docs/code/cli.md
T
stroblmeandClaude Opus 5 4479eeb726
Docs / docs (push) Successful in 38s
Playwright Tests / test-playwright (1, 2) (push) Successful in 2m56s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m7s
pre-commit / pre-commit (push) Failing after 2m17s
Test Backend / test-backend (push) Successful in 2m54s
Compose Smoke Test / test-compose (push) Successful in 44s
Playwright Tests / merge-reports (push) Successful in 1m17s
Follow a record into its fields, name the metrics, name the version
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
2026-08-27 20:42:19 +02:00

17 KiB

The fluksio command

pip install fluksio

Installs the engine and the fluksio command. Python 3.12 or newer, Linux or macOS.

There is a second, smaller distribution — fluksio-worker — for a machine that should only run nodes for an engine elsewhere. It has none of the engine in it. See Remote workers.

The command is two things at once: serve, enroll and worker are an installation, while login, sync, run, runs, sweep and status talk to one that may be anywhere.

Where an installation lives

.fluksio beside your code, found the way .git is: from the working directory, or any directory above it. Two repositories on one machine are therefore two engines, with their own flows, runs and token. fluksio serve makes one where there is none, and it ignores itself from within — a .gitignore of *, so a database and a credential cannot be committed by accident.

--global uses ~/.fluksio instead, shared by every directory. --data-dir (or FLUKSIO_HOME) names any directory outright and wins over both.

fluksio serve

Runs the engine.

fluksio serve

On the first start it creates an admin account and prints its password once. Nothing else has to be running: no database server, no message broker, no Docker.

Option Default What it does
--data-dir PATH ./.fluksio (or $FLUKSIO_HOME) where this installation keeps everything
--host HOST 127.0.0.1 what to bind
--port PORT 8000 what to listen on
--log-level LEVEL info uvicorn's log level
--admin-email ADDR admin@example.com the account created on first run
--admin-password PW generated set it instead of having one generated
--enroll CODE pair with a portal as part of coming up
--portal URL the portal --enroll redeems at

--enroll with --portal is the one-command setup: it pairs before the engine starts, so the connection is dialled as part of coming up rather than needing a restart. It is skipped if the installation is already enrolled.

!!! warning "One process"

`fluksio serve` holds the flow engine. A second one is a *second engine* —
duplicated subscriptions, duplicated cron ticks, two webhooks answering the
same path. Run one, and distribute work with
[workers](workers.md) instead.

!!! note "$HOME on a cluster"

A login node's home directory is often NFS, where SQLite's write-ahead log
does not work — the database would be locked or corrupt. `fluksio serve`
warns when it notices; point `--data-dir` at local disk.

What it prints

Created the admin account admin@example.com
  password: k3Qm-8vTpLdX
  Shown once. Change it from the dashboard.
Fluksio 0.1.0 — data in /home/you/.fluksio
  API      http://127.0.0.1:8000/api/v1
  No portal. Pair this installation with:
    fluksio enroll <code>

An enrolled installation says which portal it is on instead, and notes that the dashboard is served from there rather than here.

fluksio enroll

Pairs an existing installation with a portal.

fluksio enroll ABCD-1234
Option What it does
--portal URL a portal of your own, instead of https://hub.fluksio.com
--as EMAIL the local account a portal session arrives as
--data-dir PATH which installation, if not the one this directory is in

Get the code from the portal under Installations → Add installation. It is single-use and expires in fifteen minutes. --as matters when the installation has several superusers — without it, enrolment refuses rather than guessing.

Afterwards, fluksio serve dials the portal as it comes up, and keeps dialling: a portal that restarts, a wifi that changes, a laptop that suspends and wakes somewhere else all end the same connection, and the link is put back up without anybody noticing. A connection that stood up and then dropped is retried at once; one that never stood up waits a little longer each time, up to half a minute. See Accounts and the portal.

fluksio worker

Runs nodes for an engine elsewhere. Everything after worker belongs to the agent's own parser — it is the same program fluksio-worker installs, so the two are interchangeable:

fluksio worker --url wss://api.example.com/api/v1/workers/attach \
               --token "$FLUKSIO_WORKER_TOKEN" --labels gpu

See Remote workers.

Talking to an engine

The commands below are the client half: they run wherever you work, and address an engine over its API rather than being one — except under --local, which boots one inside the command instead.

fluksio login

fluksio login --url https://api.example.com

For an engine somewhere else. One you started yourself needs no login: fluksio serve writes the token as it comes up and says where it put it.

The token goes in this project's .fluksio/client.json, or with --global in ~/.fluksio/client.json. Every command below reads it from there — nearest first, walking up from the working directory — or from FLUKSIO_URL and FLUKSIO_TOKEN, or from its own --url and --token. A token an older version wrote to ~/.config/fluksio/client.json is still read.

fluksio sync

fluksio sync [PATH_OR_MODULE ...]        # default: the current directory

Imports what you name, collects the flows the decorators declared, and uploads each one with a generated import shim per node. A directory that is a package is walked; a dotted name is imported as it stands; nothing is loaded from a file path, because the shim has to import the same way.

Flag What it does
--dry-run print the flow documents and shims, upload nothing
--no-publish leave the upload as a draft
--force overwrite a flow, or a node body, that was edited on the canvas

Every sync retires the engine's workers, including one that had nothing to upload — a worker holds your package in memory, so an edit to it is invisible until the process goes. See Getting started: data science.

fluksio run

fluksio run train --lr 0.05 --seed 7 [--wait]

Syncs the working directory, then submits a run — so the command after an edit is this one and nothing else. Flags that are not its own are the flow's inputs, typed by what the flow declares them as. --wait blocks until the run finishes and exits non-zero if it failed. --follow waits as well, and prints the numbers the run reports as they arrive:

  train.loss[14] = 3.40295e-06

Ctrl-C while either is waiting cancels the run on the engine rather than only stopping the watching, and exits 130. --timeout SECONDS gives up waiting after that long and leaves the run going; it is nothing to do with a node's own timeout.

--seed N is the experiment's seed, and it does three things. It is recorded on the run, so what a result came from is answerable later. It goes into the digest that identifies a run's inputs, so two runs of one configuration with different seeds are different runs rather than a cache hit. And if the flow declares an input named seed, that is what fills it — so the number the run is labelled with is the number your code actually drew from, instead of merely looking like it. A flow that declares no such input still records it, and nothing reads it. Sweep over seeds with --param seed=1,2,3.

Any input takes what a previous run produced, named rather than typed out — a checkpoint, but equally a config object nobody wants to paste into a shell:

fluksio run evaluate --dataset @run:1758042000123-9f2ab41c.dataset
fluksio run evaluate --dataset sha256:6dd1f0…
fluksio run train --meta @run:1758042000123-9f2ab41c.dataset_meta

@run:<id>.<output> is whatever that run's output was, whole and with its own type; a bare digest is the content itself, resolved into a reference. Both spellings are reserved on every input, str included, so an input that has to carry one of them literally cannot. Passing the value as JSON still works and is what a script that already holds one does — the same thing flow.submit(dataset=run.result["dataset"]) does from Python.

Run a flow with no parameters at a terminal and it asks for them, one line per declared input, with the declared value in brackets:

lr (float) [0.05]: 0.01
epochs (int) [10]:
dataset (artifact): @run:1758042000123-9f2ab41c.dataset

Enter keeps what is in brackets, so pressing it through the lot runs the defaults. Nothing changes for a scripted run: passing any parameter, or piping the command, skips the questions, and --defaults skips them explicitly.

--no-sync runs what is already on the engine. Worth it in a tight loop where you know nothing changed, since syncing retires the workers and the next call pays its imports again. A directory that declares no flows syncs nothing and says nothing — a flow drawn on the canvas is run the same way.

--no-cache executes every node, including one an earlier run already answered. See Stage caching.

--local boots the engine inside this process instead of talking to a served one, so there is no fluksio serve terminal to keep open. It is the same installation either way — the same .fluksio, the same database, artifacts and run history — so a run made this way and a run made through a served engine cache against each other. It always waits, because the engine it starts lives exactly as long as the command. Starting one costs a few seconds of worker pool and module reconcile, against the ~15 ms of submitting to an engine that is already up: --local is for "I just want to run it", not for a loop you are iterating in.

fluksio status

fluksio status [--watch]

The home screen's top half in a terminal: whether the engine is healthy and what is wrong if not, whether it is paired with a portal and reaching it, then every flow with its state, its node count and whether it has unpublished changes, and the last few runs and failures under them.

A resources line names each machine the engine can run a node on and how much of it is in use, plus how many nodes are queued for one. It is absent on an engine that accounts for nothing.

The portal reads one of three ways. no portal means this installation was never enrolled. portal hub.fluksio.com means the link is up. portal unreachable names the error, and is the one worth acting on — the dashboard is served from the other end, so nobody can reach it while that is showing.

--watch keeps it on screen and refreshes every five seconds until Ctrl-C — the cadence the dashboard polls at, since nothing here moves faster. It needs a terminal; without one, run it without --watch and the output pipes cleanly.

--local reads the flows and history out of this directory with no engine served, the same way runs --local does. It cannot be watched: an in-process engine is the command itself, so nothing changes under it.

fluksio runs

fluksio runs [--flow train] [--limit 20]

The runs an engine has recorded, newest first: id, status, flow, duration, the commit of the repository it came from, and its parameters. Statuses are coloured when a terminal is reading the output — ok green, error red, cached cyan. Parameters are clamped to 80 characters so a flow taking a few kB of JSON still lists as a table; Client.runs() is where the whole value is read. --local reads the same history from an in-process engine, without one having to be served.

fluksio flavors

fluksio flavors

The named sizes a node can ask for — @node(resources="gpu-small") — with the cores, memory and cards each stands for. Editing them is the Workers screen or POST /api/v1/flavors; this is the read.

fluksio sweep

fluksio sweep train --param lr=0.1,0.01 --param epochs=10,50 --wait

Every combination of the parameter lists, submitted as one group — four runs above, sharing a group_id and executing in parallel. Values are typed by the flow's inputs, the same as run's are, and --seed, --no-sync, --no-cache and --local mean what they do there. --wait blocks until all of them are finished and exits non-zero if any failed.

fluksio export

fluksio export metrics --flow train --list
fluksio export metrics --flow train --name train.train_loss --stride 10 -o curves.csv
fluksio export runs    --flow train --status ok > arms.csv

The two tables an analysis reads. export metrics is the long one — a row per run, metric and step — which is what a plotting library takes without reshaping; --name keeps the metrics it lists and --stride keeps every Nth point of each curve. export runs is the wide one: a row per run with its inputs as columns, its final numbers, its status, its duration and the commit and digest of the code it ran.

The inputs that become columns are the ones that vary across the selected runs — the axis of the sweep, which is what a comparison is read along — unless --params lr,seed names them. --metrics narrows the final numbers the same way.

A node usually returns a record rather than a scalar, so both sides take dotted paths into one: --metrics final_metrics.train_loss,test_metrics.known.perfect selects three fields rather than two blobs, and --params model.ansatz does the same for an input. The defaults reach the same depth — every number a result carries becomes a column wherever it sits, and inputs are compared leaf by leaf, so two configurations differing in one field give that field rather than two records that are merely not equal.

Metric names are flow-qualified — a node of train writing train_loss records train.train_loss — so --list prints the names the selected runs carry when the spelling is not obvious.

Both take --flow, --run ID (repeat it), --group, --status, --since, --until and --local, and both put the run id on every row: it is the join back to the run page and to what the run made.

--format is csv (the default), jsonl or parquet; output goes to stdout unless -o FILE names somewhere. Parquet keeps the types and needs pyarrow — pip install 'fluksio[parquet]' — and a file to write, since it is not a stream. In a notebook, Client.export_metrics() and Client.export_runs() answer the same rows as a list of dicts, which pandas.DataFrame takes directly.

What lives in the data directory

.fluksio/               (or ~/.fluksio, with `--global`)
├── client.json         the token `serve` wrote, mode 600
├── .gitignore          `*` — a database and a credential, ignored from within
├── fluksio.db          SQLite: users, runs, metrics, observability, agents
├── flows/              a git repository — one directory per flow
│   ├── house/
│   │   ├── flow.json           the published structure
│   │   ├── nodes/*.py          the published node code
│   │   ├── flow.draft.json     unpublished edits, if any
│   │   └── nodes.draft/*.py
│   ├── _lib/           shared node sources
│   ├── _dashboards/    dashboards, drafts and all
│   └── requirements.txt        what the Modules screen installs
├── artifacts/          content-addressed bytes, two levels deep
├── user-venv/          the interpreter your node code runs on
├── secrets.enc         encrypted credentials, deliberately outside flows/
├── alerts.json         alert channels and rules
├── panels.json         wall-panel pairings
├── oauth-key.pem       signs agent tokens
├── cloud.json          the portal enrolment, if there is one
├── secret_key          signs sessions and derives the secrets key
└── env                 optional settings file

Two things follow from this layout and are worth internalising:

flows/ is a real git repository. git log is the history of every change anyone made to any flow. A run records the commit it ran at, so git show on that hash is literally the code that produced the number.

Backing up the data directory backs up the installation. Everything else is rebuildable. Copy it while the engine is stopped, or use SQLite's online backup for the database if it is not.

Settings

Settings come from the environment, or from an env file in the data directory. The ones you are most likely to touch:

Variable Default What it does
DATA_DIR ./.fluksio via the CLI; ~/.fluksio with --global everything below it derives from this
DATABASE_URL SQLite in the data dir any SQLAlchemy URL
NODE_VENV auto which interpreter node code runs on: auto adopts the venv Fluksio was installed into, managed builds one of its own, or name an interpreter
REDIS_HOST unset flow state in Redis instead of memory; survives a restart
FRONTEND_HOST the address used in mails, OAuth metadata and panel links
ENVIRONMENT local production closes the interactive API schema
MCP_ENABLED false opens the agent endpoint
SECRET_KEY generated once signs sessions, derives the secrets key

The full list is in Configuration.