# J R Beckett Speedreader Marimo

> Required: Fetch Official Recipes

- Skill: `tomevault-io/j-r-beckett-speedreader-marimo` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/j-r-beckett-speedreader-marimo`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/j-r-beckett-speedreader-marimo/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/j-r-beckett-speedreader-marimo

---


# Required: Fetch Official Recipes

**Immediately after reading this skill, fetch the official marimo recipes documentation:**

```
https://raw.githubusercontent.com/marimo-team/marimo/main/docs/recipes.md
```

This document contains essential patterns for UI elements, caching, control flow, and buttons that complement the project-specific guidance below. Do not skip this step.

# Fundamentals

- Marimo notebooks are defined in pure python as a series of `@app.cell` decorated functions
- All cells in marimo share a global namespace
- The first cell should handle all imports, and should always include `import marimo as mo`

## Cells

```python
@app.cell
def _():
    <cell body>
    display_me
    return
```

- Cells are referenced by position: `cell_1` is the first `@app.cell`, `cell_2` is the next, etc. The setup cell is not numbered.
- Cells receive inputs and emit outputs by reading from and writing to the global namespace
- Cells should be idempotent
- Always end cells with a bare `return` statement. The marimo autoformatter will automatically add the appropriate return tuple (e.g., `return (my_var,)`) based on which variables the cell exports. Claude should never manually write return tuples.
- The result of the last expression evaluated in the cell is displayed in the UI. To display multiple values, make the last expression be a call to `mo.vstack` or `mo.hstack`
- Visualization objects and dataframes are displayed automatically as images and tables

## The Reactive Model

- Cells in marimo form a directed acyclic graph (DAG), defined by creating an edge from cell A to cell B when B uses a global variable defined by A
- When the cell that declares a variable runs (or changes and is then ran automatically), all cells that use the variable automatically re-run
- Marimo does **not** track mutation. Never mutate a variable in the global namespace
- Cycles are illegal

## Global Namespace Management

Always encapsulate the bodies of cells in a `def _()` function to prevent cell-local variables from leaking into the global namespace. Every cell that has cell-local variables should use this pattern, without exception. The only variables that should ever enter the global state are variables that we expressly want to be there. Here's a full example of how to use this pattern:

```python
@app.cell
def _(df, pd):
    def _():
        grouped = df.groupby("category")  # does not enter the global namespace, cell-local
        means = grouped["value"].mean()
        stds = grouped["value"].std()
        return pd.DataFrame({"mean": means, "std": stds})

    summary = _()  # call cell-local function
    summary  # display the dataframe
    return (summary,)  # enter dataframe into global namespace; this return is auto-generated by marimo
```

## Autoformat and Returns

When the user has a marimo notebook open with `--watch`, marimo will automatically format the notebook on save. If Claude is having issues with a third party editing the notebook, this is likely why. It is expected. Work with the formatter, not against it.

### How Returns Work

**Returns export variables to the global namespace.** A variable is only accessible to other cells if it appears in the return tuple. This is why Claude should always write bare `return` and let the fixer handle it.

### What `marimo check --fix` Does

The fixer performs **cross-cell dependency analysis** to automatically manage returns and parameters:

1. **Adds returns for variables other cells need** - If cell B references `foo` and cell A defines `foo`, the fixer adds `return (foo,)` to cell A.

2. **Adds function parameters for variables cells read** - If cell B uses `foo` from the global namespace, the fixer adds `foo` to cell B's function signature: `def _(foo):`.

3. **Ignores underscore-prefixed variables** - Variables starting with `_` are never returned, even if referenced elsewhere. This is why the `def _()` pattern works for hiding locals.

4. **Removes unnecessary returns** - If you manually write `return (foo,)` but no other cell uses `foo`, the fixer strips it to bare `return`.

5. **Fixes malformed returns** - Invalid returns (wrong variables, non-tuples like `return x`) are replaced with bare `return`.

### Why Claude Should Trust Bare Returns

The fixer is smart. Claude should:
- **Always write bare `return`** - The fixer determines what needs to be exported
- **Never manually write return tuples** - The fixer handles this based on actual cross-cell usage
- **Leave existing return tuples alone** - They were added by the fixer for a reason

The fixer runs automatically via `--watch` mode or manually via `uvx marimo check --fix notebook.py`.

## Common Pitfalls

- **Can't access UI value in same cell where it's defined** - Define UI elements in one cell, access `.value` in a later cell
- **Matplotlib**: Use `plt.gca()` as the last expression to display a plot, not `plt.show()`. Call `plt.tight_layout()` to prevent clipping
- **Circular dependencies**: If two cells reference each other's variables, marimo will error. Reorganize to break the cycle
- **Closure capture in loops**: When creating `on_change` handlers in a loop, bind the loop variable explicitly: `lambda v, i=i: handle(i)` not `lambda v: handle(i)`

## Creating a Notebook

All notebooks should start with the following:

```python
#!/usr/bin/env -S uvx marimo edit --sandbox --no-token --no-skew-protection  --watch --port 3005
# /// script
# requires-python = ">=3.11"
# dependencies = ["marimo", "pandas", "seaborn"]
#
# [tool.marimo.runtime]
# auto_reload = "lazy"
# auto_instantiate = false
# ///

import marimo

__generated_with = "0.18.4"
app = marimo.App()

with app.setup:
    import marimo as mo
    import pandas as pd
    import seaborn as sns
    import matplotlib.pyplot as plt
    # from helpers import my_helper

    <first cell body goes here>
    sns.set_theme()


