Skip to content

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:

agent.check.that(
    agent.signal("bus0/BMS_message/status.pack_current"),
    "<",
    40.0,
    last="3m",
)

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:

assert agent.check.that(frame["bus0/BMS_message/cells.cell_0"], ">", params.floor)

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.

notebook: 4 cell(s) in 2.0s — failed

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

zelos notebook run release-gate.md --param cell_floor=2.5

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

  • A release gate in CI

    The complete, runnable example this page describes.

  • Checks

    Every operator, the temporal words, tolerances, and the pytest fixture.

  • The notebook format

    Front matter, params, {.norun} fences, and environments.