How We Built a Reactive Python Notebook Runtime Inside Excel

Excel
Python
Architecture
How Boardflare connects a reactive marimo/Pyodide notebook to Excel through the shared runtime, workbook bridge, source persistence, and worksheet outputs.
Published

June 22, 2026

The design goal for Boardflare’s Python experience was not simply “put a code editor in Excel.” It was to give substantial Python work a coherent reactive notebook while keeping the workbook directly connected as a source-data, review, output, and delivery surface.

That led to a runtime with several deliberately separate pieces: Excel’s long-lived shared runtime, a Boardflare host, a cross-origin marimo notebook, browser Python through Pyodide, Anywidget capabilities, a spreadsheet-host abstraction, workbook source persistence, and live worksheet outputs/functions. The underlying platform concepts are documented by Microsoft in its Office add-in runtime guide, by marimo as a reactive Python notebook, by Pyodide as Python compiled for the browser, and by anywidget as a widget specification and toolkit.

This article explains why those pieces exist and how they fit together. The maintained specifications live in Architecture and Runtime and Security and Data Flow.

The complete picture

A useful way to understand the system is to start with the full topology rather than with one API call:

flowchart TB
    subgraph Excel[Excel]
        WB[Workbook cells / ranges / names]
        XML[Workbook Custom XML\nsaved notebook source]
        CF[BF.OUTPUT / BF.FUNCTION]
        Shared[Long-lived Office shared runtime]
    end

    subgraph Boardflare[Boardflare parent host]
        UI[Task pane / Notebook surface]
        Session[Notebook session + capabilities]
        Bridge[Spreadsheet Bridge]
        Registry[Live publication registry]
    end

    subgraph Notebook[Separate-origin notebook iframe]
        Marimo[Stock marimo]
        Pyodide[Pyodide / browser CPython]
        Inputs[bf.inputs Anywidget]
        Publish[bf.publish Anywidget]
    end

    WB <--> Bridge
    XML <--> Session
    CF <--> Shared
    Shared --> UI --> Session
    Session <-->|validated MessagePorts| Marimo
    Marimo --> Pyodide
    Pyodide --> Inputs
    Pyodide --> Publish
    Inputs <-->|workbook input capability| Session
    Publish <-->|output/function capability| Session
    Session <--> Bridge
    Session <--> Registry
    Registry <--> CF

There are three ideas behind this shape:

  1. Excel remains useful. We do not require every assumption, input, or deliverable to move into the notebook.
  2. The notebook owns the Python program. Multi-step transformations, models, controls, charts, and explanations can live in one reactive source file instead of being distributed across worksheet cells.
  3. The boundary is explicit. Notebook Python gets workbook capabilities through a small public API rather than receiving unrestricted access to the add-in parent.

For a workbook user, the consequence is simpler than the diagram: no separate desktop Python installation is required; declared worksheet inputs can drive reactive notebook recalculation; selected notebook results can flow back to worksheet formulas; and the saved artifact is notebook source in the workbook rather than a serialized Python process. AI authoring is optional and is not part of the calculation path.

Why marimo fits spreadsheet work

Spreadsheet users already expect dependency-driven recalculation. Change an input and downstream work should update.

marimo is a reactive Python notebook. Its documentation describes dependent cells rerunning when referenced values change and notebooks being stored as Python source rather than an opaque notebook database. That maps naturally to a workbook-connected model.

Boardflare adds workbook dependencies explicitly:

import boardflare as bf

inputs = bf.inputs(
    sales=bf.ref("Sales!A1:D20", headers=True),
    scenario="Assumptions!B2",
)
inputs

Downstream cells read inputs["sales"] and inputs["scenario"] like ordinary Python values. When Excel changes one of those references, Boardflare refreshes the input model and marimo reruns dependent cells through its normal reactive graph.

The spreadsheet and notebook therefore do not need competing recalculation systems.

Why run Python in the browser?

A workbook is much easier to distribute when every recipient does not need to recreate the author’s desktop Python installation.

Marimo’s WebAssembly runtime uses Pyodide to run CPython in the browser. That provides a substantial Python environment without a separate local Python installation.

It also creates a clear compatibility boundary. Browser Python is not desktop/server Python. Pyodide documents limitations around local files and subprocesses, raw sockets, and browser networking such as CORS. Some native dependencies and desktop integrations therefore do not fit this environment.

That is a tradeoff, not something we try to hide: the browser runtime makes workbook distribution easier, while external Python remains a better fit for workloads that require unrestricted operating-system capabilities.

