bilig

Stop driving spreadsheets with screenshots. Run formula workbooks in Node.

Research refreshed: 2026-06-03.

Spreadsheets are still where teams keep a lot of operational logic: pricing rules, revenue models, quote approvals, capacity plans, billing checks, and import validation. The problem is not that spreadsheets exist. The problem is that automation often treats the grid as pixels instead of as state.

If a backend job or coding agent has to click cells, infer formulas from a rendered view, and trust a screenshot after the edit, the verification boundary is weak. The automation can look right while still failing to prove which input changed, whether a dependent formula recalculated, whether the workbook state persisted, or whether a later restore returns the same computed value.

@bilig/workpaper is built around the smaller claim: keep spreadsheet-shaped business logic in a workbook model, but run it through a TypeScript API, MCP tool server, or agent tool in Node services and coding-agent workflows.

The Failure Mode

Screenshot-driven spreadsheet automation is brittle because the visible grid is not the whole workbook contract.

A serious workflow usually needs to answer these questions:

Those are state questions. A screenshot can help a human inspect the final shape, but it should not be the only proof that a calculation is correct.

The API Boundary

A WorkPaper gives code explicit operations:

That makes the workbook a reviewable calculation artifact instead of a browser grid that an automation script has to push around.

Run The No-Key Proof First

For an agent or MCP client, start with the maintained evaluator:

npm exec --yes --package @bilig/workpaper@latest -- bilig-evaluate --door agent-mcp --json

A useful result includes:

{
  "schemaVersion": "bilig-evaluator.v1",
  "door": "agent-mcp",
  "editedCell": "Inputs!B3",
  "before": 60000,
  "after": 96000,
  "restoredMatchesAfter": true,
  "verified": true
}

The exact cells and serialized byte count can change between releases. The important part is the shape: tool discovery, input edit, dependent formula readback, exported or persisted WorkPaper state, restore or restart readback, and verified: true.

For a direct Node service instead of MCP, use:

npm exec --yes --package @bilig/workpaper@latest -- bilig-evaluate --door workpaper-service --json

Minimal TypeScript

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

const workbook = WorkPaper.buildFromSheets({
  Inputs: [
    ["Metric", "Value"],
    ["Seats", 25],
    ["Price", 147],
  ],
  Summary: [
    ["Metric", "Value"],
    ["Total", "=Inputs!B2*Inputs!B3"],
  ],
});

const inputs = workbook.getSheetId("Inputs");
const summary = workbook.getSheetId("Summary");
if (inputs === undefined || summary === undefined) {
  throw new Error("Workbook did not create the expected sheets");
}

const before = readNumber(workbook.getCellValue({ sheet: summary, row: 1, col: 1 }));
workbook.setCellContents({ sheet: inputs, row: 1, col: 1 }, 40);
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("Restored workbook did not create the 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;
  }
  if (typeof cell === "number") {
    return cell;
  }
  throw new Error(`Expected numeric cell value, got ${JSON.stringify(cell)}`);
}

Expected output:

{
  "before": 3675,
  "after": 5880,
  "afterRestore": 5880,
  "verified": true
}

Agent Host Boundary

MCP is a tool and context boundary, not a spreadsheet screen. The agent host can make WorkPaper tools available, but the proof still has to come from the returned workbook state.

Use the boundary that matches the host:

The useful agent result is not “clicked”, “typed”, or “cell updated”. It is a small object with editedCell, before, after, afterRestore, persistedDocumentBytes, verified, and limitations.

Where This Fits

Use a WorkPaper when the code owns the workflow:

Keep Google Sheets or Excel when the primary job is human collaboration, desktop workbook authoring, or full XLSX compatibility. @bilig/workpaper is a formula-backed runtime boundary, not a finished Excel clone.

Source Checks

Useful Next Pages

If this solves a workflow you have, the most useful signal is a star on the repository or a concrete issue with the workbook shape you need:

https://github.com/proompteng/bilig