Sagebrush · Articles · The notebook

How cells run

Written by Claude Opus 5.5, an AI model made by Anthropic, from the code and design notes of the Sagebrush project, led by William Stein (SageMath, Inc.).

This page states the rules of Sagebrush's reactive notebook as they are implemented. It is the notebook's specification: if the notebook does something these rules do not say, that is a bug.

The rules are marimo's where marimo has one, with the differences listed at the end. One guarantee follows from them:

What you see is what the cells compute. Every output is the result of the cells as they are now, or it is visibly marked stale.

1. What a cell defines and reads

The notebook reads each cell's code (without running it) and finds:

Names local to a function, a lambda, a class body or a comprehension are not the notebook's. A cell that defines a function which reads x reads x. As in Python, the functions, lambdas and comprehensions inside a class do not see the class's own names: x there is the notebook's.

Names starting with one underscore are private to their cell. _t, _i or _tmp in two cells are two different variables, and no other cell sees them. (__x and __ are not private.)

Sage cells are read after Sage's preparser (so R.<x> = QQ[] defines R and x), and a top-level var('x y') defines x and y. Magma cells are read after their translation to Python. A line obj? (help) reads obj. from m import * is an error: what it defines cannot be known.

2. The dependency graph

Cell B depends on cell A when:

  1. B reads a name that A defines (the definition that B sees: see section 3);
  2. A changes in place an object that B reads (A runs v.append(...), B reads v); the updaters of the same name depend on each other in page order, and each depends on the cell defining the name;
  3. B deletes a name that A reads (so A runs before the name is gone);
  4. A wrote a file that B read, the last time each ran (cells reading and writing files are connected automatically; a file connection that would make a cycle is left out).

Cells run in dependency order: a cell runs after every cell it depends on. When that does not decide, the cell higher on the page runs first. A definition may be below the cell that uses it.

A cycle (A depends on B and B on A, directly or through other cells) is an error on every cell in it.

3. A name defined in several cells: the nearest definition above

A name may be defined by several cells. Each cell then reads the nearest definition above it on the page, as when a paper says "let be ..." and later redefines , or as a Jupyter notebook does when it is run from the top:

R.<x> = QQ[]                  # cell 1: x is a polynomial variable
print((x^2 - 1).factor())     # cell 2: reads cell 1's x
x = var('x')                  # cell 3: a new x
print(diff(sin(x), x))        # cell 4: reads cell 3's x

If a name is defined by several cells and no cell reads it (a loop variable i, a scratch figure fig), nothing is renamed: each cell simply sets it. The same import in several cells is one definition.

Code that runs later than it is written reads the definition in force when it runs: the body of a function, of a stored lambda, or of a generator expression; an annotation (read when asked for, as in Python 3.14). So in

f = lambda: x
x = 2
print(f())

f reads the new x. A definition made inside a function (global x) takes effect where the cell calls that function. A renamed class or function is made under its written name and only then moved to the internal one, so its __name__, __qualname__, code names and what its decorators and metaclass see are all C; so are an f-string's {x=} label and a t-string's expression.

When renaming is not possible, marimo's rule applies: a name read by other cells may be defined by only one cell, and the cells involved show an error with a button that renames the later definitions in the code itself. That happens with Magma cells, and when the code does not say which definition a read gets:

One case the notebook cannot see: a base class's __init_subclass__ (or a descriptor's __set_name__) calling a method of the new class while it is being made, where the method reads the class by name. That method reads the new class, which is not bound yet.

4. What runs, and when

Running a cell submits its code. Until you run it, an edited cell keeps its previous code in the graph, and is marked with *. Shift+Enter, the ▶ button, Run all, Run stale and an agent's applyEdits all run cells.

When you run cells, the notebook runs, in dependency order:

  1. The cells you ran, and the stale cells they depend on (so that they compute from current values). These always run.
  2. Automatically, the cells that depend on them, and the cells whose meaning changed with this action: a cell that now sees another definition (section 3), a cell whose error went away, and the definer and updaters of an object whose updater changed or was deleted (section 5).

The time budget limits the automatic runs (the "Auto-run" control at the top: 2 seconds by default, 10 seconds, always, or never; kept in the notebook). In dependency order, each automatic cell runs if the time it took last time still fits in what is left of the budget (a cell never run counts as 0). A cell that does not fit is not run: it is marked stale, with how long it takes, and so is every cell that depends on it. The cells beside it go on. "Run stale" runs the stale cells, whatever the budget.

Manual cells (the cell menu: "Run only by hand") never run automatically, nor do the cells depending on them; they are marked stale.

Deleting a cell deletes the names it defined from the interpreter, and the cells that read them run again (and fail, unless another definition takes over by section 3).

5. What the interpreter keeps

There is one interpreter per notebook (the console has its own).

6. Errors

7. Outputs from elsewhere

Outputs that this interpreter did not compute are shown dimmed and marked stale: after Restart, after reloading the page or opening a saved notebook, and after changing the notebook's language (Python, Sage, Magma), which also reads every cell again in the new language. Running a cell first runs the stale cells it depends on.

TimeTravel's "Revert to this version" makes an old version the notebook (as a new version: nothing is lost) and computes it from scratch: a fresh interpreter, the files as they were then, and the same random numbers.

8. For AI agents

The page offers agents (WebMCP) and scripts (sagebrush.notebook):

9. Limits

What the notebook cannot see from the code:

Differences from marimo

marimo Sagebrush
A name defined in several cells an error each cell reads the nearest definition above
x += 1 defines x reads x, then defines it
A class's names, inside its methods, lambdas and comprehensions seen not seen, as in Python
The same import in several cells an error one definition
Changing an object from another cell (v.append) not tracked the cell is an updater: it runs with the definer, before the readers
Files mo.watch.file tracked automatically
Automatic reruns all (autorun) or none (lazy) a time budget per action (always and never are its two ends), and manual cells
Random numbers as Python gives them the same on every run of a cell
A cycle the cells on the cycles marimo records every cell on a cycle

The analysis of what a cell defines and reads is a port of marimo's, checked against marimo itself on thousands of cells; so is the dependency graph (see the design notes).

Updated 2026-10-12 · source on GitHub