LangGraph.js WorkPaper ToolNode Spreadsheet Tool

May 29, 2026 ยท View on GitHub

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-state wraps @bilig/workpaper directly as LangChain tools.
  • examples/langchain-mcp-workpaper-toolnode loads the published WorkPaper MCP stdio server through @langchain/mcp-adapters, then executes those MCP tools with a LangGraph.js ToolNode.

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 ToolMessage content 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:

Runnable source: examples/langgraph-workpaper-tool-state and examples/langchain-mcp-workpaper-toolnode.