| title | Export format reference |
|---|---|
| description | Durable directory, index, state, representation, asset, integrity, and version contracts for notebook exports. |
A notebook export is one canonical index.json and the content-addressed assets
declared by that index. Python, browser, agent, and custom clients consume the
same durable relation between exported states and named outputs.
report/
index.json
assets/
<sha256>.npy
<sha256>.arrow
<sha256>.output.json
<sha256>.cell.json
<sha256>.bin
index.json is the single entry point. Asset paths are derived from codec and
SHA-256.
An export directory may contain application-owned sidecar files beside
index.json. The declared export closure consists of index.json and the exact
files referenced under assets/. Repository generations apply a stricter
private directory closure before reuse.
The root object contains exactly:
| Field | Contract |
|---|---|
schema |
Exact value marimo-export.export.v1 |
spec_sha256 |
SHA-256 of canonical ExportSpec bytes |
default_state |
Fingerprint of the default normalized state |
notebook |
Notebook filename and document SHA-256 |
producer |
marimo version, marimo-export version, implementation SHA-256 |
inputs |
Ordered input definition names |
control_bindings |
Scoped UI object IDs mapped to inputs and semantic paths |
outputs |
Ordered published output names |
aliases |
Authored state names mapped to state fingerprints |
states |
Nonempty mapping from state fingerprints to complete state data |
The index is canonical UTF-8 JSON. Object keys, number spelling, string encoding, and scalar tags must match the canonical producer representation byte for byte.
Each root input name is an opaque, nonempty string of at most 255 UTF-8 bytes. ExportSpec and notebook inspection validate Python identifiers before export production. Durable readers preserve the written names across Unicode database versions.
control_bindings is the browser event-routing authority for prepared UI
controls. Each binding contains a root input name and a typed path through
that input's control tree. The input value must exactly match one root input
name. Root controls use an empty path. Sequence children append an index
step, mapping children append a key step, and wrapper or form children append
an element step. Multiple scoped IDs may carry the same binding when one
source control appears in several projections. Consumers use these records
directly and do not parse projection-scoped object IDs.
A binding may also carry a domain with the frontend values its control can
send, in the same form a state row uses:
{
"input": "threshold",
"path": [],
"domain": { "kind": "range", "start": 0, "stop": 1, "step": 0.1 }
}| Domain kind | Exact fields | Frontend values |
|---|---|---|
range |
kind, start, stop, step |
start + k * step for each integer k >= 0 up to and including stop |
options |
kind, values |
Each item of values, 1 to 4096 items |
The producer records these domains:
| Control | Domain |
|---|---|
mo.ui.slider |
range from the slider's start, stop, and step, or step 1 |
mo.ui.slider built from steps |
range over step indexes from 0 to the last index |
mo.ui.number with start, stop, step |
range from those arguments |
mo.ui.dropdown |
options with one-item arrays such as ["north"], and [] when the dropdown allows no selection |
mo.ui.radio |
options with the option strings |
mo.ui.checkbox, mo.ui.switch |
options with false and true |
Other controls carry no domain. A control whose domain differs between
prepared states also carries no domain, because no single set describes it.
A browser control change that resolves to no prepared state keeps the current display. Compare each domain with the prepared state inputs at the binding's path to find control values that have no prepared state.
Each state key is the lowercase SHA-256 over canonical JSON for its inputs.
Each state entry contains exactly:
| Field | Contract |
|---|---|
inputs |
Complete object with the exact root input-name set |
outputs |
Object with the exact root output-name set |
Each outputs value is an output descriptor or an unavailable output. Every
alias targets a declared state. Several aliases may target the same fingerprint
when authored rows normalize to the same complete vector. One output name keeps
one codec and media type across every state where it has a descriptor.
default_state references one declared fingerprint.
Every descriptor contains:
- codec
- media type
- provenance
- inline scalar or JSON value, or an asset reference
The producer implementation SHA-256 identifies the exact installed marimo-export Python source set that created the index. Capture freezes this identity before execution and commits it after the end-of-operation identity check succeeds.
Descriptor provenance contains the stored value's python_type. For an
exporter-backed output, that type is marimo_export.outputs.BlobAsset, not the
selected notebook value's original type. Native marimo cache keys and return
references remain inside the producer process.
BlobAsset descriptors also record filename and portable metadata.
The descriptor shape is closed by codec:
| Descriptor kind | Exact fields |
|---|---|
| Scalar or portable JSON | codec, media_type, provenance, value |
| Rendered output, complete cell, NumPy, or Arrow | codec, media_type, provenance, asset |
| BlobAsset | codec, media_type, provenance, asset, filename, metadata |
provenance contains exactly python_type. asset contains exactly sha256
and size. Readers reject missing and unknown descriptor fields.
An optional output that has no value in a state stores an unavailable output in place of a descriptor:
{
"unavailable": {
"message": "builtins.str has no representation as image/png.",
"reason": "representation_unavailable"
}
}The object contains exactly unavailable. unavailable contains exactly:
| Field | Contract |
|---|---|
reason |
representation_unavailable, value_missing, or stopped |
message |
Nonempty string of at most 1,024 UTF-8 bytes, with no control characters or surrounding whitespace |
representation_unavailable means the exporter found none of its accepted
representations for the selected value. value_missing means the selector names
an attribute or item the value lacks in that state. stopped means a cell in
the output's dependency closure called mo.stop. The message describes the
condition for a person.
An unavailable output references no asset and takes no part in representation
consistency. Readers expose it as ExportOutput.unavailable and reject reads
of its value with output_unavailable.
Export format version 1 accepts seven codecs:
| Codec | Required media type | Stored form | Asset suffix |
|---|---|---|---|
marimo.scalar.v1 |
application/vnd.marimo.scalar.v1+json |
Inline scalar | None |
marimo.json.v1 |
application/vnd.marimo.json.v1+json |
Inline portable JSON | None |
marimo.output.v1 |
application/vnd.marimo.output.v1+json |
Canonical rendered-output snapshot | .output.json |
marimo.cell.v1 |
application/vnd.marimo.cell.v1+json |
Canonical complete-cell snapshot | .cell.json |
numpy.npy.v1 |
application/x-npy |
NumPy NPY file | .npy |
apache.arrow.file.v1 |
application/vnd.apache.arrow.file |
Arrow IPC file or stream framing | .arrow |
marimo.blob-asset.msgpack.v1 |
Valid BlobAsset media type from descriptor | Canonical MessagePack BlobAsset |
.bin |
The codec identifies the native envelope. A BlobAsset media type identifies the representation within that envelope. Custom media types extend the output space while the codec set remains closed for version 1.
JSON-compatible scalars remain inline. Values that JSON cannot preserve use these closed tagged objects:
{ "type": "bigint", "value": "9007199254740992" }
{ "type": "float", "value": "nan" }
{ "type": "float", "value": "infinity" }
{ "type": "float", "value": "-infinity" }
{ "type": "float", "value": "negative-zero" }The tagged bigint decimal has no leading plus sign or unnecessary leading zeroes and must lie outside the JavaScript safe-integer range. Untagged numbers must be finite, with integers inside that range.
Readers reject unknown or noncanonical scalar tags.
marimo.json.v1 stores any portable JSON value inline in the descriptor.
Objects, arrays, strings, booleans, finite numbers in the JavaScript-safe
range, and null retain their JSON shape. Python returns a frozen value through
ExportOutput.json(). Browser clients load it through jsonLoader().
marimo.output.v1 stores the following shape. Whitespace is added for display.
Stored bytes use canonical JSON:
{
"schema": "marimo.output.v1",
"projectionSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"ownerCellId": "cell-id",
"output": {
"channel": "output",
"mimetype": "text/markdown",
"data": "<span>Ready</span>"
},
"resources": {
"files": {},
"modelNotifications": [],
"functions": {
"cell-id-projection-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-ui-cell-id-ui": []
},
"uiValues": {
"cell-id-projection-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-ui-cell-id-ui": 3
}
}
}output is null when the formatted value has no terminal output. The record
is inert. ownerCellId identifies the authored source cell whose UI object
graph produced the output.
marimo.cell.v1 stores the following shape. Whitespace is added for display.
Stored bytes use canonical JSON:
{
"schema": "marimo.cell.v1",
"projectionSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"cell": {
"id": "cell-id",
"name": "summary",
"codeSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"config": { "disabled": false }
},
"outcome": "completed",
"output": {
"channel": "output",
"mimetype": "text/plain",
"data": "42"
},
"console": [{ "channel": "stdout", "mimetype": "text/plain", "data": "ready\n" }],
"resources": {
"files": {},
"modelNotifications": [],
"functions": {},
"uiValues": {}
}
}cell.name is null for an unnamed cell. output is null when the completed
cell has no terminal output. console preserves the ordered marimo console
records captured during the selected cell's fresh execution.
Both snapshot records use the same replay resources:
filesmaps each closed virtual resource to a slash-prefixed/@file/path and data URL. Model notifications retain marimo's trusted relative./@file/URL, which resolves through that normalized key.modelNotificationscontains the reachable AnyWidget model lifecycle closure in replay order. Model IDs contain the record'sprojectionSha256and are scoped by planned output.functionsmaps every projection-scoped UI object ID to the sorted, unique names of the Python functions it cannot call in the export. The array is empty for ordinary controls. A table in a read-only output lists functions such assearchandget_column_summaries. The producer omits a form'svalidateregistration when the form has no validator.uiValuesmaps every projection-scoped registry-owned UI object ID to its accepted frontend value after state updates.
The producer applies the same scope to UI object IDs, random IDs, HTML
attributes, and structured UI references within one snapshot. Each UI ID begins
with the snapshot's ownerCellId or cell.id, preserving marimo's ownership
authorization, and ends in a projection-root structural path. Common controls
keep the same ID when a conditional tree adds or removes siblings. Model IDs
use their projection-scoped model namespace. This lets a consumer merge
rendered-output and complete-cell resources from the same live UI element.
Browser loaders validate and freeze these records. Rendering and model replay belong to the consuming application.
The marimo snapshot reference defines every record, message union, replay-resource field, and browser parser.
An asset reference contains its lowercase SHA-256 and byte size. Equal
(codec, SHA-256) identities share one asset across states. Reused identities
must agree on every descriptor fact.
Producer and local-reader bounds are:
- 64 MiB per asset
- 512 MiB across unique assets in one export
Browser callers can apply their own per-output and aggregate limits through the browser API.
The Python reader's 512 MiB closure limit includes index.json plus unique
declared assets. Descriptor records can represent assets up to 2,147,483,647
bytes so another format implementation can apply its own lower operational
limit.
A native BlobAsset MessagePack envelope contains exactly:
data: representation bytesmedia_type: validated media typefilename: optional portable basenamemetadata: portable JSON object
The envelope's media type, filename, and metadata must agree with the index descriptor before a reader returns the representation.
The browser scanner accepts the canonical minimal-width encoding for null, booleans, integers, finite float64 values, strings, binary data, arrays, and maps with unique string keys. It rejects float32 values, extension tokens, duplicate map keys, trailing bytes, nesting deeper than 256 levels, and more than 100,000 MessagePack values.
Readers verify:
- canonical index bytes
- exact input and output key sets
- closed unavailable-output records
- state fingerprints
- representation consistency across states
- asset path identity
- declared and observed byte length
- SHA-256
- NPY or Arrow framing
- canonical BlobAsset envelope
- descriptor and envelope agreement
- complete declared asset closure
The loaded index.json is the integrity root. Verification establishes
consistency with that index. It does not authenticate who produced the index.
Python NotebookExport.identity and browser NotebookExport.identity expose
the lowercase SHA-256 of the exact canonical index.json bytes.
| Value | Limit |
|---|---|
Canonical index.json |
16 MiB and 2,000,000 JSON values |
| Input, output, alias, and producer version name | 255 UTF-8 bytes |
| Control binding path | 256 typed steps |
| Control domain options | 4,096 values |
| Unavailable output message | 1,024 UTF-8 bytes |
| Media type | 1,024 printable ASCII bytes |
| BlobAsset metadata | 256 KiB of canonical portable JSON |
| Browser BlobAsset MessagePack nesting | 256 levels |
| Browser BlobAsset MessagePack values | 100,000 |
| Python producer or local-reader asset | 64 MiB |
| Python export closure | 512 MiB including index.json |
Every JSON value and every object key in index.json counts toward its
2,000,000-value limit. Python producers fail with export_invalid before they
write a larger index, and Python and browser readers reject one.
Producers check both limits from the authored states and outputs before any
notebook work, and again from the resolved plan. Each check counts every state
output at the cheapest entry it can write: the cheapest descriptor, or an
unavailable output for an optional output. After each state is prepared, the
producer counts that state at its exact cost and checks again. Assets, including
the resources of a read-only snapshot, live outside index.json and count only
through their descriptors. An export that cannot fit stops at the first check
that proves it, and the states it already prepared stay reusable.
The browser errors and limits reference lists browser defaults and caller overrides.
Serve index.json and each declared asset at byte-stable HTTP or HTTPS URLs.
Content encoding is permitted because browser readers verify the decoded body.
Use immutable caching for content-addressed assets. A replaceable index.json
needs a revalidation policy that matches the application's freshness contract.
Cross-origin applications need an explicit Cross-Origin Resource Sharing
(CORS) policy. The
browser API accepts a custom fetch implementation for credentials and request
policy.
Opening an export reads and validates index.json. Assets remain lazy until a
consumer reads one output or verifies the complete export.
State selection supports:
- the declared default state
- authored state alias
- exact complete input vector
- sparse patch from an existing state
Resolution returns an exported state already present in the export.
The schema identifier and codec identifiers version durable behavior. A reader rejects an unknown export schema or native codec. Readers released before unavailable outputs reject an index that contains one, so upgrade consumers together with producers that write optional outputs. Custom representations should use versioned media types so producer and consumer changes remain explicit.
Consume an export provides Python, browser, and agent workflows. Output representations maps built-in and custom representations to their consumers.