Keeping marimo stock

A major design decision was to put product-specific behavior around marimo rather than inside a private fork.

Marimo owns:

  • notebook editing;
  • source serialization;
  • reactive dependency analysis;
  • Python execution;
  • notebook UI primitives.

Boardflare owns:

  • Excel/shared-runtime startup;
  • workbook source persistence;
  • workbook input/output capabilities;
  • worksheet functions;
  • product presentation and App mode;
  • AI provider/account policy;
  • the parent/notebook security boundary.

That separation gives us a cleaner upgrade path and makes failures easier to classify. If an Excel formula cannot see a published result, for example, the question is usually about the live publication capability or startup lifecycle—not about a hidden modification to marimo internals.

Anywidget is the notebook-side capability surface

The public boardflare package returns Anywidget models for the connections that need a live browser counterpart. Anywidget’s front-end specification explicitly separates one-time model initialization from per-view rendering, which is the lifecycle distinction Boardflare relies on here.

inputs = bf.inputs(
    assumptions=bf.ref("Assumptions!A1:B8", headers=True),
)
inputs

and:

publication = bf.publish(
    outputs={"summary": summary},
    functions={"discount": discount},
)
publication

The displayed widget is not decorative. Its model owns the capability connection. That is why Boardflare documentation tells authors to keep bf.inputs() and bf.publish() displayed as cell results.

This model/view distinction matters. A widget model can render more than one visual view without multiplying workbook subscriptions, and destroying a view does not have to mean destroying the underlying capability.

The notebook does not get unrestricted access to the parent

The marimo runtime executes in a different-origin iframe. The parent establishes separate source, input, and outputs capabilities only after validating the connection.

sequenceDiagram
    participant Child as Notebook / Anywidget
    participant Parent as Boardflare parent
    participant Port as Dedicated MessagePort

    Child->>Parent: Connect request
    Parent->>Parent: Validate child window + exact origin
    Parent->>Parent: Validate protocol + session + nonce
    Parent->>Parent: Validate capability + generation
    Parent-->>Child: Transfer MessagePort
    Child<<->>Port: Ongoing capability traffic
    Port<<->>Parent: Active capability only

The goal is not to claim that arbitrary Python is safe. Notebook source is executable code. The goal is to avoid making “whatever can call postMessage” equivalent to the workbook API.

The maintained Security and Data Flow page documents the current validation checks and trust boundaries in more detail.

One notebook model, multiple spreadsheet hosts

Notebook code should not care whether the spreadsheet underneath it is production Excel or the browser spreadsheet used for demos.

The shared @boardflare/spreadsheet-bridge package puts host-specific spreadsheet operations behind a common interface:

flowchart TD
    Notebook[Boardflare notebook integration] --> Bridge[Spreadsheet Bridge]
    Bridge --> ExcelDriver[Excel driver]
    Bridge --> UniverDriver[Univer driver]
    ExcelDriver --> Office[Office.js / Excel]
    UniverDriver --> Univer[Univer browser workbook]

The two hosts are deliberately not described as identical. Excel has workbook Custom XML persistence and a shared custom-function runtime. The public demo has a browser page/session lifecycle. The bridge lets the notebook model share useful concepts without erasing those host differences.

Publishing selected results back to Excel

A notebook does not have to end at a chart in the task pane. It can expose a deliberately small worksheet-facing API.

For values and tables:

=BF.OUTPUT("summary")

For short callable Python functions:

=BF.FUNCTION("discount", A1, B1)

Both are backed by a live registry owned by a displayed bf.publish() model.

sequenceDiagram
    participant Publish as bf.publish
    participant Host as Boardflare host
    participant Excel

    Publish->>Host: Prepare values for workbook date system
    Host-->>Publish: Prepared
    Publish->>Host: Claim complete output/function registry
    Host-->>Publish: Registry acknowledged
    Excel->>Host: BF.OUTPUT subscription / BF.FUNCTION invocation
    Host-->>Excel: Live value or Python result

The claim is atomic: a replacement publication does not tear down the prior successful registry until the replacement has validated and claimed successfully.

The publication widget can also show which worksheet formulas are actively consuming outputs/functions. The same information is available to Python through publication.consumers.

Why Excel’s shared runtime matters

Excel custom functions and a task pane can be configured to use the same long-lived JavaScript runtime. Microsoft’s shared-runtime documentation describes that arrangement; Boardflare uses the model so a worksheet formula can need Python even when the user has not visibly opened the Notebook tab first.

