Spreadsheet

Witan looks like four CLI commands, but underneath it's a single architecture: a sandboxed JavaScript runtime that talks to a high-fidelity .NET spreadsheet engine over RPC. Understanding that architecture — and a few specific design decisions on top of it — is the difference between using Witan well and fighting it.

This page is the mental model. Read it once and the four reference pages will click together.

The architecture in one picture

┌──────────────────────────┐         ┌──────────────────────────┐
│   witan CLI (Go binary)  │  RPC    │   .NET spreadsheet engine │
│                          │ ──────► │                          │
│  ┌────────────────────┐  │         │   • xlsx/xml fidelity     │
│  │ JavaScript sandbox │  │         │   • Formula recalculation │
│  │   xlsx.* methods   │  │         │   • Rendering             │
│  │   global `wb`      │  │         │   • Linting               │
│  └────────────────────┘  │         │                          │
└──────────────────────────┘         └──────────────────────────┘

Your agent writes JavaScript. Witan's CLI spins up a sandboxed JS runtime with @witan/xlsx pre-loaded and the workbook exposed as a global wb. Each method call on xlsx makes an RPC to the .NET engine, which does the actual spreadsheet work and returns structured data. The script's return value becomes the CLI's JSON output.

This split is deliberate:

  • JavaScript for scripting — LLMs write JS more reliably than any other language.
  • .NET for the engine — full Excel formula fidelity, dynamic arrays, conditional formatting, and rendering at pixel parity with Excel itself.
  • Sandboxed runtime — no filesystem access, no network, no imports. The agent's script can only manipulate wb.

The four engines, one entry point

Witan has four CLI subcommands, but they are not four separate tools:

CLI command What it does Available inside exec?
witan xlsx exec Runs JS against the workbook with 30+ purpose-built methods
witan xlsx render High-fidelity PNG/WebP of a cell range Yes: xlsx.previewStyles(wb, range)
witan xlsx calc Recalculates every formula in the workbook Yes: write a formula with xlsx.setCells and recalc is automatic
witan xlsx lint Runs eleven semantic rules Yes: xlsx.lint(wb, …)

render, calc, and lint exist as standalone commands for quick checks and CI. The vast majority of agent work runs through exec, where a single script can write, recalculate, lint, and screenshot in one round-trip.

The workbook handle: wb

Inside the exec runtime, the workbook is a global called wb. Every method that operates on the workbook takes wb as its first argument:

await xlsx.readCell(wb, "Summary!A1")
await xlsx.setCells(wb, [{ address: "B5", formula: "=SUM(B1:B4)" }])
await xlsx.lint(wb, { rangeAddresses: ["Summary!A1:C10"] })

wb is not the file on disk. It's a handle to a live representation of the workbook held by the .NET engine. Writes go through this handle:

  • In-memory by default. xlsx.setCells(...) modifies the handle but does not touch the file on disk. The script can experiment freely.
  • Persisted with --save. Pass --save to the CLI and the modified workbook is written back to the file when the script returns.

This means your agent can write a script that tries an edit, lints it, and decides whether to commit — without ever risking the on-disk file.

Reads, writes, and access tracking

Every exec call returns an accesses array listing exactly which cells the script read and wrote:

{
  "ok": true,
  "result": { },
  "writes_detected": true,
  "accesses": [
    { "operation": "read", "address": "Summary!B2:B10" },
    { "operation": "write", "address": "Summary!B5" }
  ]
}

This is the audit trail your agent needs to:

  • Verify it only touched the cells it meant to.
  • Show its work to a human reviewer.
  • Detect unintended writes from a buggy script before --save persists them.

writes_detected is the high-level boolean — if it's false, the script was effectively read-only regardless of intent.

Smart error filtering

witan xlsx calc recalculates the entire workbook on every invocation, but it only reports errors the recalculation just introduced. Pre-existing #N/A and #REF! cells in the file are filtered out.

This matters because agents edit live spreadsheets. A workbook may already contain fifteen #N/A errors from upstream data issues. Without filtering, your agent has to diff before/after to figure out which errors are new. With filtering, every error in the output is one the agent needs to fix — full stop.

