The notebook format¶
A Zelos notebook is a single markdown file: optional YAML front matter, prose, and Python cells. Nothing else is required — you can write one by hand, and the CLI is just a runner for it.
Anatomy¶
---
description: Rank the pack's cells and find the one that sags.
requires-python: ">=3.10"
dependencies: ["zelos-sdk[notebook]"]
params:
unit: 1
window: "-2m"
---
Prose is ordinary markdown — write as much as you like between cells.
```python
from zelos_sdk import connect
agent = connect()
agent
```
Three parts:
- Front matter — the YAML block between the leading
---fences. Optional. - Prose — all the markdown around the cells. Rendered, never executed.
- Cells — every top-level ```python fenced block.
The notebook's name¶
A notebook's name is its file name. Nothing else stores one.
Analysis.md is the notebook "Analysis". Motor-Thermal-Sweep.md is "Motor Thermal Sweep". Zelos reads the file stem back as prose: separators become spaces, and nothing else changes. That name is what the library lists, what the editor shows, and what a rendered notebook uses as its heading.
Type a name in the editor and Zelos writes it as the file name, joining words with hyphens and keeping your capitals — so the name you typed is the name you read back. Rename the file and the notebook is renamed — through the app's Notebooks library or rename, so the agent carries the notebook's stored outputs with it.
Because the heading is derived, don't repeat the name as a # heading at the top of the body. Your prose starts with what the notebook has to say.
Front matter¶
All fields are optional. Omit the block entirely and the notebook still runs.
| Field | Type | Purpose |
|---|---|---|
description |
string | One line on what the notebook does. |
authors |
list of strings | Who wrote it. Also spelled author. |
dependencies |
list of PEP 508 strings | Packages the cells import, including zelos-sdk (e.g. zelos-sdk[notebook], pandas). |
tool.uv |
map | Native uv inline-script settings, including package indexes and dependency sources. |
params |
map of YAML values | Named parameters, overridable per run with --param. |
requires-python |
version specifier | Python to provision. Optional; the new starter includes ">=3.10". |
remote |
map | The cloud notebook this file publishes to. Zelos writes it — see The remote block. |
The first three describe the notebook and never change how it runs; the next four do. remote is the one field you don't write yourself.
Saving from the Zelos App
The visual editor writes a canonical form, not the file's original byte layout. It preserves prose wrapping and values, but emits front matter in a consistent key order and block style, quotes scalars when needed, and rewrites two-space hard breaks as visible \ breaks. YAML comments inside the front matter are dropped. A second save makes no further formatting changes.
---
description: Rank the pack's cells and find the one that sags.
authors: [Ada Lovelace]
requires-python: ">=3.10"
dependencies:
- zelos-sdk[notebook]
- pandas
params:
unit: 1
window: "-2m"
---
Quote any requirement that carries an extra when you write dependencies as a flow list — dependencies: ["zelos-sdk[notebook]", "pandas"] — because unquoted brackets inside […] open a nested YAML sequence. One requirement per line needs no quoting.
Describing a notebook¶
description and authors are how a notebook introduces itself. They travel with the file — into a rendered notebook, into an exported .ipynb's metadata, and into list, which heads each row with the name read off the file:
Find-the-weak-cell.md Find the weak cell
Rank a pack's cells and find the one that sags under load.
by Ada Lovelace
tests/Release-gate.md Release gate
Parameterized pass/fail runbook for CI.
With no path, list searches your notebooks folder; pass one to search anywhere else, . for the current directory. It searches subfolders too, and counts every markdown file it finds — a notebook with no cells yet is still a notebook. --json prints the same rows for scripting.
Write authors as a YAML list or as a single scalar (author: Ada Lovelace); either way entries are trimmed and blanks dropped. Authors are not case-folded, since two people can differ by case alone.
Tags are not front matter. They are organization vocabulary held in Zelos Cloud and shared with traces and layouts, so a label means the same thing across everything your team publishes — see Tagging a notebook.
None of these fields is rendered into the body: your prose owns the page. In an exported .ipynb, authors uses nbformat's own notebook-level key, so any Jupyter tool reads it — and import reads it back from any .ipynb, not just one Zelos wrote.
Your notebooks folder¶
Your notebooks live in ~/Documents/Zelos Notebooks. It's made for you, and nothing asks you where to put anything — a name goes there:
zelos notebook new analysis.md # ~/Documents/Zelos Notebooks/analysis.md
zelos notebook new tests/gate.md # subfolders work the same way
zelos notebook run analysis.md
zelos notebook list
Make subfolders for whatever you like — one per product, one for tests — and use tags for the questions that cut across them. Folders answer where does this belong; tags answer what is this about.
Nothing is confined there. Start a path with ./ or / and it means exactly that, so a notebook that belongs beside the code it analyzes lives in your repo and runs in CI the same way:
Set ZELOS_NOTEBOOKS_DIR in the agent's environment to keep the folder somewhere else — the agent reads it, so the app and the CLI both follow it.
new writes a notebook that already runs:
---
requires-python: '>=3.10'
dependencies:
- zelos-sdk[notebook]
---
```python
from zelos_sdk import connect
agent = connect()
agent
```
The agent resolves this starter environment once per machine, in the background at startup, and every notebook with the same dependencies shares it — see Environments.
Add zelos-sdk to dependencies
zelos-sdk is an ordinary dependency: list it like any other package and it resolves as part of the notebook's environment. Add the [notebook] extra (zelos-sdk[notebook]) to pull pandas and numpy with it — the analysis stack the frame's pandas hand-off and describe() need. Then from zelos_sdk import connect and go.
The remote block¶
remote is the one field Zelos writes into your file. The first publish mints a cloud identity for the notebook and saves it into the front matter:
So expect a remote: block to appear in a notebook you wrote by hand. That is the binding, not a stray edit. It is what makes this file — and every clone of it — publish to the same cloud notebook instead of minting a new one each time. Commit it alongside the notebook.
The organization id is opaque and immutable, so renaming an organization can never redirect a publish elsewhere. The notebook id is a UUID. To fork an already-bound notebook onto a fresh cloud notebook, publish with --new; that rewrites the block.
The binding is local metadata, and it never travels where it would be wrong. copy strips it, so a copy publishes on its own. An exported .ipynb never carries it.
Cells¶
Every top-level ```python fenced block is a cell. They run in document order, and they all share one Python process — a variable, import, or connection from an earlier cell is available in every cell below it. Connect once, near the top, and reuse the handle:
```python
from zelos_sdk import connect
agent = connect()
```
```python
# `agent` is still here — no need to reconnect.
frame = agent.query("bus0/BMS_message/status.cell_voltage", start="-2m")
frame
```
The last expression in a cell is its output — a SignalFrame, a pandas DataFrame, a chart, or a plain value — the same convention as a Python REPL or a Jupyter cell. Everything a cell prints streams as it runs.
Connecting
connect() with no argument targets the agent that is running the notebook, whichever port it listens on — the cell needs no address. Setting ZELOS_AGENT_URL on the agent points its notebooks somewhere else instead; a cell can also name an agent explicitly with connect("192.168.1.100:2300"). See the Agent SDK for the full connection and query API.
Display-only cells¶
Mark a fence {.norun} to show it as an example without executing it — useful for illustrating an alternative or a snippet you don't want run:
```python {.norun}
# Shown in the rendered notebook, but never executed.
agent.export("/tmp/run.trz", start="-1h")
```
Only top-level python fences run. A python block nested inside a list item or blockquote is treated as prose, not a cell.
Cell ids¶
Every cell owns a short, durable id written into its fence:
Outputs are keyed by that id, so they stay attached to the cell as you edit, reorder, and reopen the notebook. Display-only {.norun} cells get one too — they never run, so they own no outputs, but the id is how anything other than a cursor points at a cell, and Zelos AI editing your notebook needs to name the cell it is changing. A file you wrote by hand needs no ids: when it opens, Zelos assigns them to the in-memory document without touching the file. That exact document is written on the first save or immediately before the first run, so the ids shown in the editor are also the ids used for outputs. Imports assign ids as part of creating the notebook. Copying a cell gives the copy a fresh id, so it never shows the original's outputs. Zelos maps this identity to Jupyter's cell.id when importing or exporting .ipynb files.
Outputs stay out of the source file¶
Running a notebook does not write outputs back into the markdown, so the source diffs cleanly. The only execution-related value Zelos adds to it is a cell's {#id}, once — no outputs or execution counts.
After a cell settles, the agent stores its latest visible outputs and run provenance separately, keyed by that id. The app restores those outputs when you reopen the notebook, and exports and cloud publishes can include them without dirtying the source file. This output store is local state, not part of the markdown or its version history. Rename, move, or delete notebooks through the app's Notebooks library or the matching CLI commands so the agent can carry or remove that state with the file.
For a portable snapshot, run -o <file> writes a rendered .html or .ipynb notebook.
Images¶
Prose can reference images with ordinary markdown — . A relative path resolves beside the notebook file, so images travel with the folder and version alongside it; https: URLs work too. Pasting or dropping an image into the editor stores the file in a .assets/ folder next to the notebook and inserts the reference for you — hidden from the notebooks tree, machine-managed, and versioned alongside the file.
Reference-style markdown works the same way — ![caption][plot] with [plot]: assets/plot.png further down.
Drag an image's edge handle in the editor to size it; the width is stored in the markdown itself, as a trailing {width=420} marker. Referenced images ride along wherever the document goes: a cloud publish carries them and they count against your organization's cloud storage, a pull recreates them beside the pulled file, an HTML export inlines them so the page stays self-contained, and moving a notebook to another folder copies the images it references into the new one. Each image is at most 25 MiB, the same per-file limit a publish carries, so an image the editor accepts is one the notebook can take anywhere.
Deleting a notebook takes the stored images only it used; one another notebook in that folder still shows stays, and so does any image you placed beside the notebook yourself.
Notebooks display PNG, JPEG, GIF, WebP, SVG, AVIF and BMP. Anything else — a HEIC straight off an iPhone, say — is refused at the paste rather than stored as an image nothing can show. An .ipynb export is the one place images are carried by reference rather than inlined: it writes the relative path, which Jupyter resolves from the exported file's own folder.
Parameters¶
params in the front matter declares named inputs with default values:
Every name must be a valid Python identifier — params binds it as params.<name>, so it has to be one. A value is any YAML value; a map default is a map, so {default:, type:} is not special syntax for a typed schema — it's indistinguishable from a map param named window with keys default and type, and is reserved for that reason. There is no typed-schema form today.
Reference a declared parameter by attribute:
Override any declared parameter at run time — the same notebook, different inputs, no edit:
--param is repeatable. Each override coerces toward the type of the parameter it overrides:
| Default's type | --param value |
Result |
|---|---|---|
| number | 30 |
30 (int) |
| number | 1.5 |
1.5 (float) |
| number | unbounded |
"unbounded" (unconvertible text stays a string) |
| bool | true / false |
True / False |
| bool | anything else | kept as a string |
| list or map | [1, 2] / {a: 1} |
parsed as YAML |
| list or map | text that isn't valid YAML | kept as the string |
| string | anything | kept as a string, verbatim |
Defaults fix the type; they don't validate — text that doesn't convert is kept as a string rather than erroring. The params block gives defaults, not a schema: an override may introduce a key it never declared. Such an undeclared key has no type to coerce toward, so it's parsed as YAML on its own — 30 an integer, daily a string, [1, 2] a list — and the agent warns that the key isn't declared, in case it's a typo for one that is.
Environments¶
The agent builds each notebook's Python environment with uv from your requires-python, dependencies, and native tool.uv settings. Together those are the notebook's environment spec.
- First use of a new spec resolves the dependencies and provisions the environment. This can take a moment.
- Zelos resolves a spec once and keeps its uv lockfile under the agent's own state directory, never beside the notebook. Every notebook with the same spec shares that lock and the environment built from it, so a new notebook with a spec the machine already resolved opens without any network access.
- Later opens reuse the environment and start fast.
- Nothing is written into the notebooks folder except the notebook itself and its
.assets/images. The one exception is a notebook withtool.uvsettings — see below. - Environments nothing has used for 30 days are cleaned up automatically.
Across machines, publishing to Zelos Cloud ships the lockfile with the notebook. When another machine pulls that notebook and has never resolved its spec, it adopts the published lock and runs the same versions. A machine that already has its own lock for the spec keeps it, so all notebooks with one spec on a machine stay in one environment. To pin exact versions independent of any machine, pin them in dependencies — for example pandas==2.2.3.
Pre-provision without running any cells with env warm:
Offline
run and env warm accept --offline to provision from the local uv cache only — useful on machines without package-index access once the cache is warm (for example, after an earlier env warm run with network). On a cold cache, --offline fails rather than reaching the network.
Package indexes and dependency sources¶
Put native uv inline-script configuration under tool.uv. Zelos translates it directly to the generated PEP 723 script and lets uv validate it; there is no separate Zelos index format.
Relative paths in tool.uv keep uv's normal inline-script meaning: they resolve from the notebook's directory. This applies to local dependency sources, local indexes, find-links directories, and other path-valued native settings. Because uv resolves those paths from the script's own folder, a notebook with tool.uv settings gets a temporary generated script beside it while provisioning runs; Zelos removes it when provisioning finishes. Notebooks in different folders with the same tool.uv settings resolve separately.
This example installs torch only from the named PyTorch index while every other dependency continues to resolve from PyPI:
dependencies:
- torch
tool:
uv:
sources:
torch:
index: pytorch
index:
- name: pytorch
url: https://download.pytorch.org/whl/cpu
explicit: true
Indexes are considered in declaration order. uv's default first-index strategy is retained: once a package is found on an index, versions from lower-priority indexes are not mixed in. Marking an index explicit: true keeps it out of general resolution; tool.uv.sources selects which dependency uses it. See uv's package-index documentation for the complete native index schema.
For a private named index, keep credentials outside the notebook by setting UV_INDEX_<NAME>_USERNAME and UV_INDEX_<NAME>_PASSWORD in the agent environment. The name is uppercased and non-alphanumeric characters become underscores. Agent-wide UV_DEFAULT_INDEX and UV_INDEX settings are also inherited and follow uv's normal precedence rules.
Converting existing notebooks¶
Already have a Jupyter notebook? import converts it to this format — code cells become ``python fences, compatiblecell.idvalues are kept, and missing or incompatible ids are replaced with Zelos ids. Markdown becomes prose. Outputs are dropped, so review the result. A notebook Zelos exported comes back whole:metadata.authorsand themetadata.zelos` block become front matter again.
Where to next¶
-
Every command and flag for running and converting notebooks.
-
The SDK your cells call — frames, series math, latest values, charts.