sequenceDiagram
    participant Excel
    participant Formula as BF.OUTPUT / BF.FUNCTION
    participant Shared as Boardflare shared runtime
    participant Notebook as marimo / Pyodide

    Excel->>Formula: Recalculate workbook
    Formula->>Shared: Subscribe / invoke
    Shared->>Notebook: Mount saved notebook runtime
    Notebook->>Shared: Subscribe workbook inputs
    Shared-->>Notebook: Initial workbook snapshot
    Notebook->>Notebook: Reactive graph calculates
    Notebook->>Shared: Claim publication registry
    Shared-->>Formula: Published value / function result
    Formula-->>Excel: Resolve streaming formula

While that startup is still healthy but incomplete, Excel can keep a streaming function in its normal #BUSY! state rather than showing a fake placeholder result.

Authentication is not part of calculation startup

Another important separation is between the calculation runtime and Notebook AI eligibility.

Boardflare lets the notebook calculation runtime mount while Office authentication is still resolving. BF.OUTPUT() and BF.FUNCTION() therefore do not depend on a user opening sign-in UI.

Office identity is used separately to decide whether the Marimo AI authoring controls are enabled. At launch, Notebook AI is restricted to eligible work or school Microsoft identities; personal or unresolved identities receive the same notebook calculation runtime with AI disabled.

That separation prevents an optional authoring capability from becoming a hidden dependency of workbook calculation.

Persistence is source, not a frozen Python process

Boardflare does not try to serialize a live interpreter, DataFrames, widgets, subscriptions, or Python function objects into the workbook.

It saves the notebook source and its preferred Edit/App opening presentation. Reopening reconstructs the runtime:

flowchart TD
    Saved[Saved notebook source + opening mode]
    Runtime[Start marimo / Pyodide]
    Inputs[Hydrate bf.inputs]
    Graph[Run reactive graph]
    Publish[Claim bf.publish registry]
    Excel[Resolve worksheet consumers]

    Saved --> Runtime --> Inputs --> Graph --> Publish --> Excel

This source-first contract is easier to reason about and makes the durable artifact compatible with ordinary source practices such as review, diffing, backup, and version control when the author chooses to surface the .py file externally.

Uploading source is intentionally staged

The add-in can open an existing Marimo .py file, but choosing a file is not a persistence operation.

stateDiagram-v2
    [*] --> Saved: saved workbook source or bundled starter
    Saved --> Staged: upload .py
    Staged --> Saved: Marimo Save + verified workbook write
    Staged --> Saved: startup failure + Restore saved notebook

The replacement source starts in a fresh Edit session. The previous workbook copy remains durable until the author explicitly saves the replacement and Boardflare verifies the workbook write. That gives a broken upload a recovery path without silently destroying the last saved notebook.

Download follows the same source-first philosophy: it exports source that Marimo has already submitted to Boardflare. It cannot magically serialize editor changes that were never saved.

AI stays on the authoring side

AI is useful here because it can accelerate writing and revising normal Python. It is not a required component of the calculation runtime.

An AI action can send the user’s prompt plus selected/supplied notebook context through Boardflare’s configured AI path. The response is authoring output. The durable artifact is still ordinary source that can be inspected and tested before a workbook is distributed.

That is an important distinction for analytical software: the workbook does not need a model to improvise the core business calculation every time it opens.

The legacy function path stays separate

Boardflare still supports workbooks created with the earlier standalone Functions Editor. Those functions use workbook settings, Excel Name Manager LAMBDAs, and BOARDFLARE.EXEC.

They do not execute through the notebook registry:

flowchart LR
    NotebookFormula[BF.OUTPUT / BF.FUNCTION] --> NotebookRuntime[Live notebook registry]
    LegacyFormula[Legacy Name Manager function] --> Exec[BOARDFLARE.EXEC] --> LegacyWorker[Legacy runpy worker]

Keeping that boundary explicit matters for compatibility. Notebook startup or bundle changes should not accidentally become prerequisites for established BOARDFLARE.EXEC workbooks.

The design principle

The individual libraries and runtime versions will change. The more durable architecture is the separation of responsibilities:

Excel remains the workbook data, review, and delivery surface. The reactive notebook is the coherent home for substantial Python work. Boardflare supplies explicit runtime, persistence, and worksheet bridges between them.

For current implementation details, limits, and security controls, use Architecture and Runtime and Security and Data Flow rather than treating this dated launch article as a specification.