All pages
Command line
o41-metrics reads and organizes Metrics from a shell: sign-in, organizations,
members and keys, folders, experiments, series as JSON and series as PNG charts.
Every command prints newline-delimited JSON on standard output, reports errors
on standard error with exit code 1, and addresses experiments by slug and
folders by path, so an agent or a script can drive it without parsing text.
Install
curl -fsSL https://metrics.041.io/install.sh | sh
o41-metrics --versionThe installer verifies SHA-256 and installs o41-metrics in ~/.local/bin for
Linux and macOS on x64 and arm64. It needs no Node.js or npm. Set
O41_METRICS_INSTALL_DIR to choose another directory and put it first on
PATH. Windows is not supported. o41-metrics --help and
o41-metrics <command> --help list every flag.
Update
Run o41-metrics update --check to check the release, or o41-metrics update
to install it. The CLI verifies SHA-256 before replacing the binary.
Standalone installations check for updates in the background at most once every
six hours. Later commands use the updated binary; running commands keep their
version. Set O41_AUTO_UPDATE=0 for all 041 CLIs, or
O41_METRICS_AUTO_UPDATE=0 for Metrics, to keep a fixed version. CI and npm
installations skip automatic updates. If you installed with npm, run
npm uninstall -g @o41/metrics after installing the standalone binary so the
npm copy does not hide it on PATH.
Sign in
o41-metrics auth login # choose browser sign-in or an API key
o41-metrics auth login --device # browser sign-in without the prompt
printf '%s' "$KEY" | o41-metrics auth login --stdin
o41-metrics auth status
o41-metrics auth logout| flag | command | meaning |
|---|---|---|
--device | auth login | sign in with a browser on any device |
--org <org> | auth login | with --device, the organization id, slug or name to act in |
--stdin | auth login | read an organization API key from standard input |
--host <origin> | auth login | API origin to sign in to |
--force | auth logout | delete the login even if its API key cannot be revoked now |
Browser sign-in prints a link and a short code on standard error. Open the link in a browser on any device, for example your laptop when the CLI runs over SSH, sign in or create an account, pick the organization, and confirm the code. The CLI waits for the approval, saves the login and refreshes its tokens as needed. Only a browser sign-in can manage organizations, members and API keys.
A browser sign-in also creates a personal API key of the active organization,
named after you and the machine, and saves it with the login, because
DuckDB and the Python API client read only an
API key from it. The key works only while you are a member of that organization.
org switch and org create replace it with a key of the new organization;
auth logout revokes it. If the key cannot be revoked, logout keeps the login
so you can retry; --force deletes the login anyway and names the key to revoke
with api-key revoke. See organizations and keys.
auth login without --device or --stdin asks which method to use and needs
a terminal; in a script, pass one of the two. auth login --stdin verifies the
key before saving it and prints the host, the organization and the time.
Agents and CI usually skip the saved login and set the key in the environment:
export O41_METRICS_API_KEY=ak_...Host
The CLI talks to https://metrics.041.io unless auth login --host or
O41_METRICS_HOST names another origin; the older https://metrics.syvain.com
is the same service. The host is saved with the login. Browser sign-in is
accepted only for those two production hosts, and a saved browser sign-in
refuses to send its tokens to a different O41_METRICS_HOST.
A key goes only to the host it is paired with. auth login logs in to --host,
then O41_METRICS_HOST, then the saved login's host, then production, and
verifies the new key there. Other commands use O41_METRICS_API_KEY, with
O41_METRICS_HOST, then production; the saved login's host never applies to an
environment key. Without an environment key they use the saved login with the
host it was saved for. If O41_METRICS_HOST names another host, the command
fails with host_mismatch instead of sending the saved key or tokens there.
Usage reports follow the same rules. Empty or Unicode-whitespace-only values are
unset, including API-key input from stdin.
For example, with a saved login for https://custom.example.test, setting
O41_METRICS_API_KEY alone sends that key to https://metrics.041.io, not to
the custom host.
Addressing
- An experiment is its id or its slug:
experiment get mamba-run-001. - A folder is its absolute path or its id:
/models/mamba. The root is/. Paths come from the organization's folder tree, so quote names with spaces:"/ablations/no norm".
A command reads the folder list once to resolve folder references, and looks up
an experiment by its id or slug alone. Only experiment list without --folder
reads every experiment. An unknown reference fails with experiment_not_found
or folder_not_found. An overloaded directory answers these reads with
directory_busy; the command waits as long as it asks and retries for up to 20
seconds, then fails with directory_busy and how long to wait.
Folders
o41-metrics folder list # every folder with its path
o41-metrics folder list /models # direct children of one folder
o41-metrics folder create /models/mamba # creates missing parents; idempotent
o41-metrics folder move /scratch/run-1 /models
o41-metrics folder move /scratch/run-1 / # to the root
o41-metrics folder rename /models/mamba mamba-v2Each folder line is {folderId, name, parentFolderId, path}. folder create
prints the leaf with created: true when it made any folder on the path and
created: false when the whole path existed.
Experiments
o41-metrics experiment list --search mamba # slug substring, case-insensitive
o41-metrics experiment list --folder /models # direct members only
o41-metrics experiment get mamba-run-001
o41-metrics experiment move mamba-run-001 /models/mamba
o41-metrics experiment rename mamba-run-001 "Mamba, lr 3e-4"
o41-metrics experiment catalog mamba-run-001
o41-metrics experiment annotations mamba-run-001 --limit 20| command | prints one line per |
|---|---|
experiment list [--folder <folder>] [--search <text>] | experiment: experimentId, slug, displayName, folderId, folderPath |
experiment get <experiment> | the experiment: status, description, meta, error, lifecycle timestamps, folderPath and url in the app |
experiment move <experiment> <folder> | the moved experiment with its new folderPath |
experiment rename <experiment> <name> | the experiment with its new displayName; "" clears it |
experiment catalog <experiment> | series: seriesName and the metadata keys with every value seen |
experiment annotations <experiment> [--limit N] | annotation, newest first |
The API returns at most 100,000 annotations of one experiment; when the reply reaches that count, a warning on standard error says older ones were omitted.
A display name is what the app shows for a run; the slug stays its identity, and every command still takes the slug or the id.
Series
series query prints one line per experiment, series and metadata partition,
with the points as aligned columns:
{
"experimentId": "019e...",
"seriesName": "loss",
"metadata": { "split": "valid" },
"data": {
"step": [200, 400],
"timestampMs": [1789776000000, 1789776060000],
"value": [2.91, 2.64]
}
}o41-metrics series query mamba-run-001 --name loss --filter split=valid
o41-metrics series query mamba-run-001 > run-001.jsonl # every series
o41-metrics series query run-a run-b --name loss --x-axis timestamp
o41-metrics series latest run-a run-b --name loss --name tokens
o41-metrics series query run-a --name loss --from-step 1000 --to-step 2000
o41-metrics series query run-a --name loss --since 2026-11-05T00:00:00Z| flag | meaning |
|---|---|
<experiment>... | one or more experiment ids or slugs |
--name, -n | series name, repeatable; omit to read every series in each experiment's catalog |
--filter, -f | key (key present) or key=value (exact match), repeatable; all must match |
--x-axis | step (default) or timestamp; the axis points are sorted by |
step is null for points stored without one. For more than a few experiments,
or anything you would group or join, use DuckDB.
series latest returns the highest-step point per metric and metadata
partition, using the same JSON shape. Use --x-axis timestamp for the newest
event timestamp. Omit --name to include all metrics. Both latest and query
accept inclusive --from-step / exclusive --to-step, or inclusive --since /
exclusive --until (ISO 8601 or epoch milliseconds). Use one kind of bounds per
query. Latest reads with bounds choose the latest point within the window. Stale
latest responses fail with stale_series; latest validates completion before
printing each experiment.
series render draws the same selection as a PNG. Every experiment, series and
metadata partition becomes one line, so narrow it with --filter.
o41-metrics series render run-a run-b --name loss --filter split=valid -o loss.png
o41-metrics series render run-a --name loss --display-mode subplots -o loss.png| flag | meaning |
|---|---|
--name, -n | series name, repeatable; at least one is required |
--filter, -f, --x-axis | as for series query |
--output, -o | PNG path, or - for standard output; required |
--title | chart title |
--width | 320 to 2,400 px, default 1,200 |
--height | 240 to 1,600 px, default 720 |
--theme | light (default) or dark |
--display-mode | lines (default), one chart; or subplots, one per line |
With a file path, series render prints {"path": ..., "bytes": ...}. The
points are sent to the public Metrics renderer at
https://metrics-renderer.syvain.com, which returns the image.
Views
A view is a saved app workspace: which runs, which charts, which axes. Views
belong to the organization. link prints an app link to a state without saving
it, the way to show someone a result.
o41-metrics view list
o41-metrics view get "LR sweep"
o41-metrics view create "LR sweep" --folder /mamba/lr-sweep --state '{"defaults":{"logY":true}}'
o41-metrics view update "LR sweep" --name "LR sweep, log y"
o41-metrics view delete "LR sweep, log y"
o41-metrics view link --experiment run-a --experiment run-b --state '{"groupBy":"experiment"}'| command | prints |
|---|---|
view list | one line per view: viewId, name, timestamps and its app url |
view get <view> | the view with its full state and url |
view create <name> [--state <json>] [selection flags] | the saved view: viewId, name, updatedAtMs, url |
view update <view> [--name <name>] [--state <json>] | the same; the state is replaced whole, the name kept unless given |
view delete <view> | viewId, name and deleted: true |
view link [--state <json>] [selection flags] | url and the full state it carries |
A view is its id or its exact name; names are unique in the organization.
--state is the workspace state as JSON, where
every field is optional. The selection flags add runs to the state's selection
by name instead of id: --folder <path or id> selects a folder with every run
in it, including later ones, and --experiment <slug or id> one run. Both
repeat.
Organizations, members and keys
These need a browser sign-in. Changing members, invitations and API keys needs the admin role.
o41-metrics org list # your organizations; current marks the active one
o41-metrics org switch my-lab # id, slug or name
o41-metrics org create "My Lab" # you become admin; switches to it
o41-metrics member list
o41-metrics member role ada@example.com admin
o41-metrics member remove ada@example.com
o41-metrics invitation create ada@example.com --role member
o41-metrics invitation list
o41-metrics invitation revoke orginv_...
o41-metrics api-key create ci --expires-in-days 90 | jq -r .secret
o41-metrics api-key list
o41-metrics api-key revoke ak_...| command | arguments and flags |
|---|---|
member role | <member> <role>: user id or email; admin, member or a role key such as org:admin |
member remove | <member> |
invitation create | <email> [--role <role>], default member |
invitation revoke | <invitation id> |
api-key create | <name> [--description <text>] [--expires-in-days N], default never expires |
api-key revoke | <api key id> |
api-key create prints the key with its secret once; nothing can show it
again. A revoked key can still be accepted for up to a minute. More on roles and
keys in organizations and keys.
Docs search
docs search finds the pages of these docs that answer a question, best first.
It needs no login and asks O41_METRICS_HOST, or production when that is unset.
o41-metrics docs search "how do I compare runs in DuckDB"
o41-metrics docs search series render legend --limit 3The words are joined with spaces into one query. --limit takes 1 to 20 pages,
default 5. It prints one line, {"query", "site", "hits"}; each hit has
title, url, markdownUrl, score from 0 to 1, and snippets, the matching
passages. Fetch a hit's markdownUrl to read the whole page. An error from the
site fails with http_error and the site's message.
Feedback
feedback sends a bug report or an idea to the 041 team. It needs no login, so
it works when signing in is what broke, and it attaches what the team needs to
reproduce: the CLI and Bun versions, the OS and its version, the host, and the
login's organization and user (never a key). It sends no working directory, file
or environment variable.
o41-metrics feedback --bug "series query --x-axis timestamp drops the last point"
o41-metrics feedback "a --since flag on experiment list would save a folder walk"
o41-metrics feedback --bug --reply-to me@example.com - < report.md # long reports from stdinIt prints the receipt, {"id", "surface", "receivedAt"}. --reply-to takes an
email address or a URL, such as an issue, where the team can answer.
Environment variables
| variable | effect |
|---|---|
O41_METRICS_API_KEY | organization API key; when set, the saved login is not read |
O41_METRICS_HOST | API origin for the environment key; a saved login is used only if this names its host |
O41_METRICS_ANALYTICS | off, 0 or false stops the usage report |
DO_NOT_TRACK | any value but empty, 0 or false stops the usage report |
XDG_CONFIG_HOME | the login is saved in $XDG_CONFIG_HOME/o41-metrics/auth.json, default ~/.config/o41-metrics/auth.json |
The login file is written with mode 0600.
Usage analytics
After each command the CLI sends one usage report to the Metrics host it talks
to: the command path, such as o41-metrics experiment list, never an argument
or a value; whether it succeeded, and its error code if not; how long it took;
the CLI version, operating system and architecture; whether CI is set; whether
a coding agent runs it; and whether standard output is a terminal. The API
records it for the user and organization of the credential the command uses, and
without one for nobody. The report waits at most 1.5 seconds, and a failed
report never changes the command's output or exit code.
O41_METRICS_ANALYTICS=off or DO_NOT_TRACK=1 turns it off.
Output contract
- Standard output carries only results: one JSON object per line, or PNG bytes
for
series render -o -. A command that finds nothing prints nothing and exits 0. - Standard error carries sign-in prompts, warnings and errors. An error is one
line,
Error (<code>): <message>, where<code>is stable, for examplenot_logged_in,experiment_not_foundor an API error code. - The exit code is 0 on success and 1 on any failure.
- Nothing asks a question except
auth loginwithout--deviceor--stdin, and that one refuses to run without a terminal.
Related: working as an agent, DuckDB, organizations and keys, HTTP API.