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:
- The names it defines: names bound at the top level of the cell, by an
assignment (
a = 5,a, b = ...,a += 1,a: int = 5),def,class,import(import numpy as npdefinesnp), afororwithtarget,except ... as e,:=, orglobal ain a function that assignsa. - The names it reads: names it uses that it has not defined above that
use in the same cell. Builtins such as
printare read too, but no cell defines them, so they connect nothing.x += 1readsxbefore defining it, and so does what is evaluated before a definition takes effect: a function's default values, a class's body, a comprehension's first iterable. - The names it deletes (
del xof a name another cell defines). A top-levelexcept E as xcounts too: Python deletesxat the end of the handler. - The objects it changes in place:
v[0] = 1,z.x = 2,del v[0], augmented assignment to those,v.append(...)and other in-place methods (extend,sort,update,add, ...),random.shuffle(v)andheapq.heappush(v, ...), at the top level of the cell, wherevis a name another cell defines. Such a cell is an updater ofv.
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:
- B reads a name that A defines (the definition that B sees: see section 3);
- A changes in place an object that B reads (A runs
v.append(...), B readsv); the updaters of the same name depend on each other in page order, and each depends on the cell defining the name; - B deletes a name that A reads (so A runs before the name is gone);
- 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
- Each definition is its own variable. Editing cell 1 reruns cell 2, not cell 4.
- Within a cell, a use before the cell's own definition reads the
definition above (
x = x + 1reads the previousx). - A cell above every definition of a name reads the first one below it.
- Moving a cell, inserting one, or deleting one can change which definition a cell sees. The cells that now see a different definition run again.
- The page shows your code as you wrote it. Inside the interpreter, the
later definitions have other names (
xʹ2,xʹ3, ...: the character is U+02B9, a modifier letter). Error messages and the graph showx.
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:
- a class body where
xis the class's own or the notebook's depending on how the body runs (afterif c: x = 1, or a loop orwithblock that assigns or deletesx); - a function or lambda the cell uses both before and after a new definition;
- a definition in code that may never run, or run more than once (a
generator that is never consumed; a comprehension over something that
may be empty; a function the cell never calls, or calls under an
if, in a loop, or twice when it reads the name first); - a lambda or generator given to a call (
map(lambda: x, ...),sum(x for ...)): the call may run it at once or keep it for later; - a function with a decorator, when its body or a default reads the name (the decorator may call it, or the default);
- a function called while its class is being made (by the class body, a class decorator or a metaclass);
- an annotation that may be asked for before the new definition, and
annotations under
from __future__ import annotations(they are source text).
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:
- The cells you ran, and the stale cells they depend on (so that they compute from current values). These always run.
- 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).
- Before a cell runs, the names it defined last time are deleted, and
so are its private
_names. A definition you removed from a cell is gone once the cell runs. (A name that another cell also defines, and no cell reads, is kept: it holds whichever cell set it last.) - An updater runs with its family. Running a cell that changes
vin place reruns the cell definingvfirst, then every updater ofvin page order, then the cells readingv. A change is never applied twice. Deleting an updater, or editing it so that it no longer changesv, reruns the family the same way. - So does a deleted name. Once a cell has run
del v,vis gone: running a cell that reads or deletesvreruns the cell definingvfirst (and the deleting cell after the readers). Deleting thedel, or editing it away, reruns the definer, sovis back. A cell that gets an error (a cycle, say) is out of the computation the same way: what it changed in place or deleted is undone. - Random numbers are the same on every run of a cell. Each cell's
random seed is derived from the notebook's seed (kept in the notebook)
and the cell's id: Python's
random,numpy.random(includingdefault_rng()without a seed) and Sage's random functions give the same values however often, and in whatever order, the cell runs (so do generators made or reseeded without a seed:random.Random(),np.random.RandomState(),random.seed()).os.urandomis truly random: a key made from it in a notebook cannot be reproduced from the notebook. - Files: a cell that writes a file runs before the cells that read it (section 2), and they run again when it does. A file uploaded in the Files panel reruns the cells that read it.
- Widgets are inputs. Moving a widget (ipywidgets, or marimo's
mo.uicontrols) reruns the cells that read the variable holding it, within the budget, but not the cell that shows it. An@interactcontrol reruns its own function.
6. Errors
- An exception in a cell stops the cells that depend on it in that run: they show "Not run: cell N, which this cell depends on, raised an error", and keep waiting for cell N until it runs without one.
- Stop interrupts the running cell and ends the run: the cells after it are stale. The cells that read what the interrupted cell defines wait for it to run again. (The interpreter keeps the variables.)
- A cycle,
import *, and (when renaming is impossible) a name defined by several cells and read are errors in the graph: those cells do not run until the error is fixed. The error is shown on the cells you just ran, and on cells that were working only if they depend on those: adding a cell never breaks a cell that worked before. - A syntax error is shown by the interpreter, in the cell, when you run it.
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):
applyEdits({set, insert, remove})(tooledit_notebook): several changes, then one run, with a report of what ran, what is stale, what failed, what waited, and the errors;graph()(notebook_graph): each cell's names, the cells it depends on and that depend on it, its state, and its last run time;verify()(verify_notebook): recompute everything in a fresh interpreter and list the cells whose output differs from what was shown. A difference means an output depended on something these rules do not see (see the limits below).
9. Limits
What the notebook cannot see from the code:
- Changes through another name (
w = v; w.append(1)changesv), changes inside functions called later, and deeper changes (an object inside a list) are not tracked.verify()finds what they leave behind. - Names used through strings (
globals()["x"],exec,eval) and definitions that depend on a condition (if c: x = 1) are read as written: a cell that definesxonly sometimes still counts as defining it. - The outside world: the clock, the network, and
input()are not inputs of the graph.
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).