Building Notebooks

Understand marimo reactivity, structure maintainable Excel-connected applications and analyses, use controls and charts, and work within the browser Python environment.

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 displayed 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

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.

Write notebook code with an AI assistant

An AI assistant such as ChatGPT, Claude, or Copilot can draft and revise notebook code. Provide it the canonical Boardflare Notebook Reference (or llms-full.txt), which documents marimo reactivity, the boardflare API, and pre-bundled packages. Give it the workbook ranges it should read and what it should publish back to Excel. Then review the code, paste it into the notebook, run it, and save it.

The assistant does not see your workbook or run the notebook. Review generated code as carefully as code you wrote yourself, including imports, data handling, and published results. The notebook, not the assistant, is the runtime, so the durable result is ordinary Python source with explicit workbook inputs and published outputs. In Excel, assistants running in Office can also inspect and edit notebook cells directly on the hidden _BOARDFLARE worksheet and signal the add-in to reload by incrementing cell H1 and reading the response JSON in H2 (see Workbook Editing: Code Sheet & AI Handshake).

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.

flowchart TB
    A[Excel assumptions / source tables] --> B[bf.inputs]
    B --> C[Validation + normalization]
    C --> D[Domain model]
    D --> E[Controls / charts / explanations]
    D --> F[bf.publish]
    F --> G[BF.OUTPUT]
    F --> H[BF.FUNCTION]
    G --> I[Excel reports / formulas]
    H --> I

Keep one upstream data boundary

Declare workbook dependencies together in one displayed bf.inputs() widget 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 for reactive workbook inputs.
  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 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. For exact system limits, timeouts, payload constraints, and cell thresholds, see System Limits and Constraints.

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 The Boardflare Python API for API 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;
  • a second user opening and using the workbook without author intervention when the notebook will be shared.

Use the Sales Scenario Analysis template as the introductory integration reference, then inspect the current templates for the published operations-optimization and scientific-modeling patterns.

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.

For the authoritative specification of the runtime environment, Python and marimo versions, and pre-bundled companion packages, see Python Notebook Environment.

For browser and network constraints (including Web Worker isolation, memory boundaries, and Content Security Policy), see System Limits and Constraints.

Choosing the right execution environment

Requirement Recommended approach
Reactive workbook analysis and operator UI Boardflare notebook
pandas/NumPy/SciPy work, or packages on the published list Boardflare notebook
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.