Skip to content

notebook.toml Schema

notebook.toml is the committed configuration for a notebook. It declares the cells, their metadata, the per-notebook environment, mounts, workers, database connections, and any AI/secret-manager wiring. The file is human-editable and git-diffable; the matching backend writer round-trips it so external edits survive UI saves.

Runtime state - display outputs, per-cell provenance hashes, console snapshots, uv sync timestamps - lives in .strata/runtime.json, not here. notebook.toml only changes on structural edits (add/remove/reorder a cell, change a worker/timeout/env/mounts/AI settings), plus one narrow exception for who last edited a cell.

The schema is defined in src/strata/notebook/models.py::NotebookToml and the round-tripping rules in src/strata/notebook/writer.py.

Top-level keys

notebook_id = "01HZJV4Y9G..."       # required; UUID-like, backend-generated on create
name = "Iris classifier"            # human-readable display name
created_at = 2026-04-12T10:31:00Z
updated_at = 2026-05-18T18:04:22Z

worker = "fly-cpu"                  # notebook-level default; overridden by @worker annotations
timeout = 300                       # notebook-level default in seconds; overridden by @timeout
Key Type Description
notebook_id string (required) Stable opaque ID generated on create. Never edit by hand.
name string Display name. Default: "Untitled Notebook".
created_at datetime (UTC) Set on create; never updated.
updated_at datetime (UTC) Bumped on structural edits only. Runtime writers don't touch it.
worker string | absent Notebook-level default worker name. Overridden by cell-level worker (below) or # @worker annotations.
timeout float | absent Notebook-level default cell timeout in seconds. Same precedence as worker.

[env] - Notebook environment variables

[env]
LOG_LEVEL = "info"
DATA_BUCKET = "s3://my-bucket"
ANTHROPIC_API_KEY = ""              # blanked: keys matching KEY/SECRET/TOKEN/PASSWORD/CREDENTIAL

What gets blanked. Values for keys whose name contains KEY, SECRET, TOKEN, PASSWORD, or CREDENTIAL are written as empty strings to disk. The blanked entry is still committed so users can see which env vars a notebook expects without leaking the value into the repo. A value typed in the Runtime panel stays in the server's memory for the session, and no response or WebSocket frame carries it: clients get "__strata_masked__" in its place (see Update Notebook Default Env). In a new session the key comes back empty: fill it in the Runtime panel, or let a secret manager fill it. Until then a cell sees the server's own value for that variable, if it has one: an empty secret-looking value is a placeholder, not an override.

Whole-block elision. If every entry is either empty or a blanked sensitive key, the writer omits the [env] block entirely on save. Typing an API key into the Runtime panel doesn't churn the committed file.

[ai] - LLM configuration for prompt cells

[ai]
model = "claude-sonnet-4-6"
Key Type Description
model string Notebook-level default LLM model. Overridden by # @model <id> in prompt cells.
base_url string OpenAI-compatible API base URL, e.g. a local Ollama or vLLM server.
api_key string API key. Written to disk as is, not blanked, so it is committed with the file; prefer the Runtime panel.
max_output_tokens integer Max output tokens requested. Default 4096.
timeout_seconds float Request timeout. Default 60.0.

Every key is optional and overrides the same setting from the notebook's environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, MISTRAL_API_KEY, STRATA_AI_API_KEY) and from the server's STRATA_AI_*. See Provider configuration.

[secret_manager] - External secret-manager wiring

[secret_manager]
provider = "infisical"
project_id = "62a9b0f1c4..."
environment = "prod"
path = "/notebook-secrets"
base_url = "https://app.infisical.com"

Routing-only config for an external secret manager. Infisical is the only supported provider. The authentication token for the manager itself lives in the strata-notebook's process environment, not here. See Secret Manager.

Allowed keys: provider, project_id, environment, path, base_url. Unknown keys are dropped on save.

In every mode, base_url must be the server's INFISICAL_HOST (or the public default when unset); any other host is refused, since the server would log in there with its own credentials.

[[mounts]] - Filesystem mounts

[[mounts]]
name = "taxi_zones"
uri = "s3://nyc-tlc/misc"
mode = "ro"

