gridpath
[npm]
[license]
[node]
Real Excel files, in your web app and in your agent. Open the .xlsx
your users already have, keep every formula live at full size, let AI edit
it with review, and save back the same file.
Both run the same engine (a fork of IronCalc):
real formulas evaluated in process, and saves that patch the original file so
untouched parts come back byte-identical. Recalculated formulas and supported
chart data caches carry current values; chart definitions remain native Excel XML.
In your web app
Quick start
React and React DOM are peer dependencies. Your app doesn't have to use
React: createSpreadsheet mounts the spreadsheet into any element.
Without React:
createSpreadsheet resolves once the workbook is open. If it can't open it
rejects with a GridPathError and leaves the element empty.
Next.js
No configuration is needed. This was tested on Next.js 16 with React 19,
with Turbopack and with webpack, in next dev and in production builds.
- Render the spreadsheet from a client component (
"use client"). The page
around it can stay a server component.
gridpath/agent works in a route handler.
examples/nextjs-demo
is a complete app. Each user has a list of workbooks saved to the app's
own API: app/api/books/[[...path]]/route.ts holds the REST routes
httpStore calls, with auth. Each workbook has its own URL. The example
shows:
- uploading and starting new workbooks;
- a loading indicator;
- what to do for each error code;
- switching workbooks, which saves the one being left.
What users get without any code
-
A toolbar for formatting:
- undo and redo;
- font and size;
- bold, italic and underline;
- font and fill colour;
- alignment and wrap;
- currency, percent, comma, and more or fewer decimals;
- number formats;
- inserting and deleting rows and columns;
- sort, filter, freeze panes and clear.
Keyboard shortcuts (⌘B, ⌘Z, ⌘S…) work in the grid. toolbar={false}
hides the toolbar.
-
A save status that says where things are saved: "Saved in this
browser", "Saved to your account", "Saving…", "Unsaved changes", or
"Couldn't save · Retry".
- Edits are saved two seconds after they pause. ⌘S or the Save button
saves at once.
-
Reopening where they left off: after a reload, the workbook comes
back and so does the cell the user was on.
-
A File menu with Open…, New and Recent workbooks. fileMenu={false}
removes those entries when your app has its own list.
-
Version history: restore an earlier version, or download it.
Restoring keeps the state it replaces as a version too.
-
Download as .xlsx, a separate button from Save.
-
An "AI changes" panel when an agent works through sheet.ai. Each
change shows what it did ("D2: empty → =C2/B2-1") and where, with Accept
and Reject buttons. Clicking a change shows its cells.
reviewPanel={false} turns the panel off if your app has its own
review UI.
-
Plain explanations in place of the grid when a file can't be opened,
for example "an old-format .xls workbook or a password-protected .xlsx".
-
A count of formulas showing Excel's saved value in the toolbar, with
a list of them.
-
Its own question before anything is lost: opening another workbook
over changes that can't be saved asks "Discard unsaved changes?".
Workbooks and documents
A spreadsheet shows one workbook at a time. You identify a workbook by its
documentId, which defaults to fileName. Together with a
store, the ID is how a workbook is loaded:
The spreadsheet opens the stored state for that ID. If you also pass a
file, the stored state wins only when it was saved from that same file;
otherwise file opens. With nothing stored and no file, the workbook
starts blank.
Switching workbooks can be done in two ways:
- Remount per document. Give the component
key={book.id}, as the
Next.js example does with a page per workbook.
- Call
open() or openStored() on the same spreadsheet. Both
follow the same rules:
The File menu follows the same rules and asks the user before it discards
anything.
Cleaning up.
- Unmount the component, or call
destroy() (createSpreadsheet
only). If there are edits that aren't saved yet, they are saved first.
This covers a client-side navigation, which fires no beforeunload.
Then the engine's worker stops. From then on, every call on the old
sheet object fails with not-ready.
- Closing the page while edits are unsaved prompts the browser's
"Leave site?" warning.
CSV and text files
file, open() and the File menu also take CSV and other delimited text.
It opens as a new workbook, which saves as .xlsx: sales.csv becomes
sales.xlsx, with one sheet named sales, and the toolbar says so.
- Delimiters: comma, tab, semicolon or pipe, whichever the first lines
agree on. Excel's
sep=; first line is honoured. In semicolon files a
comma is the decimal point (1.234,5 is 1234.5).
- Encodings: UTF-8 (with or without a byte-order mark), UTF-16 with a
mark (Excel's "Unicode Text"), otherwise Windows-1252.
- Values are read as if typed into the cell: numbers, currency,
percentages, ISO and US dates,
TRUE/FALSE, and = formulas, which
calculate. A formula that doesn't parse stays text.
- Kept as text, where Excel would lose them: digits with leading zeros
(ZIP codes, IDs) and digit strings longer than 15 (card and account
numbers).
- Columns are widened to fit their text, up to 60 characters.
Text that isn't a table (.json, .xml, .html, .md…) is refused with
unsupported-file. A 17 MB CSV (2 million cells) opens in 7 to 10 seconds
in Chromium on a recent Mac.
Saving
Saving happens in three ways:
- Autosave: two seconds after edits pause. Change the delay with
autosave (milliseconds), or turn it off with false.
- ⌘S and the Save button.
sheet.saveNow() from your code.
Each save writes the workbook to the store and calls onSave. Watch it
with onStatusChange or getStatus():
save() and saveBlob() return the bytes without touching the store,
for an upload of your own.
Saving to your backend
By default, everything is kept in the user's browser (IndexedDB) by
browserStore(). Saves within five minutes of each other share one
version. The newest 20 versions are kept as they are, then one per day for
30 days. Tune this with browserStore({ checkpointMinutes, keepRecent, keepDays }).
To save to the signed-in user's account, point httpStore at your API:
The routes it calls, all relative to url. Ids are URL-encoded:
When the backend can't be reached or answers with an error, the user's
edits are not lost:
- The status reads "Saved in this browser only · Retry", with
keptLocally: true.
- Autosave retries after 2, 5, 15 and 30 seconds, then every minute.
- A copy of the edits waits in this browser.
- If the page is closed first, the copy is reopened next time, newer than
the backend's, and saved there. The status line says "Restored changes
that weren't saved to your account".
If the backend can't be reached when a workbook opens, the spreadsheet
shows the error with Try again. It does not open a blank workbook that
autosave would then write over the user's saved one.
Anything else can be a store. httpStore is one implementation of this
interface:
There are two simpler options:
onSave: receive the bytes on every save and upload them yourself.
A rejected promise counts as a failed save and is retried.
store={false}: no saving at all; users keep Download, and your code
keeps save().
Errors
Everything the spreadsheet reports is a GridPathError. Each one has:
code, for your code to act on;
message, which says what happened;
hint, a sentence you can show that says what to do.
Errors reach you in four ways:
- Methods reject with them. For example,
open(), openStored(),
save() and getCell().
onError receives engine failures and workbooks that couldn't be
opened: the first one, or one opened from the File menu.
onStatusChange reports a failed save as { state: "error", code, message, hint }. Failed saves don't go to onError, because autosave
retries them.
- The spreadsheet itself shows the problem in place of the grid, so
users are never left with an empty box.
Formulas GridPath can't calculate
Some formulas don't calculate in GridPath:
- an Excel add-in function (
_xll.…);
- a function GridPath doesn't support yet;
- a link to another workbook.
These cells keep the value Excel saved in the file, and the formulas that
read them calculate from that value. The user sees a count in the toolbar
and a list that explains each cell. Your code can read the same thing:
Options
Methods
AI: your model, your key
sheet.ai is GridPath's agent tool set (the same tools and result shapes as
the MCP server) acting on the grid the user is
looking at. Give the tools to your model, run its tool calls with
sheet.ai.run(), and let the user review: every write lands at once,
highlighted, as a pending batch they accept or reject in the "AI changes"
panel (or your own UI).
GridPath never sees a key. The model call happens on your server; the tool
calls run in the browser, where the workbook is.
Server (any Node route; this one is the demo's server/agent.ts). The
system prompt and tools are fixed here; taking them from the request would
make the route a general proxy for your key. Add your own auth and rate
limits.
Browser (the demo's src/agentLoop.ts, shortened):
Already have an agent framework? Use sheet.ai.tools() (Anthropic format)
or sheet.ai.tools("openai") and route tool calls to sheet.ai.run().
Included: describe_workbook, find_rows, query_rows, read_range, set_cell,
set_range, copy_range, set_format, clear_range, run_script (sandboxed in a
worker), row/column/sheet structure, freeze, hide, merge, notes, sizes,
define_name. Not included: tools that need the desktop app or the network
(fetch_web, edgar_lookup, reference workbooks).
save() refuses while changes are pending: it throws a
PendingChangesError (with a pending count) until the user accepts or
rejects them, so nothing unreviewed reaches the file by accident.
save({ includePending: true }) saves them anyway.
How saving works
Every save replays the session's changes (cells, formats, sizes, rows and
columns, sheets, filters) onto the bytes the workbook was opened from. Parts
the session did not touch are copied through unchanged. Formulas carry
their results: every new formula, and every formula the session's edits
recalculated, is saved with its current result as the cached value (the
formula text is left as it was), so a reader that never calculates (pandas,
openpyxl with data_only=True, file previews) sees current numbers. Formulas
the session did not affect keep the values Excel saved. The file is also
marked for recalculation on open, which Excel honours.
Charts
The browser renders imported standard line charts at their worksheet anchors,
using a separately loaded Apache ECharts SVG renderer. Source values come from
the calculation engine, including cells outside the viewport; edits and undo
update the chart after recalculation. await sheet.getCharts() returns the
display descriptors and current values. sheet.getLastEditTimings() reports
the most recent edit's calculation, serialization and transfer/apply timings.
This first version supports direct one-dimensional worksheet references up to
10,000 points per series, basic titles, legends, RGB line colors and numeric
axes. It approximates Excel's visual styling. Other chart types, named/external
data references, stacked/smoothed lines, date/log/secondary axes, and chart
creation or editing controls are not supported. Unsupported charts show a
placeholder and retain their native definitions on save.
Browser and headless saves refresh affected direct-reference numeric/string
chart caches from the saved worksheet values. Source references, styling and
unrelated chart XML stay intact. Unsupported references retain their original
caches; Excel's recalculation flag remains set.
Compared with other embeddable spreadsheets
Against the spreadsheet components a web app would otherwise embed, on real
models in Chromium (workbooks, method and full results):
Bundlers
The package loads its worker and engine with
new URL("…", import.meta.url), which Next.js, Vite, webpack 5 and Rollup
bundle on their own.
-
Next.js: no configuration (see Next.js).
-
Vite: production builds work as is. For the dev server, exclude the
package from dependency pre-bundling:
-
webpack 5 (outside Next.js): no configuration; new URL workers and
assets are built in.
-
Anything else: copy engine.worker.js, its chunks and
gridpath_engine_bg.wasm from dist/ to your static files and pass
engine: { workerUrl, wasmUrl }.
Size in the browser: about 120 KB of JavaScript and a 1.3 MB engine, gzipped
(0.9 MB with brotli). The engine downloads once and is cached by the browser.
Build from source
examples/embed-demo installs the packed tarball the way an app would.
In your coding agent (MCP)
Let your agent edit Excel files. Review before anything is saved.
An MCP server for Claude Code, Claude Desktop, Cursor, Codex or any MCP host
that edits the .xlsx / .xlsm files you already have, with a review step:
- Review before save. Every write returns a
review_url: a local page with
the workbook, changed cells highlighted, before/after per cell, Accept /
Reject per batch or per cell, Save or Save as… (a copy). With --review-required, only
that page can write the file; the agent just hands you the link.
- Leaves your Excel file intact. No re-export through a library, no
conversion into another format. Saves patch the original bytes: only the
parts an edit touched are rewritten. Untouched parts, including pivots,
VBA macros, add-in data and custom XML, come back byte-identical.
Referenced chart caches update with recalculated worksheet values.
- Real formulas, in process. A calculation engine (a fork of
IronCalc, compiled to WebAssembly)
runs inside the server. The agent reads evaluated values and writes formulas
that work when the file opens in Excel; inserts and deletes shift references.
- Runs anywhere Node runs. No Python, no openpyxl, no LibreOffice, no Excel
install. macOS, Windows, Linux.
- Sandboxed scripts.
run_script runs the agent's JavaScript against the
workbook in a worker with a hard timeout and lands the result as one
reviewable batch.
[GridPath review page: workbook with a changed cell highlighted and a panel of pending change batches]
Install
Claude Desktop (no terminal)
Download gridpath-0.5.2.mcpb
and open it. Claude Desktop shows an install dialog where you pick the folders
Claude may edit and whether saving requires review. Node is bundled; nothing else
to install. Claude Desktop shows its standard "not verified" warning for any
extension installed from a file; the source is public
and nothing is written until you accept and save on the review page.
Claude Code
Cursor
Add to Cursor
(one click), or add to mcp.json:
Codex (~/.codex/config.toml)
Gemini CLI, Windsurf, VS Code, anything else
Same command/args as the Cursor JSON in that host's MCP settings. The server
speaks stdio.
Options
Pending changes live next to the workbook (.gridpath/<file>.batches.json) until
saved or rejected, so nothing is lost if the agent's session ends.
Tools
Every tool takes path. Cells and ranges are A1 notation.
Writes never touch the file. They accumulate as pending batches and every write
returns a readback of what landed (values, formulas, a row_map of labels) plus
the review_url.
What the agent said
Claude, after editing a 1,400-row sell-side model (2,500+ named ranges, hidden
sheets) through GridPath in Cursor, asked how it compared with its built-in
approach:
It recalculates live. After I changed row 58, I could read back the new
growth rates right away (28.0%, 28.0%, 27.9%) and confirm the change worked.
My usual approach, a Python library called openpyxl, doesn't calculate
formulas. I'd have to run the file through LibreOffice to get values, and
that can differ from Excel on some functions.
It finds things fast. Its row search and workbook map located "Product
revenue" and the Assumptions structure in a few calls. Otherwise I'd be
scanning a 1,400-row sheet cell by cell.
It leaves the rest of the file alone. It rewrites only the cells that
changed. When openpyxl opens and re-saves a heavy model like this one, it can
quietly drop or damage charts, data validation, pivot caches or some
formatting. That's the biggest risk with my built-in approach.
How it compares
Benchmarks and methodology, including runs where a plain coding agent does as
well as we do, are in the gridpath-evals repo.
Supported files
.xlsx and .xlsm. Verified on real analyst models: a 1.4 MB, 12-sheet,
55,000-formula workbook (76 package parts, 75 byte-identical after an edit) and a
13-sheet .xlsm bank model whose vbaProject.bin came back byte-identical.
How it works
The engine parses the .xlsx into an in-process model; edits mutate it and
read back evaluated results, recalculating only what an edit affects. Saving
diffs the session's changes against the file as loaded and patches the
original bytes at the zip-part level: untouched parts are copied through,
nothing regenerates the package. In the browser the engine runs in a Web
Worker and the grid draws only the visible window, so large models stay light.
Headless (Node)
gridpath/headless is the engine without the grid: open a workbook, apply
the same tool calls, read results, and save, from a server or a script.
Status
v0.5. Not yet supported:
- In the grid: chart types other than line charts, creating or editing
charts, data-validation dropdowns, real-time collaboration.
- For agents: the
fetch_web and edgar_lookup sourcing tools, and
edits to charts or pivots.
GridPath is also a desktop app with the same engine
and review and its own agent built in.
License
Apache-2.0 (LICENSE, NOTICE); third-party software in the
package is listed in THIRD_PARTY_NOTICES.md. Releases
before 0.2.0 were FSL-1.1-Apache-2.0. "GridPath" is a trademark of Heelix
Technologies Inc.