🚀 marimo is doing a launch week!

Read the announcement
Engineering

Implementing marimo pair: letting agents use notebooks as a tool

marimo pair lets coding agents such as Claude Code and Codex CLI use a marimo notebook as a tool. It combines a CLI that runs Python in the notebook’s live kernel with an agent skill that teaches the workflow. From inside the live kernel, an agent writes and runs code, inspects results, and commits its work as notebook cells. It can work independently or alongside you.

Our first version adapted the notebook to the tools coding agents already had. The agent inspected the running notebook through read-only MCP tools, but changed it by editing the .py file and relying on --watch to reload the file. The split added overhead and left the agent to coordinate feedback across tools.

For the second version, we redesigned the interaction around the running notebook. We gave the agent Python for inspecting live state and an internal API for changing notebook cells. This post explains how we arrived at that design and how marimo checks and preserves the agent’s work.

Beyond the shell

Out of the box, coding agents have tools for reading and editing files and running shell commands. That toolset fits conventional software development because the work is stored in files and a set of changes can usually be checked by running tests or a build.

For data work, people often prefer a different set of tools that retain intermediate values between steps (for example, Excel, RStudio, Jupyter, and marimo). This statefulness enables fast iteration, but it also introduces challenges for reproducibility. Because results can depend on state accumulated during a session, a tool must preserve enough information to recreate them. Tools differ in whether they capture this information at all and how much they preserve when they do.

A diagram with two axes. The horizontal axis runs from throwaway on the left to durable artifact on the right; the vertical axis runs from slow iteration at the bottom to fast iteration at the top. One-off scripts and automated pipelines sit in the slow-iteration region. REPL and IPython, Jupyter, and marimo sit in the fast-iteration region, with marimo furthest toward a durable artifact.

As coding agents became part of our users’ workflows, they wanted to use them in notebooks too. The experience was poor. Agents could change notebook code, but could not readily inspect the live values and outputs that guide the work.

We wanted agents to use notebooks the way people do. They should be able to run code, inspect results, decide what to try next, and preserve useful work so it can be resumed later or shared with others.

The MCP detour

Our first version adapted the notebook to the tools coding agents already had. The agent edited the .py file with its normal file tools, relied on --watch to load changes into the notebook, and queried marimo’s original MCP server for the running state.

The MCP tools worked for questions we had anticipated, such as reporting notebook structure or previewing common data structures. Each tool exposed a fixed view, while what the agent needed to inspect depended on the notebook and the task.

Reading and writing also followed different paths. After editing the file, the agent had to wait for --watch and query one or more MCP tools to inspect the result. The extra calls added latency and token use, while reliable feedback depended on the agent coordinating the sequence.

