| name | notebooks |
| description | Author, execute, and deliver reproducible analysis notebooks in marimo (default) or Jupyter, with all cells run end-to-end and figures embedded. Also converts between marimo and Jupyter on request. |
Notebooks
A single skill for authoring, validating, and delivering reproducible analysis notebooks. Marimo is the default format; Jupyter is supported for existing .ipynb files and when a downstream tool requires JSON. Conversion between the two formats is part of this skill.
A notebook is not "done" until it has been executed end-to-end on a fresh kernel and every figure is embedded in the delivered file.
Instructions
-
Pick the format.
- New notebook: write a marimo
.py notebook. Use the canonical cell layout (one concept per cell, final expression renders, no if guards around outputs, no try/except for control flow).
- Existing
.ipynb to extend or polish: keep it as Jupyter unless the user asks to convert.
- Conversion: see "Convert between marimo and Jupyter" below.
-
Outline before coding. Write the notebook plan (purpose, data sources, analysis steps, expected outputs/plots) as the first markdown cell, then implement against that plan.
-
Keep marimo cells clean. These are hard rules for every .py notebook:
- Markdown cells use one plain triple-quoted string:
mo.md(r"""...""") or mo.md(f"""...""") only when interpolation is required. Put the prose directly inside the string; never paste quoted string fragments such as " ... " lines inside the markdown body.
- Do not leave empty generated cells, whitespace-only cells, or
@app.cell def _(): return placeholders. Remove them before final verification.
- Do not accept a marimo "fix" prompt blindly. If one is accepted during interactive editing, inspect the diff immediately and remove unintended PEP 723/header/cell churn.
-
Set up the kernel and dependencies.
- Marimo. Pin dependencies in the PEP 723 script header at the top of the
.py file:
Run with uv run marimo run <notebook.py> (uv reads the header and resolves the env automatically) or with marimo edit --sandbox <notebook.py> for interactive work.
- Jupyter. Register a named ipykernel for the project's pixi env before the first execution and pin the kernel in the notebook metadata. The kernel name is mandatory — the generic
python3 kernel leaks the system interpreter:
pixi run python -m ipykernel install --user --name <project> --display-name "<project> (pixi)"
Then in <notebook>.ipynb confirm:
"kernelspec": {"name": "<project>", "display_name": "<project> (pixi)"}
Add every import used in the notebook to pixi.toml (or the project's requirements.txt / environment.yml) so the kernel can resolve it from a clean install.
-
Load data with project-relative paths. Prefer DuckDB for TSV/Parquet (duckdb.read_csv, duckdb.read_parquet). Avoid absolute paths and ~. Avoid hidden state from the runtime working directory.
-
Run checks and all cells to generate plots. Execute the notebook headlessly on a fresh kernel before delivery:
- Marimo: run
uvx marimo check <notebook.py> before export and fix every reported issue or warning, including empty-cells and markdown formatting. Then run uv run marimo export ipynb <notebook.py> -o <notebook.executed.ipynb> or uv run marimo run <notebook.py> for a non-interactive smoke run; for a deterministic HTML artifact, uv run marimo export html <notebook.py> -o <notebook.html>. Run uvx marimo check <notebook.py> again after the final edit/export cycle.
- Jupyter:
python scripts/execute_notebook.py <notebook.ipynb> (writes <notebook>.executed.ipynb) or pixi run jupyter nbconvert --to notebook --execute --inplace <notebook.ipynb>.
-
Evaluate the plots, then refine. This step is required, not optional. After the run-all execution:
- Open the executed notebook (or exported HTML) and visually inspect every figure.
- Check for: empty axes, mis-scaled axes (log when linear was intended or vice versa), missing labels/legends, overlapping ticks, illegible font sizes at target output size, ambiguous palettes, colorbars without units, NaN-driven gaps, axis ranges clipping data, broken layouts.
- If a figure is wrong or unclear, edit the source cell and re-run end-to-end. Repeat until each figure communicates what the surrounding markdown says it communicates.
- Record what changed between revisions in a brief "Figure revision log" markdown cell or in the run log.
-
Deliver pre-executed notebooks. The artifact handed back to the user must:
- Have every cell executed against the registered kernel.
- Embed every figure (PNG / SVG cell outputs) directly in the
.ipynb (or in the marimo HTML export).
- Be reproducible from a clean clone: a new environment built from the PEP 723 header (marimo) or
pixi install + jupyter nbconvert --to notebook --execute (Jupyter) must reproduce the same notebook end-to-end.
-
Convert between marimo and Jupyter when the user asks for it:
.ipynb → marimo .py: uvx marimo convert <notebook.ipynb> -o <notebook.py>, then uvx marimo check <notebook.py>, then clean up Jupyter artifacts (display() calls, %magics, indented final expressions, ipywidget usage). See references/widgets.md and references/latex.md for ipywidget→marimo and MathJax→KaTeX mappings.
- marimo
.py → .ipynb: uvx marimo export ipynb <notebook.py> -o <notebook.ipynb>.
- After conversion, re-run step 6 (check/execute), step 7 (inspect plots), and step 8 (deliver pre-executed).
Quick Reference
| Task | Action |
|---|
| Author marimo notebook | Edit .py, run uv run marimo edit --sandbox <notebook.py> |
| Author Jupyter notebook | Register pixi kernel, set notebook kernelspec, edit .ipynb |
| Lint marimo notebook | uvx marimo check <notebook.py> |
| Execute marimo headlessly | uv run marimo export ipynb <notebook.py> -o <executed.ipynb> |
| Execute Jupyter headlessly | python scripts/execute_notebook.py <notebook.ipynb> |
Convert .ipynb → marimo | uvx marimo convert <notebook.ipynb> -o <notebook.py> |
Convert marimo → .ipynb | uv run marimo export ipynb <notebook.py> -o <notebook.ipynb> |
| Marimo references | references/MARIMO.md, references/UI.md, references/SQL.md, references/STATE.md, references/EXPORTS.md, references/PYTEST.md, references/TOP-LEVEL-IMPORTS.md, references/DEPLOYMENT.md |
| Conversion references | references/widgets.md, references/latex.md |
| Pixi + Jupyter | references/pixi_jupyter.md |
| Plot style | references/plot_style.md |
| Templates | templates/marimo_notebook_template.py, templates/jupyter_kiss_template.py |
| Headless executor | scripts/execute_notebook.py |
Input Requirements
- Notebook scope and goals (what question, what data, what output).
- Data file paths (TSV/Parquet preferred for DuckDB ingestion).
- For marimo:
uv available on PATH, or marimo installed in the environment.
- For Jupyter:
pixi available and a pixi.toml (or equivalent env spec) for the project.
Output
- A reproducible notebook (
.py for marimo, .ipynb for Jupyter) with narrative markdown cells, code cells, and embedded figures.
- A pre-executed copy (
<notebook>.executed.ipynb or an .html export) where every cell has been run on a fresh kernel.
- A short "Figure revision log" recording any plot-revision rounds.
Quality Gates
Examples
Example 1: New marimo notebook
import marimo
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
import polars as pl
import duckdb
import matplotlib.pyplot as plt
return mo, pl, duckdb, plt
@app.cell(hide_code=True)
def _(mo):
mo.md(r"""
# Analysis notebook
This notebook loads project data, validates it, and renders the requested figures.
""")
return
@app.cell
def _(duckdb):
df = duckdb.read_parquet("data/measurements.parquet").pl()
df.head()
return (df,)
@app.cell
def _(df, plt):
fig, ax = plt.subplots(figsize=(5, 3.2))
ax.scatter(df["x"], df["y"], s=10)
ax.set_xlabel("x (units)"); ax.set_ylabel("y (units)")
fig
return
Then:
uvx marimo check notebook.py
uv run marimo export ipynb notebook.py -o notebook.executed.ipynb
uvx marimo check notebook.py
Example 2: Jupyter notebook with a named pixi kernel
pixi run python -m ipykernel install --user --name myproject --display-name "myproject (pixi)"
python skills/notebooks/scripts/execute_notebook.py notebooks/analysis.ipynb \
--kernel myproject \
--out notebooks/analysis.executed.ipynb
Example 3: Convert .ipynb to marimo
uvx marimo convert notebooks/legacy.ipynb -o notebooks/legacy.py
uvx marimo check notebooks/legacy.py
uv run marimo export ipynb notebooks/legacy.py -o notebooks/legacy.executed.ipynb
Troubleshooting
Issue: Jupyter notebook executes locally but fails on a teammate's machine.
Solution: The kernel was unpinned (python3) or used a packaged interpreter outside the project's pixi env. Re-register a named kernel and pin it in the notebook kernelspec.
Issue: Marimo cell does not render a figure.
Solution: The figure must be the final expression of the cell. Indented expressions inside if blocks or expressions buried before other statements will not render.
Issue: Figures look correct interactively but the executed file shows empty plots.
Solution: Code is mutating shared state across cells (e.g. plt.gcf() reuse). Build a fresh fig, ax = plt.subplots(...) per cell and return / display fig as the final expression.
Issue: Converted notebook fails marimo check.
Solution: Remove leftover display(...) calls, drop %magic lines that have no marimo equivalent, and rewrite ipywidget usage using mo.ui.* per references/widgets.md.