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:
- Title and explanation — explain what the analysis does, its assumptions, and what the user may change.
- Imports — standard and third-party packages.
- Notebook controls — transient UI choices when interaction helps the analysis.
- Workbook input registry — one displayed
bf.inputs()cell. - Normalization/validation — convert workbook values into model-ready structures.
- Domain model — deterministic functions and calculations.
- Presentation — tables, charts, explanations, exception queues.
- 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 workbook | Put in the notebook |
|---|---|
| User-entered assumptions | Analytical/model logic |
| Source data already maintained in Excel | Data transformation that benefits from Python |
| Reviewable formulas and reconciliations | Statistics, simulation, optimization, specialized libraries |
| Familiar tables and reports | Reactive controls and custom visualizations |
| Final worksheet formulas | Published 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:
| Boundary | Current limit |
|---|---|
| Notebook source | 200,000 UTF-8 bytes |
| Named workbook inputs | 128 |
| Cells per input reference | 100,000 |
| Aggregate input cells | 250,000 |
| Complete input snapshot | 512 KiB encoded JSON |
| Published outputs | 128 |
| Published functions | 128 |
| Cells per published value/function result | 100,000 |
| Active worksheet function subscriptions | 512 |
| Concurrent Python function executions | 8 |
| Queued Python function executions | 128 |
| Function timeout | 60 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:
| Component | Current shipping build | How Boardflare owns it |
|---|---|---|
| marimo | 0.23.15 | Explicitly pinned by the notebook export pipeline |
| Boardflare notebook helper wheel | boardflare 0.4.1 | Explicit package version in the local runtime source |
| Pyodide used by the generated marimo worker | 314.0.0 | Resolved 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:
| Package | Current demo use |
|---|---|
| pandas | workbook table materialization, cleaning, reshaping |
| NumPy | vectorized modeling and simulation |
| SciPy | Sobol QMC, statistics, curve fitting, probability transforms |
| Matplotlib | charts 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
| Requirement | Recommended approach |
|---|---|
| Reactive workbook analysis and operator UI | Boardflare notebook |
| pandas/NumPy/SciPy work supported by Pyodide | Boardflare notebook |
| CORS-compatible browser API | Boardflare can be appropriate |
| Batch processing local folders | External Python |
| Reading/writing hundreds of independent files | External Python |
| Desktop application or Office automation | External Python / appropriate Office automation |
| Unsupported compiled/native package | External or managed Python |
| Scheduled/headless system job | External 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.