Notebooks as Tests¶
A notebook run is a test run, and it fails the way a test does: a cell raises AssertionError. Nothing switches it on — write the rules, then assert.
Checks report instead of raising, so every rule in a cell runs and the whole board is visible before anything fails. That is the shape: evaluate everything, then gate once.
Write the rules¶
A check takes the series you already queried, so its window is the window you queried for:
frame = agent.query("bus0/BMS_message/cells.*", start=params.window)
agent.check.that(frame["bus0/BMS_message/cells.cell_0"], ">", params.floor)
Or it takes a signal handle and its own window, when you want a rule that does not depend on a query above it:
last="3m" means "every sample in the last three minutes" — a window implies always. With no window at all, a check reads the latest sample.
The full operator list and the tolerance operators are in Checks.
Read the evidence¶
A check result renders itself — as a board in the app and in an HTML export, as a row in the terminal. Every row carries the sample that proves it:
2026-09-04 17:06:58.855 cells.cell_0 (3.628 V) > 3 V always PASSED
2026-09-04 17:06:46.840 cells.cell_3 (2.99 V) > 3 V always FAILED
A failure names the first violating sample; a pass names the closest the data came to breaking the rule. Both are timestamped, so a red run points at the second to go look at rather than at a boolean. In code, that is result.evidence — a CheckEvidence with time_ns, signal and value.
Gate on them¶
One rule is one assert — a result is true when it passed:
For a scoreboard, collect the results and end the notebook with raise_if_failed(). It raises AssertionError naming every check that failed or errored:
from zelos_sdk import CheckResults
results = CheckResults(
agent.check.that(series, ">", params.floor, name=path)
for path, series in frame.short_names().items()
)
results.raise_if_failed()
agent.check.suite("checks/live.json") returns the same object for rules kept in a JSON file.
Read the outcome¶
The run's last line names the outcome, and the process exit code matches it. The rule is pytest's:
| Outcome | When | Exit |
|---|---|---|
succeeded |
Every cell ran | 0 |
failed |
A cell raised AssertionError |
1 |
errored |
A cell raised anything else | 1 |
interrupted |
Ctrl-C | 130 |
Exit 2 is an infrastructure failure — the agent connection, the session, or the stream — not a statement about the notebook.
A notebook reads its data from an agent, so the runner needs the CLI and a reachable agent — one running on the runner itself, or one on the bench that ZELOS_AGENT_URL points at:
name: bench gate
on: [push]
jobs:
gate:
runs-on: ubuntu-latest
env:
ZELOS_AGENT_URL: bench-01.internal:2300
steps:
- uses: actions/checkout@v4
- name: Install the Zelos CLI
run: curl -fsSL https://release.zeloscloud.io/cli/install.sh | bash
- name: Run the gate
run: zelos notebook run release-gate.md -o release-gate.html
- name: Publish the report
if: always()
uses: actions/upload-artifact@v4
with:
name: release-gate
path: release-gate.html
if: always() matters: the run you most want to keep is the one that failed. -o release-gate.html writes a self-contained page — every table, chart and check board — that opens without network access, so the artifact is the evidence.
Measuring instead of judging¶
count() returns how many samples matched and never fails a run. It is the right tool for a threshold you are still calibrating, and for reporting alongside a rule:
agent.check.count(frame["bus0/BMS_message/cells.cell_3"], "<", 3.5)
# 2026-09-04 17:27:23.400 cells.cell_3 < 3.5 V count · 93 PASSED
Waive a threshold on the command line¶
The override is visible in the run command, in the run's log, and in the rendered page — never by editing the rulebook.
Read a red run¶
In the Zelos App. Each check renders under the cell that produced it, and the run's outcome shows in the status bar.
In Zelos Cloud. Publish the run — zelos notebook run … --publish, or Share in the app — and the version keeps the source snapshot and the settled outputs, checks included. Failed runs publish too, because a failed test is exactly the run someone needs to see. See Publishing notebooks.
Where to next¶
-
The complete, runnable example this page describes.
-
Every operator, the temporal words, tolerances, and the pytest fixture.
-
Front matter,
params,{.norun}fences, and environments.