The same principle runs through exec: when xlsx.setCells triggers automatic recalculation, the errors field on the return value contains only new errors.

Token-efficient by design

Witan is built for vision and language models that pay per token. Three design choices follow from that:

  • Range-targeted rendering. render produces a PNG of exactly the range you ask for, auto-scaled to stay under vision model image limits (1568px on the longest side by default). Full-sheet screenshots waste tokens on empty cells.
  • TSV reads. readRangeTsv, readRowTsv, readColumnTsv return tab-separated text instead of JSON arrays — significantly fewer tokens for the same data.
  • Semantic table access. detectTables + tableLookup let your agent look up "Total Revenue, Q4" instead of reading and parsing a whole region to find it.

When you're tempted to dump a whole sheet, reach for one of these primitives first.

Recalculation is automatic

Every write triggers a recalculation. This is true at every level:

  • xlsx.setCells inside exec recalculates immediately and returns any new errors.
  • witan xlsx calc recalculates the whole workbook and writes the correct cached values back into the file.

Excel stores a "cached value" alongside each formula — the last computed result. Many editing libraries (openpyxl, xlwings in write-only mode) leave these stale or missing, which means users who open the file see wrong numbers until Excel itself recalculates. Witan never leaves cached values stale. When you save, the file is consistent.

Dependency tracing

The .NET engine knows the full formula graph. Your agent can ask it:

const precedents = await xlsx.getCellPrecedents(wb, "Summary!B5", 3)
const dependents = await xlsx.getCellDependents(wb, "Inputs!B2", 3)
  • Precedents — what feeds into a cell. Useful before reading a number, so the agent knows what assumptions it rests on.
  • Dependents — what depends on a cell. Critical before changing a cell, so the agent knows what will move downstream.

The depth parameter limits how far the trace walks. traceToInputs and traceToOutputs are the same idea but walk all the way to leaf cells.

Pixel diff: verifying visual changes

When an agent edits a spreadsheet, the user often cares about the visual result — formatting, colors, alignment — not just the underlying values. The render --diff workflow proves only the intended cells changed visually:

  1. Render the region before the edit, save as before.png.
  2. Make the edit.
  3. Render the same region with --diff before.png.

The output is a diff image with changed pixels highlighted at full color and unchanged areas desaturated. The metadata line summarizes:

Sheet1!A1:F20 | ~768x600px | dpr=2 | diff: 1,204 pixels changed (3.2%)

This is the closest thing in the spreadsheet world to a test assertion — and it's exactly what an agent loop needs to self-verify.

Stateless vs. stateful operation

By default, Witan caches your uploaded file server-side so repeated operations on the same workbook are fast. For sensitive data, set WITAN_STATELESS=1:

  • Default (WITAN_STATELESS=0) — server-side caching for speed.
  • Stateless (WITAN_STATELESS=1) — every command re-uploads the file. Files are processed in memory and immediately discarded. Nothing is retained.

Slower for repeated operations, but a hard guarantee for compliance-sensitive workflows. See Configuration for setup.

When to use which command

A rough decision tree for agent authors:

  • Just looking? exec with readCell / readRange / tableLookup. Often a single --expr is enough.
  • Editing? exec with setCells + --save. Check accesses and errors in the return value.
  • Need a visual? xlsx.previewStyles inside exec, or standalone witan xlsx render.
  • Recalculating a file someone else wrote? Standalone witan xlsx calc. Use it as the "fix cached values" step before handing the file to a non-Excel viewer.
  • Verifying a finished edit? witan xlsx lint for semantic bugs, witan xlsx render --diff for visual regression.

What Witan does not do

Knowing the boundaries upfront:

  • No external workbook references. Formulas like =[Budget.xlsx]Sheet1!A1 are resolved from cached values only.
  • No VBA / macro execution. .xlsm files are recalculated correctly, but VBA code is not run.
  • No add-in functions. Custom functions from Bloomberg, Power Query, etc., are not evaluated.

See the individual reference pages for the full lists.

Next steps

  • CLI — the JavaScript runtime in depth.
  • render, calc, lint — the standalone engines.
  • Skills — drop-in skill files so your agent inherits this mental model automatically.