[mounts.options]
anon = true
endpoint_url = "https://s3.us-east-1.amazonaws.com"

Each mount becomes a pathlib.Path variable in the cell namespace. Cells access remote data via standard Path operations; the executor materializes the prefix on first read.

Key Type Description
name string (required) Identifier - injected as a Path variable in the cell. Must match [a-zA-Z_][a-zA-Z0-9_]*.
uri string (required) file:///path, s3://bucket/prefix, gs://bucket/prefix, az://container/prefix.
mode "ro" | "rw" Default "ro".
pin string | absent Pinned version/etag - disables auto-fingerprinting.
options table Backend storage options passed through to fsspec. Common keys: anon, endpoint_url, profile.
credential string | absent Name of a server-defined credential (Named credentials) whose fields become fsspec storage options, so no secret lives in this file. options still win on a shared key.

Cell-level mounts (under [[cells.mounts]]) supplement notebook-level ones.

Named credentials

A mount or a connection can name a credential instead of carrying one:

[[mounts]]
name = "raw"
uri = "s3://lab-bucket/raw"
credential = "lab-bucket"

[connections.warehouse]
driver = "postgresql"
host = "db.internal"
credential = "warehouse-ro"

The server defines what each name means, with STRATA_NOTEBOOK_CREDENTIALS:

STRATA_NOTEBOOK_CREDENTIALS='{
  "lab-bucket":   {"key": "${LAB_AWS_KEY}", "secret": "${LAB_AWS_SECRET}"},
  "warehouse-ro": {"user": "analyst", "password": "${WH_PASSWORD}"}
}'
  • A mount's fields become fsspec storage options; a connection's become driver auth.
  • ${VAR} resolves against the notebook's environment first, which is where a configured secret manager puts what it fetches, and then the server's.
  • The name is part of a cell's provenance, because a different credential can see different data. The values never are, so rotating a secret invalidates nothing.
  • A name the server does not define, or a reference that is not set, fails the cell with a message naming the credential.
  • STRATA_NOTEBOOK_MOUNT_CREDENTIALS='{"s3": "org-bucket"}' applies a credential to every mount of a scheme that names none, so the organization's primary store works without editing notebooks.
  • A remote worker resolves names against its own STRATA_NOTEBOOK_CREDENTIALS; only the name travels in the dispatch.

The @mount annotation takes the name too: # @mount raw s3://lab-bucket/raw ro credential=lab-bucket.

A named catalog in the server's STRATA_CATALOGS takes the same key, so the catalog's secrets (a SQL catalog's database URI, a REST token, s3.* keys) stay out of the catalog config:

STRATA_CATALOGS='{"lake": {"type": "sql", "warehouse": "s3://lake/wh", "credential": "lake-db"}}'
STRATA_NOTEBOOK_CREDENTIALS='{"lake-db": {"uri": "${LAKE_DSN}", "s3.access-key-id": "${LAKE_KEY}"}}'

Its fields become pyiceberg catalog properties, beneath the catalog's own keys. Wherever a notebook reads the catalog (an @table snapshot, a DuckDB cell's attach and the snapshots of the tables it reads, staleness checks), the references resolve against the notebook's environment first, as a mount's do. A scan through POST /v1/materialize, including client.materialize from a cell, has no notebook, so it resolves them against the server's environment only. A missing name fails the read with a message naming it. The catalog's credential is not part of provenance: a table read is already keyed by its snapshot.

[[workers]] - Remote worker registry

[[workers]]
name = "fly-cpu"
backend = "executor"
runtime_id = "fly-cpu-v1"

[workers.config]
url = "https://my-strata-worker.fly.dev/v1/execute"
transport = "direct"
token_env = "STRATA_FLY_WORKER_TOKEN"
Key Type Description
name string (required) Worker name referenced in # @worker <name> annotations. Must match [a-zA-Z0-9][a-zA-Z0-9._-]*.
backend "local" | "executor" Default "local". "executor" for HTTP workers; "local" for in-process.
runtime_id string | absent Stable fingerprint hashed into cell provenance. Bump to invalidate the cache for cells using this worker.
config table Backend-specific. For "executor": url, transport ("direct" push, the default, or "signed" pull-model), token (literal - dev only), token_env (env var name - preferred), and for "signed" an optional strata_url, the server address the worker uses for the manifest's URLs (default: the server's own URL). See Distributed Workers.

