Work with Obsidian vaults (plain Markdown notes) and automate via obsidian-cli.
npx skills add pymc-labs/agent-skills --skill "marimo-notebooks"
Install specific skill from multi-skill repository
# Description
|
# SKILL.md
name: marimo-notebooks
description: |
ALWAYS use when: creating/editing marimo notebooks, working with any .py file containing @app.cell decorators, building reactive Python notebooks, doing exploratory data analysis in notebook form, converting Jupyter (.ipynb) to marimo, or when user mentions "marimo", "reactive notebook", or asks for an interactive Python notebook. Covers marimo CLI (edit, run, convert, export), UI components (mo.ui.*), layout functions, SQL integration, caching, state management, and wigglystuff widgets. If a task involves notebooks and Python, invoke this skill first.
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:
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,)
Never use underscore prefixes to generate unique variable names. No exceptions.
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 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
# Convert
marimo convert notebook.ipynb -o notebook.py # Jupyter to marimo
# Export
marimo export html notebook.py -o out.html # Static HTML
marimo export ipynb notebook.py -o out.ipynb # To Jupyter
# Validate
marimo check notebook.py # Lint and validate
marimo check notebook.py --fix # Auto-fix issues
Code Visibility in Run Mode
CRITICAL FOR TUTORIALS: By default, marimo run hides code. Use mo.show_code() to display it.
mo.show_code() - Per-Cell Display
IMPORTANT: Call mo.show_code() as a statement on its own line, NOT in the return statement.
@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)
mu = alpha + beta * X
pm.Normal("y", mu=mu, sigma=1, observed=y)
# Show this cell's code alongside its output
mo.show_code(model, position="above")
return (model,)
WRONG vs RIGHT patterns:
# WRONG - do not put mo.show_code() in return statement
return mo.show_code(result)
# RIGHT - call as statement, then return separately
mo.show_code(result, position="above")
return (result,)
position="above"shows code first, then output (best for tutorials)position="below"shows output first, then code (default)
Markdown with mo.md()
@app.cell
def _(mo):
mo.md(r"""
# Title
Interpolate Python: {slider}
**LaTeX**: $f(x) = e^x$
$$\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$$
**Icons**: ::lucide:rocket:: or ::mdi:home::
""")
return
- Use raw strings (
r"""...""") for LaTeX - Interpolate UI elements:
f"Value: {slider}" - For complex objects:
f"Plot: {mo.as_html(fig)}"
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")
checkbox = mo.ui.checkbox(value=False, label="Enable")
dropdown = mo.ui.dropdown(["a", "b", "c"], value="a")
radio = mo.ui.radio(["option1", "option2"], value="option1")
multiselect = mo.ui.multiselect(["a", "b", "c"])
Buttons
button = mo.ui.button(label="Click")
run_button = mo.ui.run_button(label="Run") # For triggering computation
Data Components
table = mo.ui.table(df) # Interactive table with selection
dataframe = mo.ui.dataframe(df) # Editable dataframe
data_explorer = mo.ui.data_explorer(df) # No-code exploration
Grouping UI Elements
# Forms (require submit button)
form = mo.ui.text().form()
# Batch in markdown
form = mo.md("""
**Name**: {name}
**Age**: {age}
""").batch(
name=mo.ui.text(),
age=mo.ui.number(0, 120)
).form()
See references/ui_components.md for complete reference.
Layout Functions
# Stacking
mo.hstack([el1, el2, el3], justify="center", gap=2)
mo.vstack([el1, el2, el3], align="start", gap=1)
# Containers
mo.accordion({"Section 1": content1, "Section 2": content2})
mo.tabs({"Tab 1": content1, "Tab 2": content2})
mo.callout(content, kind="info") # info, warn, success, danger, neutral
mo.sidebar([nav_content])
# Display
mo.tree({"a": {"b": 1}}) # Tree view
mo.stat(value="42", label="Users", caption="+5%")
mo.lazy(expensive_component) # Defer until visible
Output Functions
mo.output.replace(new_content) # Replace cell output
mo.output.append(additional) # Append to output
mo.output.clear() # Clear output
with mo.redirect_stdout():
print("This goes to cell output")
Status Indicators
# Progress bar
for item in mo.status.progress_bar(items, title="Processing"):
process(item)
# Spinner
with mo.status.spinner(title="Loading..."):
load_data()
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()
Caching
# In-memory cache (session only)
@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)
See references/caching.md for model output caching patterns.
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 only when maintaining history, synchronizing UI bidirectionally, or introducing cycles.
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
fig = px.scatter(df, x="x", y="y")
interactive = mo.ui.plotly(fig)
interactive.value # Selected points
# Matplotlib interactive
fig, ax = plt.subplots()
ax.plot(x, y)
mo.mpl.interactive(fig)
Supported: Matplotlib, Seaborn, Plotly, Altair, Bokeh, HoloViews, hvPlot
Wigglystuff Widgets
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()
tour = mo.ui.anywidget(CellTour(
steps=[
{"cell_name": "intro", "title": "Welcome", "description": "..."},
{"cell_name": "model", "title": "Model", "description": "..."},
],
auto_start=False
))
See references/wigglystuff.md for all widgets.
Best Practices
- Wrap reusable code in functions - Keeps intermediate variables local
- Use meaningful, unique variable names - e.g.,
model1_sigma,model2_sigma - Don't mutate across cells - Mutate in same cell or create new variables
- Write idempotent cells - Same inputs produce same outputs
- Use mo.stop() - Gate expensive operations behind conditions/buttons
- Use lazy loading -
mo.lazy()for expensive components in tabs/accordions - Cache expensive ops -
@mo.cachefor session,@mo.persistent_cachefor disk
CRITICAL: Pre-Edit Checklist
BEFORE making ANY edit to a marimo notebook:
- Read the current file state โ The file may have been modified by marimo's editor
- Run
marimo check notebook.pyโ Verify valid before and after edits
AFTER completing edits:
- Grep for
print(โ Replace ALL print statements with marimo output (mo.md(),mo.stat(),mo.callout()) - Run
marimo check notebook.pyโ Verify no errors introduced
Common Gotchas
Output & Display
- NEVER use print() in cells: Print statements do NOT display in run mode. Always use:
mo.md(f"**Label:** {value}")โ formatted textmo.stat(value=f"{x}", label="Label")โ metric cardsmo.callout(content, kind="info")โ callout boxes-
Return the dataframe/object directly โ automatic display
-
mo.show_code() must be called as a statement, not in return
-
Never delete output without replacing: When removing print statements, replace with equivalent marimo output
Reactivity & Variables
- 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"
Libraries
- matplotlib cut-off: Call
plt.tight_layout()before outputting - dotenv: Use
dotenv.load_dotenv(dotenv.find_dotenv(usecwd=True))
References
- UI components: references/ui_components.md
- Wigglystuff widgets: references/wigglystuff.md
- Caching patterns: references/caching.md
- Advanced features: references/advanced.md (SQL, deployment, testing, routing)
- Official docs: https://docs.marimo.io
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.