The job ledger — guide
Class:
guide· audience: a techie reading the ledger or running a container as a job. The what is injobs-design.md; the why injobs-reasoning.md.
f0 — see what nornir is doing (the whole workflow)
Nearly every long op nornir runs is already a tracked job — you don't start jobs, you read them and drive the two ops that are jobs by hand (a container run).
- List —
nornir jobsprints the ledger newest-first. - Filter —
--kind,--running,--limit. - Script it —
--jsonemits the sameJobRecordarray the viz renders. - Run a container as a job —
nornir container …tracks a one-shotpodman/docker runlike any other op.
Local vs server is automatic: with NORNIR_SERVER set, nornir jobs reads the server's ledger over Viz.Jobs; otherwise it opens the local jobs.redb read-only (snapshotting it aside if a running server holds the writer lock).
f1 — reading the ledger
nornir jobs # newest-first table, 50 rows
nornir jobs --running # only non-terminal (queued / running) jobs
nornir jobs --kind bench_run # one op family
nornir jobs --kind release_run --limit 10
nornir jobs --json # a JSON array of JobRecord (CLI ⟺ viz parity)
nornir jobs --json | jq '.[] | select(.status=="failed")'
The table columns are STATUS · KIND · TARGET · WORKSPACE · ELAPSED · RESULT, and it ends with a one-line summary (N job(s) · R running · F failed):
STATUS KIND TARGET WORKSPACE ELAPSED RESULT
✓ done bench_run facett nordisk 1240 ms bench_runs:abc
… run release_run holger nordisk —
✗ fail docs_book nornir nordisk 310 ms
RESULT is the result_ref — the pointer to the domain Iceberg row the op produced (e.g. bench_runs:<uuid>), so you can jump from "the job ran" to "here's what it made". A … run row with no elapsed is still in flight.
Reading heavy-job queuing. Benchmarks are exclusive — submit several and only one shows … run; the rest show queued. That is the scheduler making them serialize so neither perturbs the other's numbers:
nornir jobs --kind bench_run --running # 1 running + the rest queued, draining one at a time
f3 — running a container as a tracked job
# Track a one-shot container; its exit code becomes nornir's exit code:
nornir container --image docker.io/library/alpine:3 -- echo hi
# docker instead of podman, with extra run flags and a custom job label:
nornir container --engine docker --image alpine:3 \
--flag '-e' --flag 'FOO=bar' --target my-check -- sh -c 'echo $FOO'
--imageis required;--enginedefaults topodman(--rmis always added).--flagis repeatable — each is inserted betweenrunand the image.--targetis the human label on the job row (defaults to the image).- everything after
--is the command run inside the container.
The captured stdout/stderr is printed and the container's exit code is propagated, so shell && and CI gating work:
nornir container --image alpine:3 -- sh -c 'exit 7' # nornir exits 7, job row = failed
Either way the run shows up in nornir jobs --kind container with its logs and exit code on the row (logs capped at 16 KiB/stream). A missing engine surfaces as a non-zero nornir exit and a failed job row.
Where else the same data shows up
The 🧬 nornir viz tab's job list reads the exact same records through the Viz.Jobs RPC — nornir jobs --json is its CLI twin. Nothing you run needs to opt into the ledger: release, docs, bench, scans, index/vector builds, coverage, and more already self-register, so nornir jobs is the one place to answer "what is (or was) running, and did it finish?".