[connections.<name>] - Named database connections

[connections.warehouse]
driver = "postgresql"
host = "warehouse.internal"
database = "analytics"

[connections.warehouse.auth]
user = "${WAREHOUSE_USER}"
password = "${WAREHOUSE_PASSWORD}"

[connections.warehouse.options]
application_name = "strata"
connect_timeout = 5

SQL cells reference these by name via # @sql connection=<name>.

Key Type Description
<name> section header Connection name - referenced by # @sql connection=<name>. Must match [a-zA-Z_][a-zA-Z0-9_]*.
driver string (required) One of the shipped adapters: duckdb, sqlite, postgresql, snowflake, bigquery. MotherDuck and MySQL are planned but not yet implemented.
auth table ${VAR} indirections only, resolved at execute time and never hashed into provenance. A personal server reads them from its own process environment. A server in service mode reads them from the notebook's env ([env] and its secret manager) and never from its own, so a variable the notebook does not set fails the connection; give a connection a server-held secret with credential.
credential string | absent Name of a server-defined credential (Named credentials) whose fields become driver auth, underneath this block's own auth.
options table Driver-specific runtime tunables that don't change which objects the connection sees (application_name, connect_timeout, etc.).
(driver-specific top-level keys) varies uri, host, account, database, role, path, ... - interpreted by the driver adapter.
catalog (DuckDB) string | absent A REST or Glue catalog to attach under its name, from the server's catalogs or, in personal mode, this file's [catalogs.<name>]; the query reads each of its tables at the snapshot the cell's provenance folds. See DuckDB over the lake.
mounts (DuckDB) list of strings | absent Mounts declared for the cell (file, s3, gs or az) to expose as views by name over their Parquet, CSV or JSON files.

Malformed connection preservation. If a [connections.<name>] block fails validation (bad name, missing driver, etc.), its body is written back verbatim under the same [connections.<name>] table on save, so a typo doesn't get silently erased by an unrelated edit. The annotation-validation layer surfaces a user-visible diagnostic.

[catalogs.<name>] - Notebook-level catalogs

On a personal server, a notebook can define the Iceberg catalogs its DuckDB connections attach, instead of the server's STRATA_CATALOGS:

[catalogs.lake]
type = "glue"
"glue.id" = "123456789012"
"glue.region" = "eu-west-1"
credential = "lake-ro"

[connections.lake]
driver = "duckdb"
path = ":memory:"
catalog = "lake"

Each entry is a set of PyIceberg catalog properties, the same shape as an entry in STRATA_CATALOGS, and credential names a server-defined credential so no secret is written here. A notebook entry wins over a server catalog of the same name, as a notebook's workers do.

  • Only a DuckDB connection's catalog reads these. @table and the scans a Python cell runs resolve catalog names on the server, so they see the server's catalogs only.
  • A server in service mode ignores them, and a cell whose connection names one fails saying so: on a shared server the operator configures the catalogs.

[[variant_group]] - Active-variant pointers

[[variant_group]]
group = "model_choice"
active = "logistic"
mode = "sweep"                # optional; absent means "switch"

Cells declare group membership via # @variant <group> <name> in their source. This block records which member is currently active - only the active cell participates in the DAG.

Key Type Description
group string (required) Variant group identifier. Must match the group name used in # @variant.
active string (required) Currently active variant name within the group. Empty means the first variant in source order. Ignored in sweep mode.
mode "switch" | "sweep" | absent switch (the default, written by leaving the key out) runs only the active variant; sweep runs every variant and hands downstream cells a {variant: value} dict. Any other value is treated as switch and flagged by annotation validation.

[[cells]] - Cell registry

[[cells]]
id = "a1b2c3d4"
file = "a1b2c3d4.py"
language = "python"
order = 100

