Headless Runner¶
strata run executes every cell in a notebook directory without starting the
server or opening the UI. Useful for CI, scheduled jobs, and sanity-checking
that a notebook still works after a dependency bump.
For rendering a notebook to a shareable markdown or HTML file (without
executing anything), see Export and its strata export
sibling command. For converting an existing Jupyter .ipynb into a
Strata notebook directory, see Import from Jupyter and
its strata import sibling.
It reuses the same NotebookSession and CellExecutor the UI uses, so the
execution path is identical, artifact cache hits, cascade ordering, worker
dispatch, and mount resolution all behave the same way.
Usage¶
<notebook_dir> must be a path to a directory containing notebook.toml
(plus cells/, pyproject.toml, uv.lock). It's the same on-disk layout the
UI works with; you can pass any notebook directory from STRATA_NOTEBOOK_STORAGE_DIR
or anywhere else on the filesystem. strata run is local-only - it does
not talk to a running strata-notebook or a service-mode deployment. Each
invocation opens its own NotebookSession, runs the cells, and exits.
Python, R, SQL, prompt and widget cells all run (a widget cell stores its
controls' current values); markdown cells are reported as ok with nothing to
run.
Options¶
| Flag | Description |
|---|---|
--force |
Ignore the artifact cache and re-execute every cell from scratch |
--no-sync |
Skip uv sync; use .venv/bin/python, which must already exist |
--timeout |
Per-cell timeout (seconds) for this run, overriding the 300s default; a per-cell # @timeout or notebook.toml timeout still wins. For compute-heavy cells (model training, feature extraction). |
--format |
human (default) or json - both write to stdout |
--quiet |
Suppress per-cell status lines (human format only) |
--format json writes a single JSON object to stdout when the run
finishes: {"notebook", "success", "duration_ms", "cells": [...]} with one
entry per cell (id, status ∈ ok|error|skipped, duration_ms,
cache_hit, plus stdout / stderr - truncated at 10k chars - and
error / reason where applicable, traceback when the cell raised, and
error_code when the failure has a stable name, such as fetch_pin_mismatch). For a cell that produces an output
artifact, a cache hit replays that artifact without re-emitting console
output, so stdout can be absent on warm runs (--force re-executes). A
leaf cell that only prints is different: its stdout/stderr are cached by
provenance and replayed on a warm hit. Errors and pre-flight diagnostics go to
stderr regardless of format. The executor's per-cell INFO logs are held back
during a run; set STRATA_LOG_LEVEL=INFO to see them. Pipe stdout to jq:
strata run ... --format json | jq '.cells[] | select(.status == "error")'.
A successful cell also carries provenance_hash and outputs, one
{name, artifact_id, version, content_sha256} per stored output, so two runs
on two machines can be diffed digest by digest with no server involved.
strata run stores the variables of leaf cells too (cells nothing downstream
reads, which the notebook otherwise keeps no artifact for), so the notebook's
final results have digests to compare; a leaf that is a cache hit from a run
outside strata run (the UI, cell run, MCP) lists only what that run stored,
so pass --force once before comparing; a value that cannot be serialized is
skipped. Display outputs appear as __display__N; a leaf's cached console is
not listed. An output stored before digests were recorded gets its
digest computed from its bytes when the report is written. The ZIP export's
provenance.json lists the same outputs per cell.
Exit Codes¶
| Code | Meaning |
|---|---|
0 |
All cells succeeded |
1 |
One or more cells failed |
2 |
Invocation error (bad path, env sync failure, malformed TOML) |
CI scripts can branch on the exit code; the structured output stays parseable
even when some cells failed (each failure produces a JSON object with
status: "error", its error, and the traceback when the cell raised).
Example: GitHub Actions¶
jobs:
notebooks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.1.0
- name: Install Strata
# uv tool install creates a uv-managed env at
# ~/.local/share/uv/tools/strata-notebook with the strata
# CLI on PATH - satisfies the runtime guard.
run: uv tool install strata-notebook
- name: Run notebook
run: strata run ./notebooks/daily_report --format json
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
--format json emits a single structured result object, so downstream steps
can parse per-cell status without screen-scraping.
What Gets Run¶
Every cell in the notebook executes in topological order, exactly as if you had clicked Run All in the UI. Cached artifacts are reused, the first run populates the cache; subsequent runs on unchanged source + inputs return instantly.
Passing --force skips each cell's cache lookup, so every cell re-executes
(nothing cached is deleted), which is what you usually want in CI if you're testing that the code still produces
the expected artifacts.
What Does Not Happen¶
- No server starts. No ports are bound; no UI is served.
- No WebSocket broadcasts. Progress is written to stdout only.
- No interactive prompts. A cascade that would pop a confirmation in the UI just runs, the CLI treats every cell as "confirmed."
Environment & Secrets¶
strata run reads the notebook's [env] and [secret_manager] blocks the
same way the server does. Secret-manager credentials (e.g.
INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET) must be present in the
shell that invokes the command, they are never stored in the notebook.
For notebooks that require env vars set only via the Runtime panel (never
committed), export them before invoking strata run.
strata validate¶
Static checks without executing anything - no environment sync, no subprocesses, no LLM calls:
notebook.tomlparses and the cell files load- the DAG builds without cycles
- per-cell annotation diagnostics - the same validation the server runs
on open / reload (
worker_unknown,loop_missing_carry,sql_connection_missing,fetch_unreadable,dataset_unreadable, malformed@output_schema, …)
Exit codes mirror strata run: 0 valid (warnings allowed), 1 invalid
(parse failure, DAG cycle, or any error-severity diagnostic), 2
invocation error. --format json emits
{"notebook", "valid", "errors", "cells", "summary"} where each cell
carries its defines / references (so you can check the DAG wiring you
intended) and its diagnostics with severity / code / message /
line.
The intended loop for scripts and coding agents: write files → validate →
fix → run. Validation is cheap enough to call after every edit; strata
run is the expensive step. See
Authoring Notebooks Programmatically.
strata new¶
Scaffold a notebook directory without the server:
strata new "My Analysis" [--parent DIR] [--python 3.12] [--no-env] [--no-git] [--project-mount [NAME]] [--format human|json]
Creates <parent>/my_analysis/ with notebook.toml, pyproject.toml
(pre-seeded with the notebook runtime packages), an empty cells/
directory and a .gitignore that keeps .strata/, .venv/ and
renv/library/ out of version control (skip it with --no-git; an existing
.gitignore is never replaced), then syncs the venv (skip with --no-env;
strata run syncs it later). Idempotent on an existing notebook directory:
the notebook is left as it is (its id, cells, configuration and
dependencies), and only missing scaffolding is added, so re-running it never
orphans artifacts or loses settings.
--project-mount [NAME] adds a notebook-level read-only mount of --parent
(the project directory) as a Path variable NAME (default project) in every
cell, so cells read project files as open(project / "file") without absolute
paths, even though a cell runs with its working directory set to the notebook
dir. The mount is pinned, so it never gets fingerprinted (no directory
hashing, and the project's own churn never re-stales cells). This is the
scratchpad-friendly default when the notebook lives in a subdirectory of a
project (see the notebook as an agent's scratchpad).
Inspecting a notebook (cell, dag, status)¶
For agents (and humans) that need to read a notebook's state without executing it, three read-only commands print structured JSON (default) or a compact human view. They open the notebook locally - no server, no env sync:
strata cell list <notebook_dir> # every cell: id, name, status, source
strata cell show <notebook_dir> <cell_id> # one cell: source, status, staleness (outputs and console with --format json)
strata cell show <notebook_dir> --var NAME # the cell that DEFINES a variable (else the available names)
strata dag <notebook_dir> # dependency edges + topological order
strata status <notebook_dir> # per-cell status + staleness summary
Every <cell_id> here and below is the id from notebook.toml or cell list,
not the cell's # @name.
Each takes --format human|json (JSON is the default - these are agent-first).
The JSON shapes match the server's REST API (GET /{id}/cells, GET /{id}/dag),
so a script written against the local CLI keeps working against a running
server later. Exit codes follow the same contract as run / validate: 0
success, 1 operation failure (e.g. unknown cell - a structured {"error": …}
on stdout), 2 invocation error (bad path) on stderr.
Looking at a plot¶
An image has no text form, so cell show reports a plotting cell's output as
image/png with its size and an artifact_uri, and a null preview. To see
it, write the bytes to a file:
strata cell output <notebook_dir> <cell_id> --out plot.png # the last display output
strata cell output <notebook_dir> <cell_id> --out first.png --index 0
--index counts display outputs in the order the cell emitted them; the
default -1 is the last, which is the value a cell ending in an expression
produced. It works against a live session too, with --server/--session;
the file is always written on the machine running the command.
Working against a live session (--server / --session)¶
Every command above also targets a session on a running strata-notebook -
the same session a human is watching in the TUI or web UI - instead of a local
directory. Pass --server <url> --session <id> in place of the notebook path:
strata cell list --server http://localhost:8765 --session $SID
strata cell show --server http://localhost:8765 --session $SID <cell_id>
strata dag --server http://localhost:8765 --session $SID
strata status --server http://localhost:8765 --session $SID
<id> is the session id (the route {id}, not the notebook.toml id).
strata agent prints it when it starts; without that, POST
/v1/notebooks/open with the notebook's path returns it as session_id, and
GET /v1/notebooks/sessions lists the open ones (see the
REST API). The output is identical to the local backend - both project the same wire shape - so
a script written against a local notebook works unchanged against a live session.
cell run / cell test and the authoring commands accept the same selector
(see below), so the entire surface works against a live session. cell cancel
takes only this selector (see Cancelling a run). Remote
operations target a personal-mode server (the use case is driving the session
you're watching locally).
The selector decides who sees the change. Given a notebook path, the
authoring commands edit the files directly and a running server learns nothing
until its next resync - so a colleague watching in the TUI or web UI sees
nothing happen. Given --server/--session, the same command goes through the
server, which broadcasts it, and the change appears in every viewer as it lands.
Both are correct; they are for different jobs. Use the path to prepare a
notebook, and the session to drive one somebody is watching.
Running a cell or its tests (cell run, cell test)¶
Beyond inspection, an agent can execute one cell at a time (not the whole notebook) and run a cell's unit tests, both with structured output:
strata cell run <notebook_dir> <cell_id> [--rerun | --force] [--no-sync]
strata cell test <notebook_dir> <cell_id> [--file tests.py] [--no-sync]
cell run materializes the cell (using the cache and re-running stale upstreams
by default; --rerun bypasses the target's cache, --force runs against
whatever upstream artifacts already exist). cell test runs the cell's
cells/{cell_id}.test.py via pytest and reports per-test outcomes; pass
--file tests.py (or - for stdin) to set the cell's test source first and
then run it - the one cell-test affordance that previously required editing the
file by hand. Both sync the venv first (like strata run) unless you pass
--no-sync.
Both also accept --server <url> --session <id> to run on a live session instead
of a local directory (strata cell run --server http://localhost:8765 --session
$SID <cell_id> --rerun). The server owns its venv, so the remote path never syncs
(--no-sync is a local-only flag), and --rerun / --force map to the
server's run modes, so remote execution has the same three modes as local.
--format json (default) writes a single clean JSON object to stdout - the
executor's logs go to stderr, so the stdout stream stays parseable:
strata cell run nb featurize --format json | jq '{status, cache_hit, error}'
strata cell test nb featurize --format json | jq '.cases[] | select(.outcome != "passed")'
Exit codes: cell run → 0 ran ok, 1 the cell errored (or unknown cell), 2
setup error (no .venv/bin/python under --no-sync, sync failure). cell test → 0 all
passed, 1 a test failed/errored, 2 pytest unavailable in the venv.
Cancelling a run (cell cancel)¶
A run on a live session can be stopped from another shell, whoever started it
(a cell run --server, an MCP agent, or the browser):
It does what the browser's stop button does: the run stops (a cell running in
a local process has its whole process group killed), the cell goes back to
idle, and the cell run that was waiting on it returns status: "error" with
error_code: "cancelled". The output is {"cell_id", "cancelled", "status"}.
Exit codes: 0 a run was cancelled, 1 the cell was not running
(cancelled: false; a finished cell keeps its state) or an operation error,
2 invocation error.
Only a server run can be cancelled this way. A cell run on a notebook
directory executes inside that command's own process, so cell cancel given a
path exits 2 and says so; stop that run with Ctrl-C instead.
Authoring cells and dependencies (cell add/edit/rm/mv, dep)¶
An agent can also build and edit a notebook through commands (instead of writing
notebook.toml + cells/*.py by hand):
strata cell add <notebook_dir> (--file body.py | -c 'src') [--run] [--after <id>] [--language python|markdown|sql|r|prompt|widget]
strata cell edit <notebook_dir> <cell_id> --file body.py # replace a cell's source
strata cell rm <notebook_dir> <cell_id> # delete a cell
strata cell mv <notebook_dir> <cell_id> --to <index> # reorder (0-based)
strata cell annotate <notebook_dir> <cell_id> --set worker=gpu-box --unset timeout
strata cell pin-fetch <notebook_dir> <cell_id> [NAME ...] # write sha256= into @fetch lines
strata dep add <notebook_dir> <package> # uv add
strata dep rm <notebook_dir> <package> # uv remove
--file - reads cell source from stdin; -c 'src' passes the source inline
(the two are mutually exclusive). cell add mints a backend-style 8-char id (the
same scheme the server uses) and prints the new cell; mv prints the new order.
Pass --run to execute the cell right after adding it (the one-call
scratchpad primitive), and the run outcome (status, cache_hit, stdout,
stderr) is folded into the output under run. The returned cell is the
post-run view, so its rendered outputs (a trailing bare expression's value)
ride along without a separate cell show (--no-sync skips the venv sync, as
with cell run). cell annotate splices # @key annotations into a
cell's source (--set KEY=VALUE / --unset KEY, both repeatable) while leaving
the body untouched - a convenience over rewriting the whole source with edit;
it targets scalar directives (name, worker, timeout, model, …), so edit
the source directly for repeatable mount / table / env. cell pin-fetch
pins a cell's @fetch inputs: it writes sha256= with
the digest of the bytes the notebook last downloaded into each named fetch line
(every fetch in the cell when no name is given) and prints the cell and a
pinned list of {name, url, sha256}. It reads the notebook's own fetch
cache, so it takes a local directory, not --server; a fetch with nothing
downloaded yet is an error until the cell has run. Dependency
commands run uv add / uv remove and report whether the lockfile changed. All
take --format json|human and use the shared exit codes (0 ok, 1 operation
failure such as an unknown cell or a failed uv resolve, 2 invocation error).
These also accept --server <url> --session <id> to author into a live session
instead of a local directory - edits land in the running notebook a human is
watching in the TUI. (cell add makes two calls: it mints the cell on the
server, then sets its source.)
Every cell records who wrote it. Pass --author NAME (your agent's name, say)
so a person can tell agent edits from their own; a server that authenticates
its callers ignores it and records the authenticated identity instead.
Together with inspect (cell list/show, dag, status) and execution (cell
run/test), this is the full agent surface - and all of it works either
offline against a directory or --server/--session against a running server. An
MCP server wrapping the same operations is also available; see
MCP Server, or strata agent for the one-command on-ramp
that stands it up and points a coding agent at a live session.
Workers (strata worker)¶
strata worker ls|add|rm|default <notebook_dir> ... edits the notebook's
[[workers]] block, and strata worker add-ssh|rm-ssh connects a box you reach
over SSH to a running server. See
Registering workers from the CLI and
Run cells on a machine you can SSH to.