| name | notebook-to-static-app |
| description | Turn an existing marimo Python notebook into a polished static HTML, CSS, and TypeScript application by designing an ExportSpec, creating and capturing finite notebook states with marimo-export, loading outputs through @marimo-team/marimo-export, and validating the deployed browser experience. Use when a user points to a marimo .py notebook and wants a CDN-deployable interactive app that runs with no Python server or Python WebAssembly runtime. |
Notebook to Static App
Create a user-facing web application from completed marimo executions. Keep the
notebook unchanged. Precompute the choices the audience needs, then make the
browser experience feel authored for their task.
Read first
Read:
Inspect the current marimo-export docs and source when a command or type differs
from the references. The checkout is authoritative.
Invariants
- Leave the source notebook byte-for-byte unchanged.
- Use notebook definition names in state-row keys and output selectors.
- Run every selected output through normal marimo execution.
- Treat browser choices as a finite product surface, not a free-form Python
prompt.
- Choose CLI
build for a saved file and CLI capture for an existing live
session. Both write a deployment directory. Python capture() returns a
leased prepared export.
- The deployed app must run from static files with no Python process.
- Freeze every notebook data dependency into the export. Mounted charts must
not fetch their source dataset at runtime.
- Use exported values as the data source. Do not replace failed notebook data
with fixtures or invented values.
- Keep implementation terminology out of the visible app. Users should not see
export, state, codec, loader, cache, notebook, or Python unless the product
itself teaches those concepts.
Workflow
1. Understand the notebook
Read the complete notebook before choosing outputs. Identify:
- the audience and decision the notebook can support
- authored variables and UI controls that change meaningful results
- expensive or environment-dependent calculations
- definitions that contain data, metrics, charts, images, or widgets
- browser interactions that can continue without another Python execution
Write one internal product sentence:
<audience> uses this app to <decision or task> using <notebook result>.
Reject an app concept that merely displays notebook outputs or demonstrates
marimo-export features.
2. Choose the finite interaction model
Design three to seven named states unless the notebook supports a genuinely
different meaningful range. Each state should represent a recognizable scenario,
policy, cohort, threshold, or comparison.
Use sparse overrides. Let omitted inputs come from the captured baseline.
Prefer a small set of high-value choices over a Cartesian product of every
possible control.
Choose outputs that let the browser answer the product question. A strong app
usually combines:
- one or two scalar summaries
- one primary visual or interactive value
- one inspectable table, array, or detail view
Export only values the application loads.
3. Create the app workspace
Resolve scripts/scaffold_app.py through marimo_export.agent.agent_skill() and
create the local Python and Vite workspace from the notebook's PEP 723
dependencies. Select only the browser loader families the app will import.
Treat the generated src/main.ts as a loading shell. Replace it with the real
application after the ExportSpec is known.
4. Inspect the notebook boundary
For build, run:
uv run --project "$STATIC_APP_DIR" marimo-export inspect "$NOTEBOOK_PATH" --json
For capture, set NOTEBOOK_PORT and SESSION_ID, then run:
uv run --project "$STATIC_APP_DIR" marimo-export inspect \
"http://127.0.0.1:$NOTEBOOK_PORT" \
--session "$SESSION_ID" \
--json
Both commands return definition and cell records before state preparation.
Do not infer a definition from displayed prose or a local variable whose name
starts with _. Use the inspection result.
5. Author and preflight the ExportSpec
Create <name>.export.yaml beside the app. Use domain names for states and
published output names.
Run planning after authoring the spec. Correct missing input or output names
before preparation:
uv run --project "$STATIC_APP_DIR" marimo-export plan "$NOTEBOOK_PATH" \
--spec "$STATIC_APP_DIR/app.export.yaml" \
--json
The plan reports inferred inputs, normalized states, the default, observations,
reusable fingerprints, and missing fingerprints.
Choose the narrowest representation that preserves the browser experience:
- keep supported scalars, NumPy arrays, Arrow tables, and
BlobAsset values
native
- use Parquet for browser-readable table rows
- use Vega-Lite for interactive Altair charts
- use PNG when the audience needs a fixed chart image
- use AnyWidget bundles when the widget's saved browser model supports the
audience's local interaction
- add a focused custom exporter and paired loader when no built-in
representation fits
Inspect exported Vega-Lite data references. When a notebook chart points to a
remote dataset, use an export-time representation that embeds the data or
pair the chart with an exported table and rebuild the chart in the browser.
6. Create and verify the export
Use build when the workflow owns notebook startup, execution, cleanup, and the
deployment directory. Write to public/export, then run marimo-export verify
before the browser build.
Use CLI capture --output public/export when a running kernel already owns the
configured environment or completed computation. Use the Python capture()
context when the app needs to inspect, serve, or retain the prepared generation
before writing.
Run one producer mode for an app workflow. Do not hand-edit index.json or its
assets.
7. Build the browser application
Open ./export/, resolve named states, and load outputs explicitly. For a
changing prepared publication, use @marimo-team/marimo-export/prepared with one
PreparedStateController and an application-owned PreparedStatePort.
Keep one transition owner that:
- aborts stale loads
- disposes mounted charts or widgets
- loads all outputs for the selected state
- commits the new view only if the transition is still current
- renders a recoverable error when loading fails
Make state controls shareable through a URL hash or query parameter.
Follow the marimo design system:
- use compact controls, slate borders, white or near-black work surfaces, and
restrained blue interaction
- use PT Sans for UI text and Lora for editorial headings
- use borders before shadows
- keep charts and tables full-width and overflow-safe
- avoid gradients, oversized marketing heroes, nested cards, decorative
animation, and one-off palettes
Use one clear visual direction for the domain. Keep the app inspectable and
small. Prefer vanilla DOM code until a real composition problem justifies a
framework.
8. Validate the user boundary
Run:
- Python export verification for the selected producer mode
- TypeScript type checking and production build
- a static preview with the final export
- browser checks at desktop and mobile widths
Use a dedicated agent-browser --session name. Exercise every state control and
at least one interaction inside each mounted chart or widget. Confirm:
- visible results change with the selected domain scenario
- rapid changes do not leave stale content
- no console or page errors occur
- no page-level horizontal overflow occurs
- the final app makes no request to a Python server
- notebook result data loads from the static app origin
- the UI contains no implementation commentary
Close the browser session and notebook server after validation.
Failure discipline
Treat failures as product evidence.
- Missing definition: inspect the live session and correct the spec.
- Missing Python package: add the notebook's declared dependency to the app
environment.
- Unsupported output: choose a data definition or write a focused exporter and
loader pair.
- Custom exporter import failure: keep its module in the app and run the
notebook, build, and capture with the app directory on
PYTHONPATH.
- Capture package mismatch: run the notebook from the app environment with the
same marimo-export version and implementation.
- Loader resolution failure: consume the packed browser package, then install
the peer dependency for the imported loader.
- Blank or duplicated mount: make disposal and abort ownership explicit.
- Unhelpful state matrix: return to the audience decision and redesign the
states rather than adding more.
Record the failed command, error, cause, and correction while working. Keep
that diagnostic record out of the visible app.