We considered adding more endpoints (#4345, #7133, #7625, #8172, #8175, #8176). They could cover more cases, but they would not fundamentally change how the agent worked with the notebook. We would still decide in advance what the agent could inspect, and the agent would still coordinate edits with separate queries. We needed to let the agent decide what to inspect as it worked, act on the running notebook, and receive direct feedback.

Give the model Python

In the next version, we gave the agent a CLI for running Python directly in the live marimo kernel.

bash scripts/execute-code.sh -c "print(df.head())"

execute-code replaced the read-only MCP tools. Each call runs in a scratchpad where the notebook’s variables are in scope and anything the agent defines stays local to the call. The agent writes Python to inspect whatever state it needs, and the call returns the resulting values and errors directly. Runtime inspection changes how the agent works with the notebook by letting it decide what to examine based on the objects and results it encounters, then use that feedback in its next action. Python gives the agent a general-purpose language for doing this, and coding agents already know how to write it.

execute-code gave the agent access to the kernel’s live state. It could inspect variables and run temporary code, but it had no supported way to inspect cells or make durable changes to the notebook. We needed to expose the notebook’s structure and operations through Python as well.

Harden the common path

execute-code builds on marimo’s existing scratchpad, which runs Python against the notebook without changing it. We opened that to agents and tested how much notebook functionality they could discover through Python.

“Can you say hi to me in the marimo UI?”

Claude tried several APIs, hit errors, and eventually displayed a toast saying “Hi Trevor 👋!” in the corner of the notebook.

“OK, how did you do that?”

We knew the toast API existed and were curious whether Claude could find it through Python. After a few attempts, it did. We then asked it to create, edit, and run cells. Claude could discover and sequence the internal calls, but the task required it to understand marimo’s internals and coordinate each step.

Direct Python access was enough for one-off experiments, but common notebook operations needed stable calls and structured results. Creating a cell should not require the agent to understand marimo’s internals, and a failed edit should identify what went wrong.

We kept Python as the general interface and added a small API for common notebook operations, with ordinary calls and predictable results.

Code mode

Code mode exposes notebook structure and operations through an internal Python API built for agents. The module is available inside execute-code.

import marimo._code_mode as cm
 
async with cm.get_context() as ctx:
    cid = ctx.create_cell("df.head()")
    ctx.edit_cell(cid, code="df.head(20)")
    ctx.run_cell(cid)

The agent uses cm from inside Python rather than as a separate tool. It imports cm and writes normal Python around the notebook operations, including loops, helpers, and error handling. The operations queue inside the async with block and are applied on exit as one transaction against the notebook.

async with cm.get_context() as ctx:
    a = ctx.create_cell("x = 1")
    b = ctx.create_cell("x = 2")  # x is now defined in two cells
    # nothing has run yet
# on exit: marimo rejects the batch
# ("x" is multiply-defined across the graph)

The async with block is the transaction boundary. Before applying the queued operations, marimo runs the checks it uses for any notebook edit. It looks for syntax errors, names defined in more than one cell, cycles in the dependency graph, and the removal of names that other cells use. If a check fails, marimo rejects the whole batch, names the cells at fault, and leaves the notebook unchanged.

When the batch runs, marimo reports each cell’s result back to the agent as clean, errored, or broken by an upstream change. The agent can use those results to decide what to fix next.

An API for a model

cm is not a public, versioned API. Its caller is a model, and the model can read the API at runtime.

>>> help(cm)
Help on package marimo._code_mode in marimo:

NAME
    marimo._code_mode - Code mode: programmatic notebook editing via
    async context manager.

DESCRIPTION
    Internal, agent-only API. Not part of marimo's public API. No
    versioning guarantees. May change or be removed without notice.

    Usage:

        import marimo._code_mode as cm

        async with cm.get_context() as ctx:
            cid = ctx.create_cell("x = 1")
            ctx.edit_cell("my_cell", code="z = 42")
            ctx.delete_cell("old_cell")

The skill tells the agent to run help(cm) when it starts. The docstring holds the examples and the current shape of the API, so the skill and the implementation can change independently.

We wrote earlier skills like marimo-notebook that described marimo’s execution rules so agents could edit the .py file correctly. The model had to keep rules about dependencies, multiple definitions, and re-execution in its context and apply them throughout the task. Code mode moves that responsibility into marimo. Every edit goes through the same checks as an edit made in the UI. If a change violates a rule, marimo rejects it with a specific error so the agent can correct it and continue.

Together, runtime inspection and these checks give the agent direct feedback throughout longer tasks, so it can correct errors and continue without a person stepping in.

More than a REPL

When we launched marimo pair on Hacker News, some of the discussion compared it to giving agents a Python REPL. The comparison describes the execution layer, where execute-code accepts Python, runs it in the kernel, and returns the result. Over a longer task, marimo distinguishes temporary code used to inspect state and try ideas from code the agent decides to preserve.

The agent uses execute-code as a scratchpad without changing the notebook. When it wants to preserve a step, it commits the code as cells through cm. marimo checks the batch using the same rules as an edit made in the UI, rejects invalid changes before they take effect, and adds accepted cells to the notebook’s dataflow graph and .py file.

Temporary attempts remain in the scratchpad, while accepted cells incrementally build a reproducible Python program that can be rerun and reviewed.

Diagram of marimo pair showing an agent running Python in a scratchpad,
committing checked transactions as notebook cells, and producing a marimo
notebook artifact.

Notebooks as a tool

Our first design separated observation, action, and validation across different interfaces. The model had to assemble those pieces into a reliable loop, and adding more tools would only expand what each piece could do.

In the current design, all three happen against the running notebook. The agent inspects state with Python, changes cells through code mode, and receives validation and results directly from marimo. The model decides what to try, while the runtime handles the notebook’s execution rules.

Because accepted changes become cells, the same process that supports iteration also builds a reproducible Python program. Each accepted step extends a notebook that can be rerun, reviewed, and continued by a person or an agent.

  • Deterministic execution. Cells run in dataflow order, not authoring order, so the notebook replays the same way elsewhere.
  • A Python artifact. The notebook is a .py file that is git-diffable, importable, runnable with python notebook.py, and deployable with marimo run.
  • No hidden state. Upstream changes invalidate the cells that depend on them, which avoids the “run cells 4, 7, 2, 9” problem common in shared Jupyter notebooks.
  • Checked commits. Every cell cm accepts has passed marimo’s static checks for syntax, names defined in more than one cell, cycles in the dependency graph, and removing names other cells use.

Try it

marimo pair is available now.

It works with any agent that supports Agent Skills.

npx skills add marimo-team/marimo-pair

Pair on an existing notebook.

/marimo-pair pair with me on my_notebook.py

Or hand it a question and let it build the notebook end to end.

/marimo-pair build me a notebook that explores <dataset/question>

Learn more and get started at marimo.io/pair.