marimo

star 11

Essential guidance for working with marimo notebooks — reading, viewing, editing, refactoring, running, and capturing outputs. Covers marimo's reactive model, namespace management patterns, cell execution via run_cell.py, and output capture. Use this skill for ANY task involving marimo notebooks, including just reading or viewing them. Trigger on: user mentions "notebook" or "marimo", you see marimo.App() in a file, the task involves a .py file that is a notebook, or you need to invoke marimo from the command line.

j-r-beckett By j-r-beckett schedule Updated 1/6/2026

name: marimo description: Essential guidance for working with marimo notebooks — reading, viewing, editing, refactoring, running, and capturing outputs. Covers marimo's reactive model, namespace management patterns, cell execution via run_cell.py, and output capture. Use this skill for ANY task involving marimo notebooks, including just reading or viewing them. Trigger on: user mentions "notebook" or "marimo", you see marimo.App() in a file, the task involves a .py file that is a notebook, or you need to invoke marimo from the command line.

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

@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:

@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:

#!/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

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:

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:

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:

# /// 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.

# 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:

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.

Install via CLI
npx skills add https://github.com/j-r-beckett/SpeedReader --skill marimo
Repository Details
star Stars 11
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator