Agent skill
marimo-notebook-dev
Create, edit, run, and convert marimo reactive Python notebooks. Use when working with .py marimo notebook files, creating reactive/interactive notebooks, exploratory data analysis in notebook format, or converting from Jupyter (.ipynb) to marimo. Marimo notebooks are pure Python files with @app.cell decorators that enable reactivity. Also covers wigglystuff widgets for advanced interactivity.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/marimo-notebook-dev-fonnesbeck-marimo-notebook-dev
SKILL.md
Marimo Notebooks
Marimo notebooks are reactive Python notebooks stored as pure .py files. Cells auto-execute when dependencies change, modeled as a directed acyclic graph (DAG).
Core Concepts
Reactivity Model
- marimo uses static analysis to build a dependency graph from variable references and definitions
- When a cell runs, all cells referencing its defined variables automatically re-run
- Execution order follows the dependency graph, not visual cell order
- Each global variable must be defined by exactly one cell
- marimo does not track object mutations (like
list.append())—mutate in the same cell that creates the object, or create new variables
Avoiding Variable Name Conflicts
Each global variable must be defined by exactly one cell. Two strategies to avoid conflicts:
1. Wrap code in functions (preferred for reusable patterns):
@app.cell
def _(data):
def compute_mean_with_new_col(df):
temp = df.copy()
temp["new_col"] = temp["x"] * 2
return temp.mean()
return (compute_mean_with_new_col(data),)
2. Use meaningful, unique variable names:
@app.cell
def _(model1_data):
model1_transformed = model1_data.copy()
model1_transformed["new_col"] = model1_transformed["x"] * 2
return (model1_transformed,)
@app.cell
def _(model2_data):
model2_transformed = model2_data.copy()
model2_transformed["new_col"] = model2_transformed["x"] * 2
return (model2_transformed,)
Notebook Structure
import marimo
__generated_with = "0.10.0"
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
return (mo,)
@app.cell
def _(mo):
mo.md("# Hello")
return
if __name__ == "__main__":
app.run()
Key rules:
- Each cell is a function decorated with
@app.cell - Variables shared by returning tuples:
return (var1, var2,) - Cells receive variables as parameters:
def _(mo, df): - Execution order follows dependency graph, not position
- Name cells descriptively for CellTour targeting:
def model_specification():
CLI Commands
# Create & Edit
marimo new # Create new notebook
marimo new "build a dashboard" # AI-generated starter
marimo edit notebook.py # Open editor
marimo edit notebook.py --watch # Live reload on file changes
# Run as App
marimo run notebook.py # Run as app (code hidden by default)
marimo run notebook.py --include-code # Show code in app view
marimo run notebook.py --host 0.0.0.0 --port 8080 # Network accessible
marimo run notebook.py --base-url /subpath # Deploy at subpath
marimo run notebook.py --headless # No browser auto-open
# Run as Script
python notebook.py # Execute as Python script
python notebook.py -- --arg1 val1 # Pass CLI arguments (after --)
# Convert
marimo convert notebook.ipynb -o notebook.py # Jupyter to marimo
marimo convert notebook.md -o notebook.py # Markdown to marimo
# Export
marimo export html notebook.py -o out.html # Static HTML
marimo export html notebook.py -o out.html --include-outputs # With outputs
marimo export html-wasm notebook.py -o out.html # Interactive WASM
marimo export ipynb notebook.py -o out.ipynb # To Jupyter
marimo export ipynb notebook.py -o out.ipynb --include-outputs
marimo export script notebook.py -o out.py # Pure Python
marimo export md notebook.py -o out.md # Markdown
# Utilities
marimo check notebook.py # Lint and validate
marimo check notebook.py --fix # Auto-fix issues
marimo tutorial intro # Interactive tutorials
marimo tutorial markdown # Markdown tutorial
marimo tutorial plots # Plotting tutorial
marimo tutorial layout # Layout tutorial
Code Visibility in Run Mode
CRITICAL FOR TUTORIALS: By default, marimo run hides code. Use these options to show code:
mo.show_code() - Per-Cell Display (Recommended)
@app.cell
def model_definition(mo, pm, X, y):
with pm.Model() as model:
alpha = pm.Normal("alpha", mu=0, sigma=10)
beta = pm.Normal("beta", mu=0, sigma=10)
sigma = pm.HalfNormal("sigma", sigma=1)
mu = alpha + beta * X
likelihood = pm.Normal("y", mu=mu, sigma=sigma, observed=y)
# Show this cell's code alongside its output
return mo.show_code(model)
@app.cell
def sampling(mo, pm, model):
with model:
trace = pm.sample(1000)
# Position: "above" or "below" (default)
return mo.show_code(trace, position="above")
Key points:
mo.show_code(output)- shows code below output (default)mo.show_code(output, position="above")- shows code above output- In displayed code,
mo.show_code(...)is replaced with... - Use for EVERY code cell in tutorials/educational notebooks
CLI Flag - Show All Code
marimo run notebook.py --include-code
URL Parameter - For Hosted Apps
https://your-app.marimo.app/app?show-code=true
Markdown with mo.md()
@app.cell
def _(mo):
mo.md(r"""
# Title
Interpolate Python: {slider}
**LaTeX inline**: $f(x) = e^x$
**LaTeX block**:
$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$
**Icons** (Iconify): ::lucide:rocket:: or ::mdi:home::
""")
return
Key features:
- Use raw strings (
r"""...""") for LaTeX to avoid escaping backslashes - Interpolate UI elements directly:
f"Value: {slider}" - For complex objects, use
mo.as_html():f"Plot: {mo.as_html(fig)}" - Load LaTeX macros:
mo.latex(filename="macros.tex") - Icons:
mo.icon("lucide:rocket", size=20, color="blue")
UI Components (mo.ui.*)
Basic Inputs
slider = mo.ui.slider(0, 100, value=50, label="Value")
number = mo.ui.number(0, 100, value=50)
text = mo.ui.text(value="", placeholder="Enter text")
text_area = mo.ui.text_area(value="", placeholder="Multi-line")
checkbox = mo.ui.checkbox(value=False, label="Enable")
switch = mo.ui.switch(value=False, label="Toggle")
dropdown = mo.ui.dropdown(["a", "b", "c"], value="a")
radio = mo.ui.radio(["option1", "option2"], value="option1")
multiselect = mo.ui.multiselect(["a", "b", "c"])
range_slider = mo.ui.range_slider(0, 100, value=(20, 80))
Date/Time
date = mo.ui.date(value="2024-01-01")
datetime = mo.ui.datetime()
date_range = mo.ui.date_range()
Buttons
button = mo.ui.button(label="Click") # Simple button
run_button = mo.ui.run_button(label="Run") # For triggering computation
refresh = mo.ui.refresh(default_interval="1s") # Auto-refresh timer
# Counter button
counter = mo.ui.button(value=0, on_click=lambda count: count + 1)
# Toggle button
toggle = mo.ui.button(value=False, on_click=lambda v: not v)
Files & Media
file = mo.ui.file(kind="file", filetypes=[".csv", ".json"])
file_browser = mo.ui.file_browser(initial_path="./")
microphone = mo.ui.microphone()
Data Components
table = mo.ui.table(df) # Interactive table with selection
dataframe = mo.ui.dataframe(df) # Editable dataframe
data_editor = mo.ui.data_editor(df) # Full editor
data_explorer = mo.ui.data_explorer(df) # No-code exploration
Advanced
code_editor = mo.ui.code_editor(value="", language="python")
chat = mo.ui.chat(model) # Chat interface
anywidget = mo.ui.anywidget(MyWidget()) # Custom widgets
tabs = mo.ui.tabs({"Tab1": content1, "Tab2": content2})
Grouping UI Elements
# Dynamic arrays
array = mo.ui.array([mo.ui.slider(0, 10) for _ in range(5)])
array.value # List of values
# Dynamic dictionaries
dictionary = mo.ui.dictionary({"x": mo.ui.slider(0, 10), "y": mo.ui.slider(0, 10)})
dictionary.value # Dict of values
# Forms (require submit button)
form = mo.ui.text().form()
form.value # None until submitted
# Batch multiple elements in markdown
form = mo.md("""
**Name**: {name}
**Age**: {age}
""").batch(
name=mo.ui.text(),
age=mo.ui.number(0, 120)
).form()
Layout Functions
# Stacking
mo.hstack([el1, el2, el3], justify="center", gap=2) # Horizontal
mo.vstack([el1, el2, el3], align="start", gap=1) # Vertical
# Containers
mo.accordion({"Section 1": content1, "Section 2": content2})
mo.tabs({"Tab 1": content1, "Tab 2": content2}) # Use mo.ui.tabs for state
mo.callout(content, kind="info") # info, warn, success, danger, neutral
mo.carousel([item1, item2, item3])
mo.sidebar([nav_content])
# Alignment
mo.center(content)
mo.left(content)
mo.right(content)
# Display
mo.plain(content) # No styling
mo.tree({"a": {"b": 1}}) # Tree view
mo.json(data) # JSON viewer
mo.stat(value="42", label="Users", caption="+5%") # Statistic card
# Lazy loading (defer until visible)
mo.lazy(expensive_component) # Object
mo.lazy(lambda: compute_expensive()) # Function (deferred)
mo.lazy(async_func, show_loading_indicator=True) # Async with spinner
# Outline/Table of Contents
mo.outline() # Creates navigation outline
# Routing (multi-page apps)
mo.routes({
"#/": render_home,
"#/about": render_about,
mo.routes.CATCH_ALL: render_home,
})
# Combined with navigation
mo.sidebar([
mo.nav_menu({
"#/": f"{mo.icon('lucide:home')} Home",
"#/about": f"{mo.icon('lucide:user')} About",
}, orientation="vertical")
])
Output Functions
# Dynamic output manipulation
mo.output.replace(new_content) # Replace cell output
mo.output.append(additional) # Append to output
mo.output.clear() # Clear output
mo.output.replace_at_index(val, 2) # Replace at specific index
# Console redirection
with mo.redirect_stdout():
print("This goes to cell output")
with mo.capture_stdout() as buffer:
print("Captured")
text = buffer.getvalue()
Status Indicators
# Progress bar (like tqdm)
for item in mo.status.progress_bar(items, title="Processing"):
process(item)
# With manual control
with mo.status.progress_bar(total=100, title="Loading") as bar:
for i in range(100):
do_work()
bar.update(title=f"Step {i}")
# Spinner
with mo.status.spinner(title="Loading...", subtitle="Please wait"):
load_data()
# Conditional spinner
mo.status.spinner(title="Loading") if loading else mo.md("Done!")
Control Flow
# Stop execution conditionally
mo.stop(condition, mo.md("*Message when stopped*"))
# Example: require button click
run_button = mo.ui.run_button()
mo.stop(not run_button.value, mo.md("Click Run to execute"))
expensive_computation()
# Example: require form submission
mo.stop(form.value is None, mo.md("Submit the form first"))
process(form.value)
# Conditional output
result if condition else None
Caching
# In-memory cache (session only, fastest)
@mo.cache
def expensive_function(x, y):
return compute(x, y)
# Persistent cache (survives restarts)
@mo.persistent_cache(name="embeddings")
def compute_embeddings(text):
return model.encode(text)
# Block-style persistent cache
with mo.persistent_cache(name="data_load"):
df = load_large_dataset()
processed = transform(df)
# LRU cache with size limit
@mo.lru_cache(maxsize=128)
def bounded_cache(x):
return compute(x)
State Management
Warning: Use sparingly—over 99% of cases don't need mo.state().
@app.cell
def _(mo):
get_count, set_count = mo.state(0)
return get_count, set_count
@app.cell
def _(mo, get_count, set_count):
mo.ui.button(
label=f"Count: {get_count()}",
on_click=lambda _: set_count(lambda n: n + 1)
)
return
Use mo.state() only when:
- Maintaining history tied to UI elements
- Synchronizing multiple UI elements bidirectionally
- Introducing cycles across cells at runtime
SQL Integration
Install: pip install "marimo[sql]"
@app.cell
def _(mo, threshold):
# Query Python dataframes by name
result = mo.sql(f"""
SELECT * FROM my_dataframe
WHERE value > {threshold.value}
LIMIT 100
""")
return (result,)
# External sources
mo.sql("""
SELECT * FROM 's3://bucket/file.parquet';
SELECT * FROM read_csv('data.csv');
""")
SQL can query: Python dataframes (polars/pandas), DuckDB, PostgreSQL, MySQL, SQLite, Snowflake, BigQuery, Redshift, CSV/Parquet files, S3
Escape literal braces with {{}}:
SELECT unnest([{{'a': 42, 'b': 84}}]);
Output types: native (DuckDB relation), lazy-polars, polars, pandas, auto
Interactive Plotting
# Altair with selection
chart = alt.Chart(df).mark_point().encode(x="x", y="y")
selection = mo.ui.altair_chart(chart, chart_selection="point")
selected_data = selection.value # DataFrame of selected points
# Plotly (scatter, treemap, sunburst)
fig = px.scatter(df, x="x", y="y")
interactive = mo.ui.plotly(fig)
interactive.value # Selected points
# Matplotlib interactive (pan, zoom, hover)
fig, ax = plt.subplots()
ax.plot(x, y)
mo.mpl.interactive(fig)
Supported libraries: Matplotlib, Seaborn, Plotly, Altair, Bokeh, HoloViews, hvPlot, Leafmap
Media Functions
mo.image(src) # URL, path, or file-like object
mo.image_compare(img1, img2) # Side-by-side comparison
mo.video(src)
mo.audio(src)
mo.pdf(src)
mo.download(data, filename="file.csv", label="Download")
mo.plain_text(text)
External Data Access
# CLI arguments (after --)
args = mo.cli_args() # Dict of arguments
size = mo.cli_args().get("size") or 100
# URL query parameters
params = mo.query_params()
params["key"] # Read
params.set("key", "value") # Write (updates URL, triggers reactivity)
# File watching (reactive to changes)
watched_file = mo.watch.file("config.json") # Re-runs on file change
watched_dir = mo.watch.directory("data/") # Re-runs on structure change
App Metadata
meta = mo.app_meta()
meta.mode # "edit", "run", "script", "test", or None
meta.theme # "light" or "dark"
meta.request # HTTP request info (headers, cookies, params)
Embedding Notebooks
# Import another notebook
from other_notebook import app as other_app
@app.cell
async def _(other_app):
# Must call from different cell than import
result = await other_app.embed()
return result.output, result.defs
# Run programmatically with overrides
outputs, defs = other_app.run(defs={"param": value})
HTML Manipulation
html = mo.Html("<div>Custom HTML</div>")
html.text # Get underlying string
# Styling
styled = html.style({"color": "red", "font-size": "20px"})
styled = html.callout(kind="info")
# iframes (for scripts that need execution)
mo.iframe(html_with_scripts, width="100%", height="400px")
Best Practices
- Wrap reusable code in functions - Keeps intermediate variables local and avoids namespace pollution
- Use meaningful, unique variable names - e.g.,
model1_sigma,model2_sigmainstead of generic names - Don't mutate across cells - Mutate in same cell or create new variables
- Write idempotent cells - Same inputs should produce same outputs
- Use mo.stop() - Gate expensive operations behind conditions/buttons
- Split into modules - Import from
.pyfiles, use auto-reload - Use lazy loading -
mo.lazy()for expensive components in tabs/accordions - Cache expensive ops -
@mo.cachefor session,@mo.persistent_cachefor disk
Common Gotchas
- Closures in loops: Use default args
lambda v, i=i: ...notlambda v: ... i - on_change handlers: Only work if element is bound to global variable
- Dynamic UI elements: Must wrap in
mo.ui.array(),mo.ui.dictionary(), ormo.ui.batch() - Type annotations: Registered as references unless quoted:
x: "SomeType" - matplotlib cut-off: Call
plt.tight_layout()before outputting - dotenv: Use
dotenv.load_dotenv(dotenv.find_dotenv(usecwd=True))
Configuration
Environment Variables
MARIMO_OUTPUT_MAX_BYTES(default 8MB)MARIMO_STD_STREAM_MAX_BYTES(default 1MB)MARIMO_SKIP_UPDATE_CHECKMARIMO_SQL_DEFAULT_LIMIT
User Config (.marimo.toml)
- Runtime: autorun, lazy mode, SQL output format
- Display: theme, font size, output placement
- Editor: hotkeys, vim mode, copilot, formatting
App Config (in notebook file)
- Width, title, custom CSS, HTML head
Deployment
# Docker
marimo run notebook.py --host 0.0.0.0 --port 8080
# Health endpoints
/health # 200 OK
/healthz # Alternative
/api/status # JSON status
Platforms: Docker, Kubernetes, HuggingFace, Railway, SkyPilot, Slurm/HPC
Testing
# Run as pytest
pytest notebook.py
# With doctest
python -m doctest notebook.py
marimo notebooks are pure Python—test like any Python module.
Wigglystuff Widgets
Install: pip install wigglystuff
from wigglystuff import Slider2D, Paint, SortableList, Matrix, CellTour
import marimo as mo
slider2d = mo.ui.anywidget(Slider2D())
slider2d.x, slider2d.y
paint = mo.ui.anywidget(Paint(width=400, height=300))
paint.to_pil()
sortable = mo.ui.anywidget(SortableList(items=["A", "B", "C"]))
sortable.items
matrix = mo.ui.anywidget(Matrix(rows=3, cols=3))
matrix.data
# CellTour for guided tutorials
tour = mo.ui.anywidget(CellTour(
steps=[
{"cell_name": "intro", "title": "Welcome", "description": "...", "position": "bottom"},
{"cell_name": "model", "title": "Model", "description": "...", "position": "bottom"},
],
auto_start=False,
show_progress=True
))
Available: Slider2D, Matrix, Paint, EdgeDraw, SortableList, ColorPicker, KeystrokeWidget, GamepadWidget, WebkitSpeechToTextWidget, CopyToClipboard, WebcamCapture, CellTour
See references/wigglystuff.md for complete widget reference.
Resources
- Official docs: https://docs.marimo.io
- Examples: https://github.com/marimo-team/examples
- Tutorials: https://github.com/Haleshot/marimo-tutorials
- UI components: See
references/ui_components.md - Wigglystuff: See
references/wigglystuff.md
Didn't find tool you were looking for?