Skip to main content

Working with Excel

Excel and the notebook are complementary surfaces. Use workbook ranges and names as reactive notebook inputs, then publish only the results or reusable calculations that Excel actually needs to consume.

Read workbook data

Use bf.inputs() to declare the Excel data a notebook depends on. Boardflare watches those references and updates the notebook when the workbook changes.

Declare inputs once

import boardflare as bf

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

Then read the synchronized values in downstream cells:

sales = inputs["sales"]
tax_rate = inputs["tax_rate"]

Prefer one clear upstream input registry for a related analysis rather than scattering workbook references throughout many cells.

:::important Display the bf.inputs() result The displayed Anywidget model owns the live workbook connection. Do not hide it in an assignment-only cell or replace it with a private bridge API. :::

Reference cells and ranges

A plain reference string is enough when no options are required:

inputs = bf.inputs(
threshold="Controls!B4",
)
inputs

Use bf.ref() when you need options such as headers:

inputs = bf.inputs(
transactions=bf.ref("Data!A1:G500", headers=True),
)
inputs

Boardflare resolves supported workbook reference forms through the spreadsheet host, including A1 references, sheet-qualified ranges, and defined names. In multi-sheet workbooks, prefer sheet-qualified references so the dependency is obvious.

Understand returned values

Workbook referencePython value
One cellScalar
Multi-cell rangepandas DataFrame
Multi-cell range with headers=TrueDataFrame with the first row used as columns
Empty cellMay materialize as None

For example:

inputs = bf.inputs(
scenario="Control!B2",
orders=bf.ref("Orders!A1:E200", headers=True),
)
inputs

inputs["scenario"] is a scalar, while inputs["orders"] is a DataFrame.

Let reactivity do the refresh work

The notebook is reactive. When Excel changes a declared dependency, Boardflare refreshes that input and marimo reruns cells that depend on it.

You normally should not build manual “refresh all Python” logic. Structure cells around explicit variables and dependencies instead.

During initial startup, Boardflare waits for the first workbook snapshot before allowing dependent calculations to proceed. This prevents downstream cells from publishing plausible-looking results based on an uninitialized placeholder.

Inspect input errors

Use:

inputs.errors

After initialization, reading an input that failed to resolve raises instead of silently returning a placeholder value.

Common causes include:

  • a renamed or deleted worksheet;
  • a mistyped reference;
  • an invalid defined name;
  • a range shape that no longer matches the notebook's assumptions.

Name inputs for the analysis

Use Python names that describe the business role of the workbook data:

inputs = bf.inputs(
actuals=bf.ref("Actuals!A1:F50", headers=True),
assumptions=bf.ref("Assumptions!A1:B12", headers=True),
tolerance="Controls!B3",
)
inputs

That makes downstream code easier to read than repeating workbook coordinates throughout the notebook.

Return results to Excel

Notebook results can stay in the notebook. When Excel needs a selected result, publish it once and consume it with BF.OUTPUT().

Publish a completed value

Suppose an upstream cell produces a summary table:

summary = [
["Metric", "Value"],
["Revenue", revenue],
["Gross Margin", gross_margin],
]

Publish it:

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

Then use the result in Excel:

=BF.OUTPUT("summary")

The value spills into the worksheet when it is a supported array/table shape.

:::important Display the publication widget Keep the bf.publish() result displayed. Its Anywidget model owns the live output registry used by worksheet formulas. :::

Publish several results together

Treat bf.publish() as the notebook's explicit public registry:

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

Then worksheet consumers can select only what they need:

=BF.OUTPUT("forecast")
=BF.OUTPUT("exceptions")

Keeping the complete registry in one downstream cell makes it easy to inspect what the notebook exposes to Excel.

See which worksheet formulas consume a publication

A displayed publication also tracks active worksheet consumers. Expand the publication widget to see the worksheet reference, published name, and whether the consumer uses an output or function.

If you assigned the publication to a variable, inspect the same mapping from Python:

publication.consumers
publication.consumers["outputs"]
publication.consumers["functions"]

This is useful when changing or removing a published name because it shows which worksheet formulas are currently connected to that registry. In Excel, the references follow live streaming subscriptions and identify the formula anchor cell rather than an entire spill footprint.

Consumer tracking is runtime state. It is not stored as part of the notebook source and should not be treated as a complete static dependency audit for a closed workbook.

What BF.OUTPUT() is for

Use it when a notebook has already performed the expensive or multi-step work and the workbook needs the finished value for:

  • a report section;
  • a reconciliation table;
  • downstream worksheet formulas;
  • review or sign-off;
  • a dashboard or schedule maintained in Excel.

You do not need to publish every intermediate DataFrame or chart. Keep analysis detail in the notebook unless the workbook actually needs it.

Startup behavior

BF.OUTPUT() is a streaming Excel custom function. When the saved notebook is still starting, Excel can show its normal #BUSY! state. Boardflare waits for the notebook to establish the live publication registry rather than returning a temporary placeholder value.

If #BUSY! never resolves, open Troubleshooting and confirm that the saved notebook starts successfully and the displayed bf.publish() cell runs without error.

Output shapes

