The spreadsheet framework built for agents.
Describe the model you need in natural language — your coding agent writes the source. open-sheet owns the cell addressing, the formula references, the recalculation and the export. No A1 address is ever authored by hand.
Agents write excellent analysis and terrible spreadsheets.
The reason is specific: =SUM(B2:B13). Cell addresses are the one thing an agent cannot hold onto. It miscounts the header row. It forgets the total moved when the data grew. It writes a reference to B7, inserts a quarter above it, and now every formula in the file is quietly wrong. Not crashed. Wrong, which is worse.
So open-sheet takes the addresses away. Add a quarter to the data and press F3 — the column definitions below it do not change, and every address in the file moves anyway.
const quarters = [
{ quarter: 'Q1', revenue: 12_400_000, cogs: 5_100_000 },
{ quarter: 'Q2', revenue: 13_900_000, cogs: 5_600_000 },
{ quarter: 'Q3', revenue: 15_200_000, cogs: 6_050_000 },
{ quarter: 'Q4', revenue: 18_650_000, cogs: 7_100_000 },
]
const quarters = [
{ quarter: 'Q1', revenue: 12_400_000, cogs: 5_100_000 },
{ quarter: 'Q2', revenue: 13_900_000, cogs: 5_600_000 },
{ quarter: 'Q3', revenue: 15_200_000, cogs: 6_050_000 },
{ quarter: 'Q4', revenue: 18_650_000, cogs: 7_100_000 },
{ quarter: 'Q5', revenue: 21_300_000, cogs: 7_900_000 },
]
col('quarter', { header: 'Quarter' }),
col('revenue', {
header: 'Revenue',
format: 'currency',
bar: { color: '#0000aa' },
}),
col('cogs', { header: 'COGS', format: 'currency' }),
col('grossProfit', {
header: 'Gross profit',
format: 'currency',
formula: (r) =>
sub(r.cell('revenue'), r.cell('cogs')),
}),
col('margin', {
header: 'Margin',
format: 'percent',
scale: ['#aa0000', '#000000', '#00aa00'],
formula: (r) =>
div(
sub(r.cell('revenue'), r.cell('cogs')),
r.cell('revenue'),
),
}),
col('qoq', {
header: 'QoQ growth',
format: 'percent',
highlight: {
above: 0.15,
color: '#ffff55',
bold: true,
},
formula: (r) =>
r.isFirst
? null
: sub(
div(r.cell('revenue'), r.prev().cell('revenue')),
1,
),
}),
P&L
| Quarter | Revenue | COGS | Gross profit | Margin | QoQ growth |
| Q1 | 12,400,000 | 5,100,000 | 7,300,000 | 58.9% | |
| Q2 | 13,900,000 | 5,600,000 | 8,300,000 | 59.7% | 12.1% |
| Q3 | 15,200,000 | 6,050,000 | 9,150,000 | 60.2% | 9.4% |
| Q4 | 18,650,000 | 7,100,000 | 11,550,000 | 61.9% | 22.7% |
| Total | 60,150,000 | 23,850,000 | 36,300,000 |
P&L
| Quarter | Revenue | COGS | Gross profit | Margin | QoQ growth |
| Q1 | 12,400,000 | 5,100,000 | 7,300,000 | 58.9% | |
| Q2 | 13,900,000 | 5,600,000 | 8,300,000 | 59.7% | 12.1% |
| Q3 | 15,200,000 | 6,050,000 | 9,150,000 | 60.2% | 9.4% |
| Q4 | 18,650,000 | 7,100,000 | 11,550,000 | 61.9% | 22.7% |
| Q5 | 21,300,000 | 7,900,000 | 13,400,000 | 62.9% | 14.2% |
| Total | 81,450,000 | 31,750,000 | 49,700,000 |
| over four quarters | over five | |
|---|---|---|
| total revenue | B6 =SUM(B2:B5) | B7 =SUM(B2:B6) |
| last gross profit | D5 =B5-C5 | D6 =B6-C6 |
| last QoQ growth | F5 =B5/B4-1 | F6 =B6/B5-1 |
The total row moved down one and its range grew. The new quarter’s growth formula points at a row that did not exist a moment ago. Nobody edited a formula.
Gross profit is =B2-C2 in the file, not 7,300,000. Change a number and the whole sheet recalculates — in Excel, in Sheets, in LibreOffice.
Six workbooks the framework builds for itself.
These are the dogfood workbooks in the repo, compiled by the version above and exported here. 171 live formulas across 6 files, and not one cell address among them was written by a person. Open any of them and change a number.
-
Departmental budget vs actual
↓ dept-budget-variance.xlsx · 9.1KBudget against actual by department, with variance flagged.
-
SaaS KPI dashboard
↓ kpi-dashboard.xlsx · 11.3KCohort retention, a conversion funnel and month-over-month movement.
-
Fixed asset register
↓ asset-register.xlsx · 9.1KStraight-line depreciation over a fixed asset register.
-
FY26 Budget
↓ fy26-budget.xlsx · 8.7KA quarterly P&L driven by named assumptions on their own sheet.
-
請款單 Invoice
↓ tw-invoice.xlsx · 8.8K請款單 — the invoice that follows it.
-
報價單 Quotation
↓ tw-quotation.xlsx · 8.8K報價單 — a Taiwanese quotation, tax and terms included.
Every count on this page came out of a compilation while the page was being built. There is no figure here that somebody typed and might forget to update.
Where did this number come from?
The question every spreadsheet you inherit refuses to answer. Click a cell in the viewer and open the inspector: it names the line of source that produced the cell, the formula that reached the file, and whether the number is something you can change or something the model works out.
- value
- 12,400,000
- formula
- — stored, not computed
- source
- sheets/fy26-budget/index.tsx:39
12_400_000 - editable
- yes — type a new number here
- value
- 2,480,000
- formula
- =F6*(1-taxRate)
- source
- — produced by a column rule
- editable
- no
column "netIncome" is computed, not stored — edit the inputs or the formula
The left cell is an input, so the inspector offers to edit it — and writes the new value back into the data array in the source, not into the grid.
The right one is derived. Note what its formula says: not $B$2 but taxRate, an Excel defined name, so the recipient reads intent rather than coordinates.
Isn’t that what Claude for Excel does?
Different tool, different job. Claude for Excel makes one person faster at making a spreadsheet. open-sheet makes the spreadsheet not need making a second time.
| Claude for Excel | open-sheet | |
|---|---|---|
| Input | a workbook that already exists | a source file |
| In the loop | a person, every time | a person once, at review |
| Reviewing a change | a binary diff | a code review |
| Doing it 500 times | 500 sessions | a loop |
| Needs | Excel, an account, a paid plan | Node. MIT. |
An assistant working inside a spreadsheet still writes =SUM(B2:B13) into a cell. That is a hand-authored address — authored by a model, but hand-authored. Insert a row and it is wrong, and nothing tells you. This is not a question of the model being clever enough: A1 is the only language that medium has.
And when open-sheet cannot compute a cell, it says so. #NOT_EVALUATED, never a plausible-looking number. Anything writing into a live cell produces something number-shaped whether or not it was sure — the failure that costs the most and shows the least.
Where Claude for Excel wins, and it is not close. It writes live formulas too; the difference is never “ours recalculate and theirs do not.” A workbook you did not generate, we cannot read at all. It computes with Excel’s own engine, so its numbers are Excel’s numbers by construction, where we earn that with a cross-engine check on every build. Five hundred-odd functions to our hundred-odd. Pivot tables.
Use that when you have a spreadsheet and want help with it. Use this when the spreadsheet is the output of a pipeline — a monthly board pack, five hundred invoices, a model regenerated whenever the data lands. The two compose, in that order.
What the recipient gets.
- a live model
- The .xlsx holds formulas, not baked numbers. Change an assumption and it recalculates in whatever they opened it with.
- validate
- A dropdown instead of a free-text field, carrying both the prompt and the refusal message. A rejection with no reason is worse than no validation.
- protect
- Unlocks the cells they are meant to fill in and leaves every formula shut. A derived cell is not an input.
- appendable
- Emits an Excel Table, so a row they add is taken into every range that reads it — and the derived columns fill themselves.
- note
- Attaches provenance to the cell itself. Shown on hover, not buried in a footnote nobody reads.
- Headers, footers, page numbers, and page breaks that name blocks rather than row numbers, so they move when the content does.
For your agent.
Scaffold
npx @open-sheet/cli init my-sheets writes a workspace carrying five skills — how to author, how blocks are placed, how formats work, what the recipient can do with the file, and how it prints.
Ask
/create-sheet and describe the model. The skill opens by confirming the material and refusing to invent numbers, which is the failure this medium punishes hardest.
Inspect
Click any cell in the viewer for its source line, its resolved formula and its computed value — or leave a comment for the agent to apply.
Automate
An MCP server over Streamable HTTP exposes the same operations the browser uses, so a stale write is refused with a 409 rather than silently overwriting somebody.
Checked against a real spreadsheet, every build.
Our evaluator and Excel have to agree about what a formula means, or the number in the viewer is a guess. Every whitelisted function is compiled into a fixture, exported without cached results, recalculated by LibreOffice, and diffed against what we computed. A function joins the whitelist only when a second engine agrees.
That check is also why SORT is implemented here rather than taken from a library: the library’s compares as strings, which is indistinguishable from correct until your numbers stop having the same number of digits.
pnpm test formula/verify
138 cases: 138 agree, 0 disagree, 0 not evaluated, 0 engine failed127 functions are whitelisted, and that line is printed by the run that produced this page — not remembered from a run that passed once. A disagreement fails the build; a function the engine cannot compute is reported as a gap, which is a different thing and must never read as agreement.