bilig
A workbook grid showing cell B2 changed to 32 and cell D5 recalculated to 51,300.

Package chooser

Install the package that matches the job.

Start with the narrow npm package for your problem. The full headless runtime is still available when you need lower-level APIs or agent metadata.

highest-traffic entry SheetJS or XLSX inputs changed and formulas are stale?

Start with the SheetJS-named package before moving to a full workbook runtime. The bridge example covers SheetJS, xlsx-populate, and ExcelJS with one workbook.

npx --package @bilig/sheetjs-formula-recalc sheetjs-recalc --demo --json

npx --package @bilig/xlsx-formula-recalc xlsx-recalc --demo --json

npx --package @bilig/sheetjs-formula-recalc sheetjs-recalc pricing.xlsx \
  --set Inputs!B2=48 \
  --read Summary!B7 \
  --out pricing.recalculated.xlsx \
  --json

When it fits

Stop translating workbook logic into one-off code.

Pricing rules, import checks, payouts, and budget models often start life in cells. If that shape is still the clearest source of truth, keep it and run it from Node.

01 / create

Build the workbook model.

Start from arrays, records, CSV-shaped data, or saved WorkPaper JSON.

02 / edit

Write the input cell.

Update a quantity, rate, assumption, imported value, or threshold.

03 / read

Read the calculated value.

Use the value the workbook produced after the input changed.

04 / save

Save the document.

Persist sheets, formulas, and config as JSON between runs.

Quickstart

Create a workbook in a blank Node project.

This installs the package, changes one input, reads the calculated total, saves JSON, and restores the workbook.

Blank Node project

terminal Node.js 22+
mkdir bilig-headless-eval
cd bilig-headless-eval
npm init -y
npm pkg set type=module
npm install @bilig/workpaper
npm install -D tsx typescript @types/node
curl -fsSLo quickstart.ts \
  https://proompteng.github.io/bilig/npm-eval.ts
npx tsx quickstart.ts

More TypeScript examples live in examples/headless-workpaper , examples/serverless-workpaper-api, and examples/xlsx-recalculation-node.

quickstart.ts TypeScript
import {
  WorkPaper,
  createWorkPaperFromDocument,
  exportWorkPaperDocument,
  parseWorkPaperDocument,
  serializeWorkPaperDocument,
} from "@bilig/workpaper";

const workbook = WorkPaper.buildFromSheets({
  Inputs: [
    ["Metric", "Value"],
    ["Customers", 20],
    ["Average revenue", 1200],
  ],
  Summary: [
    ["Metric", "Value"],
    ["Revenue", "=Inputs!B2*Inputs!B3"],
  ],
});

const inputs = workbook.getSheetId("Inputs");
const summary = workbook.getSheetId("Summary");
if (inputs === undefined || summary === undefined) {
  throw new Error("Missing sheet");
}

const before = readNumber(workbook.getCellValue({ sheet: summary, row: 1, col: 1 }));
workbook.setCellContents({ sheet: inputs, row: 1, col: 1 }, 32);
const after = readNumber(workbook.getCellValue({ sheet: summary, row: 1, col: 1 }));

const saved = serializeWorkPaperDocument(exportWorkPaperDocument(workbook, { includeConfig: true }));
const restored = createWorkPaperFromDocument(parseWorkPaperDocument(saved));
const restoredSummary = restored.getSheetId("Summary");
if (restoredSummary === undefined) {
  throw new Error("Missing restored Summary sheet");
}

const afterRestore = readNumber(restored.getCellValue({ sheet: restoredSummary, row: 1, col: 1 }));
console.log({ before, after, afterRestore, verified: after === afterRestore });

function readNumber(cell: unknown): number {
  if (typeof cell === "object" && cell !== null && typeof (cell as { value: unknown }).value === "number") {
    return (cell as { value: number }).value;
  }
  throw new Error(`Expected numeric cell value, got ${JSON.stringify(cell)}`);
}

Where it runs

Put the workbook behind the code that owns the workflow.

Route handler, queue worker, CLI, or MCP server: load the workbook, edit cells, read results, and save state.

bilig-workpaper-mcp stdio
$ npm exec --package @bilig/workpaper@0.40.43 -- bilig-workpaper-mcp --workpaper ./pricing.workpaper.json --init-demo-workpaper --writable
ok tools/list
ok tools/call read_range
ok tools/call set_cell_contents
ok WorkPaper JSON persisted to file

$ curl https://bilig.proompteng.ai/mcp -H 'accept: application/json, text/event-stream' -H 'mcp-protocol-version: 2025-11-25' ...
ok stateless Streamable HTTP tools/list

Benchmark

The benchmark is public. So is the caveat.

The checked JSON has bilig ahead on the aggregate mean and p95 scorecard, with holdouts visible. The worst p95 row is named beside the number.

workpaper-vs-hyperformula.json current run
comparable mean rows 94/100

94 of 100 comparable mean-latency rows are faster in the checked file. structural-move-rows is the current worst p95 row: 4.047x. Browser UI rendering is not part of this benchmark.

Command
pnpm workpaper:bench:competitive:check
Artifact
packages/benchmarks/baselines/workpaper-vs-hyperformula.json
Worst p95
structural-move-rows is the current worst p95 row: 4.047x.
Out of scope
UI rendering, Excel file compatibility, and workbook shapes this suite does not cover.

Start here

Pick the path that matches the job.

Try the package, put it behind a service route, expose it as an agent tool, or compare it with the spreadsheet stack you already use.

Reference

Short paths for real integration work.

Open the install path, service recipe, agent adapter, or comparison page that matches the thing you are building.

Decide

Run it on one calculation you care about.

If the quickstart matches your workflow, star the repo. If it does not, name the blocker.