Spreadsheet agents need a transaction log, not a screenshot

Spreadsheet agents need a transaction log, not a screenshot cover image

A screenshot can show an agent selecting F12 and typing 48. It cannot tell you whether F12 contains a literal or a formula, whether the cells that depend on it recalculated, or whether the saved workbook contains the same state.

That makes a screenshot a projection of a workbook, not a transaction. It is useful evidence for a person looking at the screen. It is a poor boundary for an agent that needs to change a document and account for the change.

The distinction matters because a write has several different moments:

  1. the agent decides what it means to do,
  2. the workbook accepts a mutation,
  3. formulas calculate from the new input,
  4. the new document is persisted,
  5. the browser presents the result.

Those moments can disagree. A good design makes the disagreement visible instead of compressing it into a green “done” message.

For a concrete implementation of this split, see Inside Bilig’s verification path for spreadsheet agents.

A grid is a projection

Spreadsheet APIs already expose more than a grid can show. Google Sheets’ values.update endpoint lets a caller request the updated values in the response, and its includeValuesInResponse option is false by default. Microsoft Graph’s workbookRange resource separates formulas, raw values, displayed text, and value types.

That separation is not API ceremony. It is the data an agent needs to answer basic questions:

  • Did the requested cell change, or did the write target the wrong range?
  • Is the new content a formula, a value, or an error?
  • Did a dependent total change after recalculation?
  • Did the change reach the persisted document?
  • Is the visible range showing the same revision as the workbook model?

A screenshot may help answer the last question. It cannot reliably answer the others, especially when the interesting cell is hidden, off-screen, or still using a cached result.

Write a receipt for every mutation

The useful output of a workbook tool is a receipt with enough information to reconstruct what happened. The shape does not need to be elaborate. It needs to preserve the boundaries that a write-only API throws away.

type MutationStatus =
  'staged' | 'queued' | 'applied' | 'verification_incomplete' | 'failed'
 
type MutationReceipt = {
  readonly id: string
  readonly operation: 'setCell' | 'writeRange' | 'setFormula'
  readonly baseRevision: number
  readonly appliedRevision: number | null
  readonly changes: ReadonlyArray<{
    readonly address: string
    readonly before: string | number | boolean | null
    readonly after: string | number | boolean | null
    readonly formulaBefore: string | null
    readonly formulaAfter: string | null
  }>
  readonly checks: {
    readonly recalculated: boolean
    readonly authoritativeReadback: boolean
    readonly persisted: boolean
    readonly renderedReadback: boolean | null
    readonly undoAvailable: boolean
  }
  readonly warnings: readonly string[]
  readonly status: MutationStatus
}

The before and after values make a receipt useful to a person. The serialized formulas and revision numbers make it useful to a test or a replay worker. The checks explain why status is verification_incomplete instead of pretending that an applied write is automatically a trusted write.

The browser does not disappear from this design. It becomes one reader of the receipt. A human can inspect the rendered range, while the agent can inspect the formula, calculated value, document revision, and persistence result.

Make the proof path explicit

The operation should be small enough to describe in one sentence:

Read Inputs!B2, set it to 48, recalculate, read Summary!B2, persist the document, reload it, and compare the restored value.

That sentence is a workflow. It is also a useful test case. Each step should produce data that the next step can check:

  • Capture the input’s serialized content and the workbook revision before the edit.
  • Reject the operation if the caller’s base revision is no longer current.
  • Apply one named operation to one explicit range.
  • Read the edited cell and its dependent cells from the authoritative workbook model after recalculation.
  • Serialize and restore the document, then compare the restored readback with the post-edit readback.
  • Ask the renderer for the range the user will inspect, and record whether that render belongs to the applied revision.

The last check is intentionally optional in a headless service. A service can prove the workbook state without having a browser attached. It should say that rendered proof was not requested, though, rather than silently turning a missing check into success.

Replay needs a command, not just a diff

A before/after diff tells you what changed once. A replayable log also needs the operation that produced the change and the state it expected to find.

type ReplayableEdit = {
  readonly id: string
  readonly baseRevision: number
  readonly target: 'Inputs!B2'
  readonly command: { readonly kind: 'setCell'; readonly value: number }
  readonly expectedBefore: number
  readonly expectedAfter: number
}
 
function canReplay(
  edit: ReplayableEdit,
  current: { revision: number; value: number },
) {
  return (
    current.revision === edit.baseRevision &&
    current.value === edit.expectedBefore
  )
}

The precondition prevents an old agent decision from overwriting a newer human edit. It also makes a conflict diagnosable: the command was valid, but the workbook it expected is no longer the workbook that exists.

Replay must be separated from external side effects. Recalculating a formula is an internal workbook operation; sending an email or charging a card is not. A replay worker should be able to rebuild or inspect workbook state without repeating effects outside the workbook. If a workflow needs both, its receipt should record the workbook mutation and the external handoff as separate operations.

Undo is part of the contract

Undo is not the same thing as “the user can probably press Command-Z.” An agent needs a recoverable state or an inverse command that it can identify and verify.

The principle is familiar from databases. SQLite’s rollback-journal documentation describes saving the original pages before changing the database, so an interrupted transaction can restore the prior state. A spreadsheet agent does not have to copy SQLite’s implementation, but it needs the same invariant: the pre-edit state must still be available when the edit is considered risky.

An undo record should answer three questions:

  • What snapshot, inverse operation, or token restores the old state?
  • Which revision or ranges will it restore?
  • After restore, did the authoritative readback match the original values and formulas?

If the agent cannot answer those questions, the receipt should say that undo is unproven. A visually correct cell is not a reason to hide that gap.

The smaller claim is the stronger one

Spreadsheet interfaces still matter. They should not be asked to prove more than they can show.

Let the grid show selection, formatting, frozen panes, and the result a human will review. Let the workbook API own named operations, formula evaluation, revision checks, persistence, and readback. Let the transaction record carry the warnings and the recovery path.

Then the agent can say something precise. The values below are illustrative; the important part is the evidence carried by each field:

I changed Inputs!B2 at revision 42. Summary!B2 recalculated from 24000 to 38400. The restored document returned 38400. The rendered range belongs to revision 42. Undo is available through token revision:42.

That is a useful answer. A screenshot can accompany it, but it should not have to stand in for it.