Skip to main content

Building Notebooks

Boardflare uses marimo as the notebook surface and Pyodide as the browser Python runtime. This guide covers the notebook concepts that matter once you move beyond the starter example.

Reactive notebook basics

Boardflare uses marimo, a reactive Python notebook. It looks familiar if you have used notebooks before, but its execution model is intentionally different from an execution-history notebook.

Cells form a dependency graph

Consider three cells:

price = 100
discount_rate = 0.10
net_price = price * (1 - discount_rate)
net_price

The last cell depends on variables defined upstream. If price or discount_rate changes, marimo reruns the dependent calculation so the displayed result stays consistent with the source.

You normally should not rely on “run these cells in this historical order.” Structure the notebook so dependencies are visible in the variables each cell reads and defines.

Avoid redefining shared variables

A marimo notebook expects a clear owner for a shared variable. Prefer:

raw_sales = inputs["sales"]

followed by:

clean_sales = raw_sales.dropna().copy()

rather than redefining sales in several unrelated cells.

This makes the dataflow graph easier to understand and reduces accidental dependency problems.

Workbook values are reactive inputs too

Boardflare's bf.inputs() widget connects Excel references to the same notebook dependency graph:

inputs = bf.inputs(
sales=bf.ref("Sales!A1:D20", headers=True),
)
inputs
sales = inputs["sales"]

When the workbook range changes, dependent notebook cells update.

Notebook controls participate in the same graph

Marimo UI elements such as sliders, dropdowns, tables, and buttons can be normal reactive inputs:

scenario = mo.ui.dropdown(
options=["Base", "Upside", "Downside"],
value="Base",
label="Scenario",
)
scenario

A downstream cell can read scenario.value; changing the control reruns affected cells without callback plumbing.

This is why the same notebook can serve both exploratory analysis and, when useful, an app-style presentation.

Display Boardflare bridge widgets

Two Boardflare calls return Anywidget models that must remain displayed:

inputs = bf.inputs(...)
inputs

and:

publication = bf.publish(...)
publication

The displayed models own the live connection to the workbook. Treat them as integration endpoints, not decorative outputs.

Keep notebook stages clear

For a substantial analysis, a useful shape is:

workbook inputs

validation / normalization

model / transformation

diagnostics / charts / controls

optional publication to Excel

See Designing notebooks for a complete pattern.

Source is Python

Marimo notebooks are stored as Python source. That makes the notebook a normal source artifact rather than an opaque execution-history file. Boardflare saves that source with the Excel workbook so the analysis can be reconstructed when the workbook reopens.

Learn more about marimo

Boardflare documents the Excel integration contract. For the complete marimo programming model, editor features, UI elements, and reactive notebook practices, use the official marimo documentation.

Designing maintainable notebooks

A maintainable Boardflare notebook has a clear boundary between the Excel workbook, the Python analysis, and the results the workbook actually needs to consume.

Keep one upstream input registry

Declare workbook dependencies together so the notebook's connection to Excel is easy to inspect.

inputs = bf.inputs(
transactions=bf.ref("Data!A1:G500", headers=True),
scenario="Control!B2",
discount_rate="Control!B3",
)
inputs

Use sheet-qualified references in multi-sheet workbooks. Unqualified A1 references follow the host's active-sheet behavior, which is usually less explicit for a workbook intended to be shared.

Refactor the boundary, not every formula

A common migration mistake is to move an existing workbook into Python cell by cell. Instead, first define what Excel should continue to own and where Python should begin.

Suppose a workbook currently has:

Inputs sheet
B2 = starting revenue
B3 = monthly growth
B4 = churn
B5 = months

Forecast sheet
dozens of copied formulas implementing the recurring model

A cleaner workbook/notebook boundary is:

inputs = bf.inputs(
starting_revenue="Inputs!B2",
monthly_growth="Inputs!B3",
churn="Inputs!B4",
months="Inputs!B5",
)
inputs

Then keep the model in a normal function:

def build_forecast(starting_revenue, growth, churn, periods):
value = float(starting_revenue)
rows = [["Period", "Revenue"]]
for period in range(1, int(periods) + 1):
value *= 1 + float(growth) - float(churn)
rows.append([period, value])
return rows

forecast = build_forecast(
inputs["starting_revenue"],
inputs["monthly_growth"],
inputs["churn"],
inputs["months"],
)

Excel still owns the visible assumptions and can still own reconciliations and presentation formulas. Python owns the repeated model logic.

