This page is for people building coding agents or backend workers that need a spreadsheet engine but do not want to drive a spreadsheet UI.
@bilig/headless lets the agent expose workbook operations as ordinary Node
tools: read a range, set a cell, recalculate formulas, verify the result, and
serialize the workbook for persistence. The important part is the loop, not the
agent framework around it.
Run the maintained example from a clean checkout:
cd examples/headless-workpaper
npm install
npm run agent:tool-call
The example builds a workbook with Inputs and Summary sheets, then executes
one agent-style tool call:
{
"toolName": "setInputCell",
"arguments": {
"sheetName": "Inputs",
"address": "B3",
"value": 0.4,
"reason": "Use the latest qualified pipeline conversion estimate."
}
}
The tool result reports the edited cell, before and after formula outputs, restored workbook outputs, and persistence checks. A passing run proves that:
Inputs!B3Expected ARR moved from 60000 to 96000Target gap moved from -34000 to 5600That is the contract an agent needs. It should not have to infer formula success from a screenshot.
Keep the first tool surface small:
readRange(range) returns computed values and serialized cell contents.setInputCell({ sheetName, address, value, reason }) validates the sheet and
A1 address before writing.This shape works with OpenAI tool calls, local coding-agent tools, queue workers, and normal service endpoints because the WorkPaper API is the boundary. The agent SDK can change without rewriting the workbook logic.
examples/headless-workpaper/agent-tool-call-loop.mjsscripts/workpaper-external-smoke.tspackages/headless/README.mddocs/agent-workpaper-tool-calling-recipe.mdUse this pattern when a product asks an agent to edit a forecast, refresh a scenario, check a formula-backed model, or generate a workbook artifact from service data. Keep the model responsible for choosing the next action, and keep the WorkPaper tool responsible for addresses, formulas, recalculation, and persistence proof.
If the workflow needs HTTP boundaries instead of direct function calls, start
with the Node service recipe. If the input
is JSON records rather than arrays, use the
json-records-input.mjs
example.