Building A Revenue Model With Headless WorkPaper
Status: public adoption article for @bilig/headless
Most spreadsheet automation demos stop at “generate a formula.” A service or agent needs more than that. It needs to load workbook state, edit inputs, recalculate formulas, verify outputs, and persist the document for the next turn.
This article uses the runnable
examples/headless-workpaper/revenue-scenarios.ts
example to show that loop with @bilig/headless.
The Model
The example builds three sheets:
Pipeline: segment-level leads, conversion rates, ARPA, churn, and net MRRSummary: total net MRR, annual run rate, enterprise net MRR, and expansion targetScenarios: conservative, expansion, and stretch projections
The key point is that the formulas stay in the workbook:
const workbook = WorkPaper.buildFromSheets({
Pipeline: [
['Segment', 'Leads', 'Conversion Rate', 'Customers', 'ARPA', 'Gross MRR', 'Churn Rate', 'Net MRR'],
['Enterprise', 80, 0.18, '=B2*C2', 4200, '=D2*E2', 0.05, '=F2*(1-G2)'],
['Mid-Market', 220, 0.22, '=B3*C3', 1100, '=D3*E3', 0.08, '=F3*(1-G3)'],
['SMB', 900, 0.09, '=B4*C4', 180, '=D4*E4', 0.12, '=F4*(1-G4)'],
],
Summary: [
['Metric', 'Value'],
['Total net MRR', '=SUM(Pipeline!H2:H4)'],
['Annual run rate', '=B2*12'],
['Enterprise net MRR', '=SUMIF(Pipeline!A2:A4,"Enterprise",Pipeline!H2:H4)'],
['Expansion target', '=B3*1.18'],
],
})
That means the model can be changed by code without turning the spreadsheet into a pile of copied numbers.
The Agent-Style Edit
The example applies a focused planning change in a single batch:
workbook.batch(() => {
workbook.setCellContents({ sheet: pipelineSheet, row: 1, col: 1 }, 92)
workbook.setCellContents({ sheet: pipelineSheet, row: 2, col: 2 }, 0.26)
})
The first edit changes enterprise leads from 80 to 92. The second changes
mid-market conversion from 0.22 to 0.26.
After that, formulas recalculate through the model:
- total net MRR moves from
119267.2to136791.2 - annual run rate moves from
1431206.4to1641494.4 - enterprise net MRR moves from
57456to66074.4 - stretch projected net MRR moves from
161010.72to184668.12
Those are not hand-copied outputs. The script reads them back from cells and fails if the values drift.
The Persistence Loop
The example also serializes and restores the workbook before final readback:
const serialized = serializeWorkPaperDocument(exportWorkPaperDocument(workbook, { includeConfig: true }))
const restored = createWorkPaperFromDocument(parseWorkPaperDocument(serialized))
That is the operational pattern for agents and services:
- load workbook state
- apply a narrow edit
- recalculate
- persist the workbook document
- restore it later and verify the same formulas
Run It
From the repository:
pnpm --dir examples/headless-workpaper install --ignore-workspace
pnpm --dir examples/headless-workpaper run scenarios
The expected output includes both the pre-edit and post-edit model:
{
"beforeEdit": {
"totalNetMrr": 119267.2,
"annualRunRate": 1431206.4,
"enterpriseNetMrr": 57456,
"expansionTarget": 1688823.55
},
"afterEdit": {
"totalNetMrr": 136791.2,
"annualRunRate": 1641494.4,
"enterpriseNetMrr": 66074.4,
"expansionTarget": 1936963.39
}
}
The repository smoke test also runs this scenario against packed local runtime packages:
pnpm workpaper:smoke:external
Why This Matters
Spreadsheet-backed products often need the spreadsheet model without the browser grid:
- a backend service calculates workbook-backed business logic
- an agent edits inputs and needs deterministic readback
- a workflow stores a formula-backed document between turns
- a test runner checks that a workbook change has the expected downstream effect
@bilig/headless is meant for that boundary. It does not claim to be a complete
Excel clone; it gives code a typed workbook surface with formulas, structural
edits, persistence, and benchmark evidence in the same TypeScript package.