@app.cell
def _():
    <second cell body goes here>
    return

if __name__ == "__main__":
    app.run()
```

- Shebang so the user doesn't have to memorize an incantation to launch the notebook. Pick a random port for the kernel to run on
- The shebang includes `--watch`, which makes the notebook automatically pick up changes from the notebook file and dependencies. Cells still have to be re-run manually, but you do *NOT* have to restart the kernel
- Pandas for data manipulation, seaborn for visualization. This is our standard stack
- `with app.setup` creates the setup cell. This will be the first cell in the notebook, and is treated specially by marimo
- `auto_reload = "lazy"` configures marimo to mark cells that depend on functions defined outside the notebook as stale when those functions change
- `auto_instantiate = false` prevents marimo from automatically running the setup cell on notebook startup
- After creating the notebook, run `chmod +x notebook.py` and notify the user that they can now open it with `./notebook.py`

## `uvx` Tricks

```bash
uvx marimo check --fix notebook.py  # Lint and autofix
uvx marimo development openapi  # View the OpenAPI spec for the marimo kernel API
uvx marimo export html notebook.py --sandbox -o notebook.html  # Run notebook and export to HTML. Not machine readable unfortunately
```

# Working Agentically

# Running Notebooks

When collaborating with a user, let the user run the notebook themselves. But when Claude is working agentically, Claude can run the notebook. Run it as a background bash process. Start it with the shebang, simply `./notebook.py`. This will open the notebook in the browser-based marimo editor, but it will **not** automatically run the notebook cells.

Because we're not running cells when we open the notebook, opening the notebook is nearly instantaneous. It is natural to want to `sleep` after opening the notebook and before running the first `run_cell` command. NEVER, under ANY circumstances, `sleep` for longer than 2 seconds.

After the notebook is open, Claude should run the first cell in the reactive sequence. This is *not* the setup cell, which is handled specially by marimo. This cell should be the first cell that the **user** runs, which is `cell_1`. The setup cell runs automatically as a dependency.

*IMPORTANT:* **All** notebooks are ran with the `--watch` option (it's in the shebang). This means that marimo will automatically pick up changes to the notebook, including any changes to the notebook's dependencies. *IMPORTANT:* After changing a cell or notebook dependency, Claude should *NOT* re-run the notebook. Claude should re-run cells instead. Trust `--watch`!

# Running Cells

Use `.claude/skills/marimo/scripts/run_cell.py` to run a cell from the command line while a notebook is open in the browser:

```bash
uv run .claude/skills/marimo/scripts/run_cell.py <cell_id> <port> [output_dir]
```

This connects to the running marimo server, triggers the specified cell, and waits for it (and all dependent cells) to complete.

Cells are identified by `cell_n` where n is the 1-based position among `@app.cell` functions. The setup cell (`with app.setup`) is not numbered and runs automatically as a dependency when needed.

The script outputs:
- Which cells actually ran (e.g., `Ran [cell_2, cell_3, cell_4]`)
- Any print/stdout output from each cell
- Errors with full stack traces (exits with code 1 on error)

# Capturing Outputs

If `output_dir` is provided to `run_cell.py`, cell outputs are automatically captured to disk:

```bash
uv run .claude/skills/marimo/scripts/run_cell.py cell_1 3005 ./outputs
```

Output:
```
Ran [cell_1, cell_2]
cell_1 output:
Processing data...

Captured outputs:
  cell_1 [dataframe]: ./outputs/cell_1_dataframe.json
  cell_2 [figure]: ./outputs/cell_2_figure.png
```

Supported output types:
- **DataFrames**: Saved as JSON (`cell_N_dataframe.json`)
- **Figures**: Saved as PNG (`cell_N_figure.png`)
- **Markdown**: Saved as HTML (`cell_N_markdown.html`)
- **Simple values**: Saved as text (`cell_N_text.txt`)

The user will expect you to use this to look at what they're seeing when they run their notebook in the marimo editor. If they're asking you for your opinion on an output, you must go find it and view it. You should run the cell that produces the output using `run_cell.py`. Typically, this cell is cheap analysis. Do NOT re-run the expensive data generation. *Never* claim or imply the claim that you've seen an output that you in fact did not view.

## Code Reuse

### Local Packages

Reference local packages via `[tool.uv.sources]` in the notebook's inline metadata:

```python
# /// script
# requires-python = ">=3.11"
# dependencies = ["pandas", "seaborn", "build_utils"]
#
# [tool.uv.sources]
# build_utils = { path = "../build_utils", editable = true }
# ///
```

With `editable = true`, changes to the package reflect immediately.

### Helper Files

For shared utilities, create a regular Python file (e.g., `helpers.py`) in the notebook's directory and import from it. Regular functions can use marimo constructs like `mo.status.spinner` or `mo.md()` without any special decorators.

```python
# helpers.py - a regular Python file
import marimo as mo
import time

def run_with_spinner(task_name: str):
    """Regular functions can use marimo constructs just fine."""
    with mo.status.spinner(title=f"Running {task_name}..."):
        time.sleep(1)
    return f"{task_name} completed"
```

Then import in your notebook's setup cell:

```python
with app.setup:
    import marimo as mo
    from helpers import run_with_spinner
```

# Further Reading

The marimo documentation is **excellent**. When you encounter marimo-specific issues or unknowns (API signatures, interactivity options, etc), use search! The docs are available on the docs.marimo.io domain.

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/j-r-beckett) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-11 -->