[[cells]]
id = "e5f6g7h8"
file = "e5f6g7h8.py"
language = "python"
order = 200
worker = "fly-cpu"            # cell-level override of notebook default
timeout = 600                 # cell-level override

  [cells.env]                 # cell-level env additions (same blanking rules)
  CUDA_VISIBLE_DEVICES = "0"

  [[cells.mounts]]            # cell-level mount additions
  name = "model_weights"
  uri = "s3://my-models/bge-large"
  mode = "ro"
Key Type Description
id string (required) Stable cell identifier. Backend generates an 8-character UUID prefix when cells are created via UI / REST; hand-edits can use any unique string (e.g. seed, top-orders). This is what @after and @loop start_from= resolve against - not @name. See Cell IDs.
file string (required) Path to the cell source under cells/. The backend names it <id>.py, or <id>.md, <id>.r and <id>.widget for markdown, R and widget cells.
language "python" | "prompt" | "sql" | "markdown" | "r" | "widget" Default "python".
order float Display order. Float so cells can be inserted between existing ones without renumbering. Default 0.
worker string | absent Cell-level worker override. Beaten by # @worker in the cell source.
timeout float | absent Cell-level timeout override. Beaten by # @timeout.
env table Cell-level env additions / overrides. Same sensitive-key blanking as the notebook-level [env].
mounts array of MountSpec Cell-level mounts. Supplement notebook-level mounts.
created_by string | absent Who added the cell. Written by the server, not meant to be hand-edited.
updated_by string | absent Who last changed it.

created_by and updated_by record who wrote a cell, so a reader can tell cells an agent wrote from cells a person did. The value is the authenticated principal where the server has one. Otherwise it is what the client declared: local for the browser, or an external agent's own name sent through MCP or --author. In personal mode nothing checks that declaration, so treat it as a claim rather than an attestation. Both keys are absent on cells written before authorship was recorded.

They are the one exception to the structural-edits-only rule above. An edit to a cell's source rewrites notebook.toml when it changes who last edited it, and at no other time. So one person editing their own cell never touches the file, and the first edit by someone else touches it once. That rewrite leaves updated_at alone, because who edited a cell is not a structural change.

Annotation precedence

When the same piece of metadata is declared in multiple places, the most specific wins:

  1. # @worker X / # @timeout T / # @env K=V annotations in the cell source (highest)
  2. Cell-level worker / timeout / env in [[cells]]
  3. Notebook-level worker / timeout / env in the top-level table

Annotations are the canonical per-cell configuration surface; there's no UI editor for per-cell overrides. See Cell Annotations.

What lives elsewhere

Belongs in .strata/ (runtime state, gitignored) Belongs in notebook.toml
Cached display outputs (DataFrames, plots, scalars) Cell IDs, source filenames, order
Per-cell provenance hashes Cell-level worker/timeout/env overrides
Per-cell console (stdout/stderr) snapshots Mounts, workers, connections, variant pointers
uv sync timestamps AI default model, secret-manager routing
Artifact-store SQLite + blobs Notebook name, created/updated timestamps

Runtime writers never touch notebook.toml; structural-edit writers never touch .strata/. The invariant is enforced at the writer layer.

What to commit

strata new writes a .gitignore covering the runtime side, so git add -A in a notebook directory stages the right things. It never replaces an existing one; strata new --no-git skips it.

The committed set is:

  • notebook.toml
  • pyproject.toml and uv.lock (plus renv.lock for R notebooks)
  • everything under cells/, including cell tests
  • the .gitignore itself

The .mcp.json and CLAUDE.md that strata agent writes are ignored: each launch rewrites them with the server's port and the session id, so they describe one machine's run rather than the notebook.

The rules live in strata.notebook.layout: committed_paths() returns the set for a given directory and IGNORED_PATTERNS is what the .gitignore contains, so this list is a rendering of the code rather than a second source that drifts from it. The ZIP and snapshot exports carry exactly committed_paths(), and strata import writes those files back.

Round-trip safety

The writer preserves unknown top-level keys verbatim. If you hand-edit the file with a key the parser doesn't know about, it survives saves - useful for experimental settings or external tooling. A malformed [connections.<name>] block (no driver, etc.) is likewise written back verbatim under its own [connections.<name>] table so you don't lose the data while you debug.