Agent Framework Spreadsheet Tools
This page is for agent builders who already have an AI SDK, LangChain, Mastra, LlamaIndex.TS, LangGraph.js, CopilotKit, or Cloudflare Agents loop and need a spreadsheet tool that can do more than return a screenshot.
@bilig/workpaper/ai-sdk gives Vercel AI SDK users a ready tool() map for
WorkPaper reads and verified input-cell edits. Other frameworks can use the
same handler contract directly: sheets, addresses, formulas, computed readback,
and JSON persistence stay in ordinary Node functions while the framework wrapper
stays thin.
import { WorkPaper } from '@bilig/workpaper'
import { createAiSdkWorkPaperTools } from '@bilig/workpaper/ai-sdk'
const workpaper = WorkPaper.buildFromSheets({
Inputs: [
['Metric', 'Value'],
['Qualified opportunities', 20],
['Win rate', 0.25],
['Average ARR', 12000],
],
Summary: [
['Metric', 'Value'],
['Expected customers', '=Inputs!B2*Inputs!B3'],
['Expected ARR', '=B2*Inputs!B4'],
],
})
const tools = createAiSdkWorkPaperTools({
workpaper,
defaultReadRange: 'Summary!A1:B3',
proofRange: 'Summary!A1:B3',
writableSheets: ['Inputs'],
})
Real AI SDK generateText() Smoke
Run this first if you use the Vercel AI SDK and want to see the complete
generateText() path execute WorkPaper tools:
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:ai-sdk-generate-text
The smoke test uses the real ai package: generateText(), tool(),
stepCountIs(), and MockLanguageModelV3 from ai/test. The mock model keeps
the example provider-free. The WorkPaper tool execution is real TypeScript:
readWorkPaperSummary reads Summary!A1:B5, then setWorkPaperInputCell
writes Inputs!B3 = 0.4, recalculates dependent formulas, serializes the
document, restores it, and returns structured readback.
Passing output includes:
{
"apiShape": "AI SDK generateText -> tool -> execute",
"modelCallCount": 2,
"toolNames": ["readWorkPaperSummary", "setWorkPaperInputCell"],
"writeResult": {
"editedCell": "Inputs!B3",
"before": { "expectedArr": 60000, "targetGap": -34000 },
"after": { "expectedArr": 96000, "targetGap": 5600 },
"checks": {
"formulasPersisted": true,
"restoredMatchesAfter": true,
"expectedArrChanged": true
}
}
}
Inspect the runnable file here:
examples/headless-workpaper/ai-sdk-generate-text-tool-smoke.ts.
Real AI SDK streamText() Smoke
Use this command when your agent path streams model output:
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:ai-sdk-stream-text
The smoke test uses the real streamText() API, tool() wrappers, and
simulateReadableStream() from ai. The deterministic MockLanguageModelV3
streams two WorkPaper tool calls, the AI SDK executes those tools, and the
second model step streams the final answer. The WorkPaper behavior is the same
as the generateText() smoke: read Summary!A1:B5, write
Inputs!B3 = 0.4, recalculate, serialize, restore, and verify the restored
summary.
Passing output includes:
{
"apiShape": "AI SDK streamText -> tool -> execute",
"modelStreamCallCount": 2,
"streamChunkTypes": ["tool-call", "tool-result", "tool-call", "tool-result", "text-delta", "text-delta"],
"writeResult": {
"editedCell": "Inputs!B3",
"before": { "expectedArr": 60000, "targetGap": -34000 },
"after": { "expectedArr": 96000, "targetGap": 5600 },
"checks": {
"formulasPersisted": true,
"restoredMatchesAfter": true,
"expectedArrChanged": true
}
}
}
Inspect the runnable file here:
examples/headless-workpaper/ai-sdk-stream-text-tool-smoke.ts.
AI SDK onStepFinish WorkPaper Transcript
Use onStepFinish when your application needs to persist or audit each
WorkPaper tool step while generateText() or streamText() is running. The AI
SDK calls it after a step has text, tool calls, and tool results available, so
the callback is the right place to record step.toolCalls and
step.toolResults before the next model step explains the calculated readback.
type ToolStepRecord = {
stepNumber: number
toolCalls: Array<{
toolCallId: string
toolName: string
input: unknown
}>
toolResults: Array<{
toolCallId: string
toolName: string
output: unknown
}>
}
const workpaperTranscript: ToolStepRecord[] = []
const result = await generateText({
model,
tools,
stopWhen: stepCountIs(2),
prompt: 'Read Summary!A1:B5, set Inputs!B3 to 0.4, then report the ARR proof.',
onStepFinish(step) {
workpaperTranscript.push({
stepNumber: step.stepNumber,
toolCalls: step.toolCalls.map(({ toolCallId, toolName, input }) => ({
toolCallId,
toolName,
input,
})),
toolResults: step.toolResults.map(({ toolCallId, toolName, output }) => ({
toolCallId,
toolName,
output,
})),
})
},
})
Use the same onStepFinish option with streamText(). For streaming paths,
make sure your app consumes the stream or awaits result.text / result.steps
so the tool calls execute and the callback can fire.
A compact projected transcript from the checked WorkPaper smokes looks like this:
[
{
"stepNumber": 0,
"toolCalls": [
{
"toolCallId": "call_read_summary",
"toolName": "readWorkPaperSummary",
"input": { "range": "Summary!A1:B5" }
},
{
"toolCallId": "call_set_input_b3",
"toolName": "setWorkPaperInputCell",
"input": { "sheetName": "Inputs", "address": "B3", "value": 0.4 }
}
],
"toolResults": [
{
"toolCallId": "call_read_summary",
"toolName": "readWorkPaperSummary",
"output": { "range": "Summary!A1:B5", "expectedArr": 60000 }
},
{
"toolCallId": "call_set_input_b3",
"toolName": "setWorkPaperInputCell",
"output": {
"editedCell": "Inputs!B3",
"before": { "expectedArr": 60000 },
"after": { "expectedArr": 96000 },
"restored": { "expectedArr": 96000 },
"checks": {
"formulasPersisted": true,
"restoredMatchesAfter": true,
"serializedBytes": 1162
}
}
}
]
}
]
Keep this as an application transcript, not a new dependency in this
repository. The runnable proof still lives in
examples/headless-workpaper/ai-sdk-generate-text-tool-smoke.ts
and
examples/headless-workpaper/ai-sdk-stream-text-tool-smoke.ts.
Runnable Adapter Example
Run the dependency-free adapter example from a clean 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:framework-adapters
The script builds the same workbook once per adapter family and exposes the same operations in the shapes those frameworks expect:
readWorkPaperSummaryandsetWorkPaperInputCellfor an AI SDK-style tool map withinputSchemaandexecuteread_workpaper_summaryandset_workpaper_input_cellfor a LangChain-style tool list withschemaandinvoke- Mastra-style
createTool({ id, inputSchema, outputSchema, execute }) - LlamaIndex.TS
tool(fn, { parameters })/FunctionToolstyle functions - LangGraph.js
ToolNode-style dispatch over LangChain tools - CopilotKit
useCopilotAction({ parameters, handler })action objects - Cloudflare Agents
AIChatAgent/streamText({ tools })style tools
The example installs zod for real schemas, but it does not install the agent
frameworks. That is deliberate. It keeps the WorkPaper contract visible and
avoids hiding workbook logic behind framework setup.
What A Passing Run Proves
The mutating tool edits Inputs!B3 and then verifies the dependent summary
formulas:
{
"editedCell": "Inputs!B3",
"before": {
"expectedCustomers": 5,
"expectedArr": 60000,
"expansionArr": 66000,
"targetGap": -34000
},
"after": {
"expectedCustomers": 8,
"expectedArr": 96000,
"expansionArr": 105600,
"targetGap": 5600
},
"checks": {
"previousValue": 0.25,
"newValue": 0.4,
"formulasPersisted": true,
"restoredMatchesAfter": true,
"expectedArrChanged": true
}
}
That is the useful part for agents. The tool result names the exact edited cell, returns before and after computed values, preserves formula contracts, serializes the workbook, restores it, and proves the restored output still matches the post-write state.
LangChain.js Structured Tool Smoke
The runnable adapter includes a LangChain-shaped smoke that keeps the tool output structured. The model can decide to call the tools, but the WorkPaper functions return JSON evidence instead of prose:
const tools = createLangChainTools(createWorkPaperTools(workbook))
const readResult = requireTool(tools, 'read_workpaper_summary').invoke({
range: 'Summary!A1:B5',
})
const writeResult = requireTool(tools, 'set_workpaper_input_cell').invoke({
sheetName: 'Inputs',
address: 'B3',
value: 0.4,
})
return {
readResult,
writeResult: {
editedCell: writeResult.editedCell,
before: writeResult.before,
after: writeResult.after,
checks: writeResult.checks,
},
}
Those four fields are the important LangChain.js contract: editedCell says
what changed, before and after prove the dependent formulas recalculated,
and checks records persistence and restored-readback assertions. Keep that
shape as the tool return value; do not collapse it into a sentence like
“spreadsheet updated successfully.”
Adapter Boundary
Keep the adapter boring:
- Build small SDK-neutral WorkPaper functions first.
- Validate the sheet name and A1 address before writing.
- Read dependent formulas before and after the edit.
- Serialize and restore the WorkPaper document.
- Return formula contracts and restored readback in the tool result.
The framework wrapper can then expose those functions with the local tool shape:
inputSchema and execute, schema and invoke, parameters and handler,
or a ToolNode-style dispatch wrapper. The workbook behavior should not care
which framework called it.
Official docs for the framework shapes:
- Vercel AI SDK tool calling: https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling
- AI SDK
toolreference: https://ai-sdk.dev/docs/reference/ai-sdk-core/tool - AI SDK
streamTextreference: https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text - LangChain JavaScript tools: https://docs.langchain.com/oss/javascript/langchain/tools
- Mastra
createTool(): https://mastra.ai/reference/tools/create-tool - LlamaIndex.TS tools: https://developers.llamaindex.ai/typescript/framework/modules/agents/tool/
- LangGraph.js
ToolNode: https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.prebuilt.ToolNode.html - CopilotKit
useCopilotAction: https://docs.copilotkit.ai/reference/hooks/useCopilotAction - Cloudflare Agents API and agent tools: https://developers.cloudflare.com/agents/api-reference/agents-api/ https://developers.cloudflare.com/agents/api-reference/agent-tools/
Framework-specific WorkPaper pages:
- Mastra WorkPaper spreadsheet tool
- LlamaIndex.TS WorkPaper spreadsheet tool
- LangGraph.js WorkPaper ToolNode spreadsheet tool
- CopilotKit WorkPaper spreadsheet action
- Cloudflare Agents WorkPaper spreadsheet tool
Files To Inspect
- adapter script:
examples/headless-workpaper/agent-framework-adapters.ts - real Mastra
createTool()smoke:examples/mastra-workpaper-tool/src/mastra-workpaper-tool.ts - example README:
examples/headless-workpaper/README.md#agent-framework-adapters - longer tool-calling recipe:
docs/agent-workpaper-tool-calling-recipe.md - agent writeback verification:
examples/headless-workpaper/agent-writeback-verification.ts
When This Is A Good Fit
Use this pattern when the agent needs to change a forecast, pricing model, pipeline summary, budget check, or workbook-backed business rule and then prove the formulas reacted. If the tool only says “I updated the spreadsheet” without computed readback, it is not enough for production workflows.
Start with the adapter command above. If it saves you an agent-tooling spike, keep the repository and release feed nearby: https://github.com/proompteng/bilig.
If it almost matches but a gap blocks adoption, open an implementation gap discussion: https://github.com/proompteng/bilig/discussions/new?category=general.