Published values are normalized to worksheet matrices:

  • a scalar returns one cell;
  • a one-dimensional sequence returns one spill row;
  • a rectangular two-dimensional sequence returns a spill range;
  • supported pandas and NumPy values are converted to worksheet-compatible values.

See Supported values and limits for the detailed conversion boundary.

Save the source that recreates the result

Published Python objects are live-session state. Saving the workbook stores notebook source, not the current in-memory output registry.

When the workbook opens again, Boardflare starts the saved notebook, reruns the reactive analysis, and recreates the publication registry. That is why workbook formulas can remain connected without serializing Python objects into the file.

Create Excel functions

Use BF.FUNCTION() when a calculation should have one centralized Python implementation while workbook users call it from ordinary worksheet cells.

Define the Python function

def discount(price, rate=0.0):
return float(price) * (1 - float(rate))

Publish it explicitly:

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

Then call it from Excel:

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

The function name is looked up in the explicit mapping passed to bf.publish(). Boardflare does not discover arbitrary notebook globals or use eval.

Publish values and functions together

A normal notebook often exposes both:

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

This keeps the notebook's Excel-facing contract visible in one place.

Keep expensive work in notebook cells

BF.FUNCTION() is best for relatively short calculations or lookups whose implementation should be centralized in Python.

For expensive modeling, simulation, large transformations, or slow API work:

  1. perform the expensive work in ordinary reactive notebook cells;
  2. publish the completed result with BF.OUTPUT(), or publish a lightweight function that looks up values from the already-computed model.

A long synchronous function can block the notebook kernel until it returns or reaches the worksheet timeout.

Supported function shapes

Common supported signatures include:

def required(a, b):
...

def optional(a, b=10):
...

def variable(a, *items):
...

async def async_lookup(code):
...

Worksheet calls are positional. Required keyword-only parameters and **kwargs are not part of the current worksheet function contract.

Range arguments

Worksheet arguments are converted before Python receives them:

  • scalar cell values remain scalars;
  • a one-cell range is unwrapped to a scalar;
  • a multi-cell range becomes a two-dimensional list;
  • omitted trailing optional arguments use Python defaults.

Return values follow the same worksheet conversion families as published outputs. See Supported values and limits.

Why this matters

Without a centralized implementation, a complex calculation can be repeated or re-expressed in many worksheet locations. BF.FUNCTION() lets the implementation live once in the notebook while Excel remains the place where workbook users invoke it.

That is useful for algorithms, valuation logic, scoring, normalization, specialized numerical calculations, and other logic that is awkward to maintain as a long worksheet formula.

Supported values and limits

Use this page when a workbook reference, published result, function call, or notebook save is near a current product boundary.

Workbook inputs

Workbook referencePython value
One cellScalar
Multi-cell rangepandas DataFrame
Multi-cell range with headers=TrueDataFrame with first row used as columns
Empty workbook cellMay materialize as None

Current input limits:

LimitValue
Named inputs128
Reference text512 characters
Cells per input reference100,000
Aggregate input cells250,000

Published outputs

BF.OUTPUT() normalizes supported values to a worksheet matrix:

  • scalar → one cell;
  • one-dimensional sequence → one spill row;
  • rectangular two-dimensional sequence → spill range.

Supported result families include finite numbers, strings, booleans, blanks, supported dates/times, pandas Series/DataFrames, NumPy scalars/arrays, rectangular Python sequences, and supported Excel rich-value dictionaries. A Python None, pandas missing value, or floating-point NaN in a result cell becomes a blank worksheet cell.

Unsupported or unsafe values—including positive/negative infinity, ragged matrices, invalid rich-value payloads, unsupported Python objects, unsafe integers, and oversized results—return deterministic worksheet errors.

LimitValue
Published outputs128
Cells per published value100,000

Published functions

Worksheet function calls are positional.

Supported callable patterns include required positional arguments, trailing defaults, optional *args, synchronous functions, asynchronous functions, and optional keyword-only parameters that have defaults.

Required keyword-only parameters and **kwargs are not supported by the current worksheet contract.

Argument conversion:

  • scalar worksheet values remain scalars;
  • a one-cell range becomes a scalar;
  • a multi-cell range becomes a two-dimensional list;
  • omitted trailing optional arguments use Python defaults.

Current function limits:

LimitValue
Published functions128
Cells per function result100,000
Cells across one call's arguments100,000
Active worksheet function subscriptions512
Concurrent Python function executions8
Queued Python function executions128
Function timeout60 seconds

Public names

Input, output, and function names are:

  • case-sensitive;
  • required to start with an ASCII letter;
  • limited afterward to ASCII letters, digits, and underscores;
  • limited to 128 characters.

Choose names that remain readable in formulas such as:

=BF.OUTPUT("forecast")

Notebook persistence

ItemLimit
Notebook source200,000 UTF-8 bytes
Complete notebook persistence record1,000,000 bytes

Browser/runtime compatibility

Boardflare's notebook executes in a browser-based Pyodide environment. Pure-Python packages are usually the easiest fit. Packages that depend on unsupported native binaries, unrestricted sockets, desktop APIs, or local file-system behavior may not work even when they work in desktop CPython.

See Packages and environment before designing a workflow around a specialized dependency.