# Notebooks

<!-- Generated by scripts/gen-docs-mcp.mjs from the shared tool catalog. Do not edit by hand —
     edit the catalog in packages/shared/src/mcp.ts and regenerate. -->

## Tools

A notebook is one markdown document: prose and `python` cells, which read workspace data through the `vistazo` library (`await vistazo.widget(...)`, `vistazo.dataset`, `vistazo.call`). `run_notebook` runs every cell on a cloud kernel as the person the agent acts for, behind the same review gate as a person’s Run, and bills compute per second of execution.

| Tool | What it does | |
|---|---|---|
| [`list_notebooks`](#list_notebooks) | List notebooks | Read-only |
| [`get_notebook`](#get_notebook) | Get notebook | Read-only |
| [`run_notebook`](#run_notebook) | Run notebook | Destructive write |
| [`save_notebook`](#save_notebook) | Save notebook | Destructive write |

## list_notebooks

`Read-only`

List the notebooks you may open in a workspace: analyses written as markdown with fenced Python cells that run over this workspace's data. Each row carries `id` (the handle — names are labels and may repeat), `name`, a one-line `description`, `updatedAt`/`updatedBy`, your `role` on it (owner | editor | viewer — only an editor may change it or run it here; a viewer may try its own parameter values on the page), `parameters` (the keys it defines), `sourceUpdatedBy` (an `agent:<uid>` value means an agent wrote the cells and a person has not vouched for them yet), `lastRun` — the last execution's status, when, who ran it and how many cells — and `refreshCadence` (the schedule, or null; change it with save_notebook's `cadence`). Reach for this before writing a new notebook: an existing analysis of the same question is usually worth extending. Descriptions and run summaries only; read the cells with get_notebook. Archived notebooks are hidden unless includeArchived:true.

## get_notebook

`Read-only`

One notebook in full: its markdown `source` (every cell verbatim — what save_notebook's `source` replaces and its `ops` address by index), `cells` (each index's kind), `parameters` and the last run's per-cell outputs, each with the `params` it ran with (`paramsStale` once a default moved). Read it BEFORE any edit: `updatedAt` is every write's `expectedUpdatedAt`, and `ops` indices come from this parse. Outputs are trimmed (marked `truncated`); figures and large tables read as `[image attachment <id>]`. A failed cell carries `lead`, the line the page shows; a run that failed outside any cell carries `error.message`. `lastRunDetail.stale: true`: the cells changed since that run, so indices may not match. A cell that run skipped shows its newest earlier output (`fromEarlierRun`). format:'ipynb' returns a Jupyter document that save_notebook's `ipynb` reads back. `versions` lists the revs manage_version `restore` (kind 'notebook') takes; `url`, `publicLink` and `embed` as on get_dashboard. Requires view access.

## run_notebook

`Destructive write` · `Reaches outside Vistazo`

Run a notebook on Vistazo's servers, as the person you act for: it restarts that person's kernel for the notebook and runs every Python and data cell top to bottom with the parameter defaults, so the outputs always match the code, then returns each cell's `status`, `ms` and output, trimmed the way get_notebook trims them. It stops at the first cell that raises, and the cells after it come back `skipped`. The outputs publish to everyone who can view the notebook, with the runner named, exactly as a person's run does, and get_notebook reads them afterwards. Compute is billed per second of execution against the person's compute allowance, the same price as a run from the app, and `usage_limit` means it is spent. `too_many_kernels` means the person already holds the most notebook kernels one person may; the message names each notebook holding one and when it frees itself. No tool stops a kernel, so tell the person which to close rather than retrying. The whole run must finish inside 9 minutes. Requires edit access on the notebook.

## save_notebook

`Destructive write`

Create or update a notebook — the ONE write path for its text, and the tool for a regression, t-test or clustering rather than a chart. Omit `notebookId` to create (`duplicateOf` copies a notebook); pass it to replace the whole `source`, or send `ops`, `ipynb` or `cadence` instead (rules on each field). A notebook is MARKDOWN with two kinds of fenced cell and nothing else; every other paragraph is prose:

```python
sales = await vistazo.widget('w_8f2…')
smf.ols('margin ~ units', data=sales).fit().summary2().tables[1]
```

CELL RULES (the runtime's). Data arrives only through `vistazo`, each call awaited and returning a pandas DataFrame: `vistazo.widget('<widgetId>')` (a widget's rows), `vistazo.dataset('<file name or id>')` (an uploaded table), `vistazo.call('<mcpId>', '<toolName>', {…})` (one read-only call on a connected source), `vistazo.tools()` (the connections and read-only tools); `vistazo.param('<key>')` (NOT awaited) reads a set_parameter value, as `{{ params.<key> }}` does in data cell args or prose. NO files and NO network: `pd.read_csv`, `open()` and `requests` fail. Packages are FIXED: pandas 3.x (not 2.x), numpy, scipy, statsmodels, scikit-learn, matplotlib; no pip or micropip. A cell shows its last line, as in Jupyter: a DataFrame or list of dicts (or `result`) is a table, an open matplotlib figure a PNG. ONE output per cell: a chart before a closing table is dropped; give each its own cell. Prints before a table or figure are kept as `stdout`. Top-level `await` works. A `vistazo-data` fenced block is a DATA cell: one call as JSON, edited as a form (shape: `ops`).

A `<!-- cell -->` line between two prose blocks makes them two markdown cells; without it they are one.

run_notebook runs the cells on a cloud kernel; never report an output you have not read back. Caps: 100 000 characters, 50 cells, 50 `vistazo` calls per kernel session every ten minutes. `expectedUpdatedAt` (from get_notebook) refuses a concurrent edit. Creating needs full workspace membership; updating, edit access on the notebook.

### `cadence`

The schedule: `enabled` plus an interval, every N minutes | hours | days | weeks (minimum 15 minutes). refresh runs once, NOW; this keeps data current. MATCH THE INTERVAL TO HOW OFTEN THE SOURCE PUBLISHES. Annual/quarterly sources: weeks (daily is 365 identical fetches a year). Daily sources (exchange rates, weather, filings): a day, or hours for same-day movement. Minutes only for a live feed. A DASHBOARD schedule is free: saving attributes it to YOU — runs execute on the dashboard owner's data-source connections, and a chart one repairs with AI (at most once a day per chart) draws your usage allowance — and re-saving un-pauses one paused for credential failures or failing sources. A SCHEDULE KEEPS THE FETCH CURRENT, NOT THE WINDOW: a widget argumentTemplate with a literal end bound (end_year 2020), or a notebook data cell / vistazo.call with a literal date, re-fetches the same window every run — check get_widget before promising it stays current. A NOTEBOOK schedule is BILLED: each run restarts the notebook and runs every cell as YOU, on your data access and compute allowance; it needs edit access, runs whatever the notebook holds at run time, and a failed run is reported to you and the owner. A seat holds at most 25 scheduled dashboards and 25 notebooks per workspace. enabled:false stops scheduling, keeping the configuration. An interval repeats from its first run (the next 15-minute sweep after enabling); the reply's `note` names that weekday and UTC time. Fixed weekdays/times (calendar schedules) are set in the web app's Schedule card; toggling enabled without an interval preserves one, and passing interval/unit replaces it. The reply reports the schedule AS IT NOW STANDS — `nextRunAt` (null when it will not fire; with a `nextRunNote` the run is due on the next sweep and nextRunAt is the latest it comes), `lastRunAt`, and `pausedReason` with how to resume. get_dashboard and get_notebook return the same block.

### `ops`

Edits the notebook CELL BY CELL instead of resending the whole source, applied IN ORDER: `{ op: 'set', index, cell }` replaces one cell, `{ op: 'insert', index, cell }` puts one before that index (`index` equal to the cell count appends), `{ op: 'delete', index }` removes one. A `cell` is `{ kind: 'markdown', text }`, `{ kind: 'python', code }` or `{ kind: 'data', code }`. A data cell's `code` is a JSON object naming one call, run as `<output> = await vistazo.call(...)` (or `.dataset`) so later Python cells read `output`: `{"source": "tool", "mcpId": "economy", "toolName": "series_observations", "args": {"series_id": "UNRATE"}, "output": "unrate"}` or `{"source": "dataset", "name": "orders.csv", "output": "orders"}` (`name` and/or `datasetId`). In `source` it is a `vistazo-data` fenced block. One that would not run is refused; use one where a person will edit the call. Indices are the ones get_notebook returned, and later ops see the list the earlier ones produced — so `expectedUpdatedAt` is REQUIRED with `ops` (not with `source`): an index against text that moved edits the wrong cell, which is the failure this guards. Get the notebook, then patch. While people have it open your ops merge into what they are typing: edits to other cells land, and so do edits to other text in a code or data cell someone is typing in; only an edit overlapping theirs (or a prose cell they changed) is refused, naming who is editing. After any refusal, get_notebook again and send ops against the fresh indices, never the same ones. The caps and cell rules are `source`'s.

### `ipynb`

A Jupyter `.ipynb` document to turn into the notebook's source, as the file's text or as the parsed object; omit `notebookId` to create, pass it to overwrite that notebook's source. Markdown cells stay markdown and code cells become `python` cells, with three deliberate changes the reply lists as `notes`: stored outputs are DROPPED (they carry no provenance here and a run re-produces them), magics and shell lines (`%matplotlib`, `!pip install`) are COMMENTED OUT (there is no shell and no installer), and every cell that reads a file or the network is FLAGGED in a leading note rather than rewritten — only a person knows which widget, dataset or connected tool holds those rows, and the note says to read them with `await vistazo.dataset(...)` or `await vistazo.call(...)` instead; every cell importing a package the runtime does not ship is flagged the same way. Cell numbers in the notes are the 0-based indices get_notebook and `ops` use. Up to 1.5 MB of `.ipynb` on the way in, then `source`'s caps (100 000 characters, 50 cells) on what the conversion produced. Read the imported result with get_notebook and fix the flagged cells before telling anyone it runs — then run_notebook.
