Use this when an OpenAI Responses agent needs to change workbook inputs and then explain the number the workbook calculated.
The model should not write workbook JSON. Give it two small function tools:
read a bounded range, and set one validated input cell. Your Node process runs
those tools against @bilig/headless, returns function_call_output items, and
asks the model to answer from the computed readback.
Run the dependency-light example from a checkout:
git clone https://github.com/proompteng/bilig.git
cd bilig
pnpm --dir examples/headless-workpaper install --ignore-workspace
pnpm --dir examples/headless-workpaper run agent:openai-responses
The script does not call the OpenAI API. It gives you the application-side code you run between Responses API turns:
read_workpaper_summary and set_workpaper_input_cell function
tools.function_call items from the model.zod.function_call_output items.Expected proof:
{
"apiShape": "OpenAI Responses function_call -> function_call_output",
"toolNames": ["read_workpaper_summary", "set_workpaper_input_cell"],
"followupInputTypes": ["user", "function_call", "function_call", "function_call_output", "function_call_output"],
"writeResult": {
"editedCell": "Inputs!B3",
"before": {
"expectedArr": 60000,
"targetGap": -34000
},
"after": {
"expectedArr": 96000,
"targetGap": 5600
},
"checks": {
"previousValue": 0.25,
"newValue": 0.4,
"formulasPersisted": true,
"restoredMatchesAfter": true,
"expectedArrChanged": true
}
}
}
The full output also includes the exact model-style function calls, the
serialized function_call_output strings, formula contracts, restored summary,
and a deterministic final message built from tool output.
The official Responses function-calling flow is a loop: send tools, receive
function_call items, run your code, append function_call_output items, then
send the updated input back to the model. The WorkPaper part is the dispatcher:
function dispatchOpenAiResponsesCall(call: OpenAiResponsesFunctionCall) {
if (call.name === 'read_workpaper_summary') {
const args = readSummaryInputSchema.parse(JSON.parse(call.arguments))
return tools.readWorkPaperSummary(args.range)
}
if (call.name === 'set_workpaper_input_cell') {
const args = setInputCellInputSchema.parse(JSON.parse(call.arguments))
return tools.setWorkPaperInputCell(args)
}
throw new Error(`unknown WorkPaper tool: ${call.name}`)
}
Return JSON from the tool, not prose. The next model turn can then say:
Edited Inputs!B3. Expected ARR moved from 60000 to 96000. That sentence is
grounded in formula readback, not in a guess.
OpenAI’s current function-calling guide covers the Responses API item types and
the function_call_output handoff:
https://platform.openai.com/docs/guides/function-calling?api-mode=responses.
That is the useful contract for workbook automation. A response that says “updated” is not enough unless the tool result proves what changed.
examples/headless-workpaper/openai-responses-tool-wrapper.tsexamples/headless-workpaper/README.md#openai-responses-tool-wrapperdocs/agent-workpaper-tool-calling-recipe.mdexamples/headless-workpaper/agent-framework-adapters.tsIf this saves you from building a spreadsheet tool from scratch, star the repo so other agent builders can find it: https://github.com/proompteng/bilig/stargazers.