CLI Scripting

witan xlsx exec is the main spreadsheet interface for agents. The CLI sends the workbook and script to the configured Witan API, which runs the script inside an xlsx-serve sandbox with the xlsx API preloaded and the live workbook exposed as wb.

Use it when a task needs more than a single verifier command: read workbook structure, search for labels, trace dependencies, test formulas, write cells, recalculate, lint, and render from one script. The standalone render, calc, and lint commands remain useful for quick checks and CI, but exec is the workflow tool for agent-driven spreadsheet edits.

For command flags and other CLI commands, see the CLI Reference. For every xlsx.* method, see the xlsx API.

$ witan xlsx exec report.xlsx --code '
  const result = await xlsx.setCells(wb, [
    { address: "Summary!B5", formula: "=SUM(Revenue!B2:B13)" }
  ])
  const lint = await xlsx.lint(wb, { rangeAddresses: ["Summary!A1:C10"] })
  return { touched: result.touched, errors: result.errors, warnings: lint.total }
'
{"ok":true,"result":{"touched":{"Summary!B5":"$1,250,000","Summary!B6":"$187,500","Summary!B10":"$3,750,000"},"errors":[],"warnings":0},"writes_detected":true}

witan xlsx exec gives agents a TypeScript scripting surface over the same workbook engine used by render, calc, and lint. A single request can read, write, recalculate, lint, and render, with results returned as structured JSON.

Four engines in one tool

The exec runtime exposes the main workbook operations in one scripting entry point.

The Edit Engine

Methods for browsing and modifying workbooks from code.

Read workbook structure, discover tables, search content, edit cells, change sheet layout, inspect dependencies, test formulas, and update styling from the same script. Use the xlsx API for the complete xlsx.* method list, signatures, and return shapes.

The Rendering Engine

await xlsx.previewStyles(wb, "Sheet1!A1:F20")

Generate a PNG preview of a sheet-qualified range directly in code. Use this when a script needs to verify layout, formatting, chart output, or a visual change without leaving the workbook session.

The Formula Engine

const result = await xlsx.setCells(wb, [
  { address: "Sheet1!B5", formula: "=SUM(B1:B4)" }
])
print(result.errors)

Formula-affecting writes trigger recalculation. Errors are reported immediately in the write result, so scripts can stop before saving a broken workbook.

The Linting Engine

const diagnostics = await xlsx.lint(wb)
print(diagnostics.filter(d => d.severity === "Warning"))

Run semantic rules against the workbook from within your code, using the same lint engine as witan xlsx lint.

How it works

$ witan xlsx exec report.xlsx --expr 'await xlsx.readCell(wb, "Summary!A1")'

The CLI sends your code and workbook request to the configured Witan API. The API runs the script in an xlsx-serve sandbox with @witan/xlsx preloaded. The workbook is available as the global wb. Your code calls methods on the xlsx object; see the xlsx API for full signatures and return shapes. The result is returned as structured JSON.

exec can also run Office Scripts with an ExcelScript-like workbook object when the script defines a top-level main entry point whose first parameter is typed as ExcelScript.Workbook.

Example scenario

Discover, look up, trace, test, write, and verify

// 1. Discover the workbook's structure
const summary = await xlsx.describeSheet(wb, "Summary")
const tables = Object.values(summary.tables)
const table = tables.find(t => t.tableName === "Summary") ?? tables[0]
if (!table) throw new Error("No table detected on Summary")

// 2. Look up a value by label, not by hardcoded cell address
const [q3Revenue] = await xlsx.tableLookup(wb, {
  table: table.address,
  rowLabel: "Total Revenue",
  columnLabel: "Q3"
})
const [q4Revenue] = await xlsx.tableLookup(wb, {
  table: table.address,
  rowLabel: "Total Revenue",
  columnLabel: "Q4"
})
if (!q3Revenue || !q4Revenue) throw new Error("Revenue cells not found")

// 3. Check what depends on this cell before changing it
const downstream = await xlsx.getCellDependents(wb, q4Revenue.address, 3)
print(`Updating ${q4Revenue.address} will affect ${downstream.cells.length} cells`)

// 4. Test a formula before writing it
const preview = await xlsx.evaluateFormula(wb, "Summary",
  `=${q3Revenue.address}*1.1`)

// 5. Write the update
const result = await xlsx.setCells(wb, [{
  address: q4Revenue.address,
  formula: `=${q3Revenue.address}*1.1`
}])

// 6. Verify — lint and screenshot
const lint = await xlsx.lint(wb, { rangeAddresses: [table.address] })
await xlsx.previewStyles(wb, table.address)

return { updated: q4Revenue.address, downstream: downstream.cells.length,
         preview, warnings: lint.diagnostics }

One request discovers structure, looks up by label, checks impact, tests a formula, writes, lints, and renders a preview.

Access tracking

Every exec call returns an accesses array showing exactly which cells were read and written:

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

Code sources

Provide exactly one source of TypeScript or JavaScript:

witan xlsx exec report.xlsx --expr 'await xlsx.readCell(wb, "Summary!A1")'
witan xlsx exec report.xlsx --script ./analyze.ts --input-json '{"threshold":10}'
cat analysis.ts | witan xlsx exec report.xlsx --stdin

Use --expr for a single expression. Use --code, --script, or --stdin for multi-statement scripts.

The runtime compiles scripts as TypeScript before execution, so .ts files, type annotations, interfaces, type aliases, generics, and TypeScript enums are accepted. Imports are still blocked by the sandbox; scripts run with the preloaded xlsx, wb, input, and print globals.

Image file inputs

When adding workbook images with xlsx.addImage or xlsx.setImage, pass local PNG/JPEG files with --input-file key=@path. The CLI converts the file to a data:image/...;base64,... URI before sending the script request:

witan xlsx exec report.xlsx --save --input-file logo=@./logo.png --stdin <<'WITAN'
await xlsx.addImage(wb, "Sheet1", {
  name: "Logo",
  position: { from: { cell: "A1" }, to: { cell: "D6" } },
  source: { base64: input.logo }
})
WITAN

Saving changes

By default, exec does not overwrite the local workbook. Add --save when the script should write returned workbook bytes to the target path:

witan xlsx exec report.xlsx --save --code '
  await xlsx.setCells(wb, [{ address: "Sheet1!A1", value: "Updated" }])
  return { ok: true }
'

Create a new workbook with --create --save:

witan xlsx exec model.xlsx --create --save --code '
  await xlsx.addSheet(wb, "Inputs")
  return { ok: true }
'

Next steps

  • CLI Reference — installation, global flags, and command flags.
  • Office Scripts — run Excel Office Script-style code through xlsx exec.
  • xlsx API — full xlsx.* method signatures and return shapes.
  • Concepts — understand how the runtime, the workbook handle, and access tracking fit together.
  • Skills — optional skill files for agent tools that support them.