Let marimo manage dependency order

Marimo builds a dependency graph from cell references. Put calculations in downstream cells and avoid hidden mutable state or callbacks that recreate manual notebook execution order.

A useful rule is one owner per public variable: define an important value in one cell and let downstream cells reference it. Do not repeatedly redefine the same variable across cells.

Separate model logic from notebook UI

Keep domain calculations in ordinary Python functions and use marimo UI components for interactive controls. This makes the analytical logic easier to understand and test independently of presentation.

Use worksheet cells for durable business assumptions; use notebook controls for transient exploration such as scenario, risk multiplier, or visualization choices.

The current Revenue Command Center follows that split:

Drivers sheet assumptions

├── starting MRR
├── growth / churn
├── margin / opex
└── target / simulation count


Reactive Python model


Notebook controls
scenario / growth lift / risk multiplier

Validate at the boundary

Do not let malformed workbook data silently flow into a consequential model. Validate as soon as workbook values enter Python.

required = {"SKU", "Demand", "Demand Std", "Lead Time"}
missing = required.difference(inputs["skus"].columns)
if missing:
raise ValueError(f"Missing required columns: {sorted(missing)}")

service_level = float(inputs["service_level"])
if not 0.5 <= service_level < 1.0:
raise ValueError("Service level must be between 0.5 and 1.0")

For finance/accounting workflows, add control totals and reconciliation assertions before publication:

source_total = float(source["Amount"].sum())
output_total = float(cleaned["Amount"].sum())
if abs(source_total - output_total) > 0.01:
raise ValueError("Control total changed during transformation")

Useful checks include required cells/columns, expected types, bounded assumptions, duplicate identifiers, reconciliation totals, infeasible constraints, and explicit handling of missing values.

Organize the notebook for maintenance

For a nontrivial notebook, this sequence is usually easier to maintain:

  1. Title and explanation — explain what the analysis does, its assumptions, and what the user may change.
  2. Imports — standard and third-party packages.
  3. Notebook controls — transient UI choices when interaction helps the analysis.
  4. Workbook input registry — one displayed bf.inputs() cell.
  5. Normalization/validation — convert workbook values into model-ready structures.
  6. Domain model — deterministic functions and calculations.
  7. Presentation — tables, charts, explanations, exception queues.
  8. Publication — one displayed bf.publish() cell with the complete registry.

Do not mix workbook bridge calls throughout every analytical cell. Keeping the input and publication boundaries obvious makes the notebook easier to review.

Keep one downstream publication registry

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

A successful publication atomically replaces the previous output/function registry. A failed candidate publication does not partially replace the last successful one.

Published objects are live-session state. The workbook persists the source that recreates them, not the Python objects themselves.

Use outputs and functions for different jobs

Use BF.OUTPUT() for state the reactive notebook has already calculated: KPI blocks, forecasts, exception queues, fitted parameters, selected allocations.

Use BF.FUNCTION() for short callable calculations that take worksheet arguments and return a result through the live notebook model. Published functions should be bounded and non-blocking; a long synchronous callable can block the Python kernel even after its worksheet result times out.

Use App mode only when the notebook needs a simplified interface

Most notebooks can remain in Edit mode for their entire useful life. When the same analysis becomes a repeatable tool for another user, choose Open as: App so the next session emphasizes controls, explanations, and outputs.

App mode is presentation only, not an authorization boundary. Do not rely on it to protect secrets or proprietary source from a workbook recipient.

Workbook versus notebook responsibilities

Put in the workbookPut in the notebook
User-entered assumptionsAnalytical/model logic
Source data already maintained in ExcelData transformation that benefits from Python
Reviewable formulas and reconciliationsStatistics, simulation, optimization, specialized libraries
Familiar tables and reportsReactive controls and custom visualizations
Final worksheet formulasPublished notebook state and callable functions

Performance and size boundaries

Design for the actual workbook/runtime protocol rather than assuming an unlimited desktop process.

Important current limits include:

BoundaryCurrent limit
Notebook source200,000 UTF-8 bytes
Named workbook inputs128
Cells per input reference100,000
Aggregate input cells250,000
Complete input snapshot512 KiB encoded JSON
Published outputs128
Published functions128
Cells per published value/function result100,000
Active worksheet function subscriptions512
Concurrent Python function executions8
Queued Python function executions128
Function timeout60 seconds

