LangGraph.js WorkPaper ToolNode Spreadsheet Tool
LangGraph.js workflows often route model tool calls through a ToolNode. That is
a good place for WorkPaper tools when the graph needs a number it can trust:
read a formula-backed summary, edit one input, recalculate, persist WorkPaper
JSON, restore it, then keep the proof in graph state.
The checked example uses the real @langchain/langgraph ToolNode with
AIMessage tool calls and returned ToolMessage state. It does not require an
LLM key because the smoke test supplies deterministic tool calls directly.
There are two owned proofs:
examples/langgraph-workpaper-tool-statewraps@bilig/workpaperdirectly as LangChain tools.examples/langchain-mcp-workpaper-toolnodeloads the published WorkPaper MCP stdio server through@langchain/mcp-adapters, then executes those MCP tools with a LangGraph.jsToolNode.
Run the checked graph
git clone https://github.com/proompteng/bilig.git
cd bilig
cd examples/langgraph-workpaper-tool-state
pnpm install --ignore-workspace --lockfile=false
pnpm run typecheck
pnpm run smoke
The smoke builds this graph:
new StateGraph(MessagesAnnotation)
.addNode('agent_requests_workpaper_tools', deterministicToolCalls)
.addNode('tools', new ToolNode(workpaperTools))
.addEdge(START, 'agent_requests_workpaper_tools')
.addEdge('agent_requests_workpaper_tools', 'tools')
.addEdge('tools', END)
It returns the graph nodes, tool-message names, the pre-edit summary, and the verified WorkPaper write proof:
{
"framework": "langgraphjs-toolnode",
"graphNodes": ["agent_requests_workpaper_tools", "tools"],
"toolMessageNames": ["read_workpaper_quote", "set_workpaper_quantity"],
"proof": {
"editedCell": "Inputs!B2",
"before": {
"total": 1458
},
"after": {
"total": 2187
},
"afterRestore": {
"total": 2187
},
"verified": true
}
}
ToolNode shape
import { AIMessage } from '@langchain/core/messages'
import { tool } from '@langchain/core/tools'
import { StateGraph, MessagesAnnotation, START, END } from '@langchain/langgraph'
import { ToolNode } from '@langchain/langgraph/prebuilt'
const tools = [
tool(readQuoteSummary, {
name: 'read_workpaper_quote',
schema: z.object({}),
}),
tool(setQuantityAndProve, {
name: 'set_workpaper_quantity',
schema: z.object({ quantity: z.number().finite().positive() }),
}),
]
const graph = new StateGraph(MessagesAnnotation)
.addNode('agent_requests_workpaper_tools', () => ({
messages: [
new AIMessage({
content: '',
tool_calls: [
{ id: 'call_read_quote', name: 'read_workpaper_quote', args: {}, type: 'tool_call' },
{ id: 'call_set_quantity', name: 'set_workpaper_quantity', args: { quantity: 18 }, type: 'tool_call' },
],
}),
],
}))
.addNode('tools', new ToolNode(tools))
.addEdge(START, 'agent_requests_workpaper_tools')
.addEdge('agent_requests_workpaper_tools', 'tools')
.addEdge('tools', END)
.compile()
What to copy
- Use separate read and write tools so graph state stays easy to inspect.
- Return exact
ToolMessagecontent with the edited cell and formula readback. - Keep persistence and restore verification in the tool result when the graph can resume later from a checkpoint.
- Keep the compatibility caveat visible: this is a WorkPaper API, not full desktop Excel UI automation.
MCP adapter proof
Use this path when the agent stack already loads tools through MCP:
git clone https://github.com/proompteng/bilig.git
cd bilig
cd examples/langchain-mcp-workpaper-toolnode
pnpm install --ignore-workspace --lockfile=false
pnpm run typecheck
pnpm run smoke
The smoke starts the published WorkPaper MCP server over stdio:
npm exec --yes --package @bilig/workpaper@latest -- \
bilig-workpaper-mcp \
--workpaper .tmp/pricing.workpaper.json \
--init-demo-workpaper \
--writable
Then MultiServerMCPClient discovers the file-backed WorkPaper tools and
ToolNode calls read_cell, set_cell_contents, read_cell again,
get_cell_display_value, and export_workpaper_document. Finally it starts a
second read-only MCP client against the same WorkPaper JSON to prove the
persisted formula result survives a process boundary.
Expected proof shape:
{
"framework": "langchainjs-mcp-adapters-toolnode",
"mcpTransport": "stdio",
"workpaperPackage": "@bilig/workpaper@latest",
"editedCell": "Inputs!B3",
"dependentCell": "Summary!B3",
"before": 60000,
"after": 96000,
"afterRestore": 96000,
"afterRestart": 96000,
"displayValue": "96000",
"persistedDocumentBytes": 1162,
"checks": {
"discoveredFileBackedTools": true,
"dependentCellChanged": true,
"persistedToDisk": true,
"restartReadbackMatchesAfter": true,
"displayValueRead": true,
"exportedWorkPaperDocument": true
},
"verified": true
}
Official LangGraph.js references:
- https://docs.langchain.com/oss/javascript/langchain/mcp
- https://docs.langchain.com/oss/javascript/langchain/tools
- https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph.prebuilt.ToolNode.html
- https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph.prebuilt.toolsCondition.html
Runnable source:
examples/langgraph-workpaper-tool-state
and
examples/langchain-mcp-workpaper-toolnode.