Practical implications:

  • bind the workbook ranges the model actually needs rather than whole sheets;
  • prefer one vectorized/table calculation over thousands of worksheet function calls;
  • publish a table as one output instead of hundreds of independent scalar outputs when possible;
  • keep BF.FUNCTION() callables short;
  • use external Python when the workflow is fundamentally a large batch/file/system job rather than an Excel-connected notebook.

See Workbook API for conversion rules and Packages and environment for browser-runtime constraints.

Testing before sharing

At minimum, test:

  • representative inputs and boundary values;
  • missing/invalid workbook data;
  • control totals or model invariants;
  • save, close, and reopen behavior;
  • intended Edit/App presentation;
  • BF.OUTPUT() cold start;
  • BF.FUNCTION() cold start and optional arguments;
  • package loading in a fresh session;
  • network/API failure paths if used;
  • a second user opening and using the workbook without author intervention when the notebook will be shared.

Use the Revenue Forecasting example as a worked reference, then inspect the other current examples for patterns closer to accounting, operations, analytics, quality, optimization, or engineering.

Packages and environment

Boardflare's notebook runs Python through Pyodide, a CPython distribution compiled for WebAssembly and the browser. This removes the need for a separate local Python installation but creates different compatibility boundaries from desktop Python.

Current resolved runtime

The current checked-in notebook export resolves to:

ComponentCurrent shipping buildHow Boardflare owns it
marimo0.23.15Explicitly pinned by the notebook export pipeline
Boardflare notebook helper wheelboardflare 0.4.1Explicit package version in the local runtime source
Pyodide used by the generated marimo worker314.0.0Resolved inside the generated stock-marimo runtime, not maintained as a separate Boardflare source pin

Treat this as a description of the current checked-in export, not a compatibility promise for future workbooks. Marimo and the Boardflare helper version are explicit build inputs; the Pyodide value is reported from the generated runtime and should be re-verified after a marimo export upgrade.

Packages demonstrated by the shipping catalog

The current Boardflare demos exercise packages including:

PackageCurrent demo use
pandasworkbook table materialization, cleaning, reshaping
NumPyvectorized modeling and simulation
SciPySobol QMC, statistics, curve fitting, probability transforms
Matplotlibcharts and analytical visualizations

A package appearing here means it is used by current checked-in examples; it does not mean every package/version combination from desktop Python will work in the browser.

How package loading works

The stock marimo/Pyodide runtime can load packages available in the Pyodide distribution and can install compatible wheels through micropip.

For a package with a browser-compatible wheel:

import micropip
await micropip.install("textdistance")

Then import it normally:

import textdistance

Whether this succeeds depends on the package and its dependencies. Pure-Python wheels are generally the simplest case; packages that require unsupported native binaries or operating-system services may not be installable.

See Pyodide's official package list, package loading guide, and WebAssembly constraints.

Browser constraints

A browser Python runtime does not have the same operating-system privileges as desktop Python. Expect important differences around:

  • native extensions that do not have a Pyodide-compatible build;
  • unrestricted local file-system traversal;
  • subprocesses and desktop applications;
  • raw sockets and low-level networking;
  • CORS and browser HTTP rules;
  • operating-system credentials/environment variables;
  • libraries that assume a normal CPython desktop/server environment.

A library importing successfully on a developer laptop is not proof that it will work in Boardflare.

Network requests

User-authored Python can make browser-compatible HTTP requests, but those requests operate under browser security rules. The destination service must support the relevant cross-origin/authentication pattern.

If an API works from requests on a server but fails in Boardflare, check CORS, authentication redirects, forbidden headers, cookie behavior, and whether the service supports browser clients.

Do not embed long-lived secrets in notebook source that will be saved with and shared in the workbook.

Choosing the right execution environment

RequirementRecommended approach
Reactive workbook analysis and operator UIBoardflare notebook
pandas/NumPy/SciPy work supported by PyodideBoardflare notebook
CORS-compatible browser APIBoardflare can be appropriate
Batch processing local foldersExternal Python
Reading/writing hundreds of independent filesExternal Python
Desktop application or Office automationExternal Python / appropriate Office automation
Unsupported compiled/native packageExternal or managed Python
Scheduled/headless system jobExternal automation runtime

The practitioner research reviewed for Boardflare shows the same split: Python inside Excel is strongest for bounded analysis and interactive notebook logic; Python around Excel remains stronger for file/system automation. See What People Actually Use Python in Excel For.