Excel Automation

Development Log: What a 120,000-Row Spreadsheet Actually Breaks

A working session on a spreadsheet of 121,254 rows and 1.8 million cells, and the five separate things that had to change before a large cross-table analysis could finish, verify its output, and return a usable formatted report.

Most spreadsheet features are tested on data that fits on a screen. This log covers a working session on a sales workbook with 121,254 rows across 15 columns — roughly 1.8 million cells — and a request that sounds ordinary: combine the product, customer, order, and sales tables, then flag declining products, restock risks, and low-value customers.

Nothing about that request is exotic. It failed anyway, several times, for reasons that had almost nothing to do with the analysis itself. What follows is what actually broke and what changed.

A platform limit, not a slow query

The first failure looked like a bug in the durable job system. The real cause was a rule in Google Apps Script: an editor add-on may not create a time-driven trigger that fires more often than once an hour.

The background-job design assumed a one-minute trigger. That assumption held during development, where a container-bound script may schedule freely, and stopped holding the moment the same code ran as an installed add-on. The request to install the trigger did not degrade — it threw, and it threw before the job was created, so the work never started.

Two changes followed. Installing a trigger is now best-effort: it tries a one-minute cadence, falls back to hourly, and finally to no trigger at all, and it never throws. And the first processing step now runs inside the same execution that created the task, rather than in a second call that has to look the task up again.

That second detail mattered more than it sounds. Apps Script properties are not reliably read-your-writes across executions, so a task written moments earlier could come back as "task not found" on the very next call.

Reporting progress is not the same as finishing

With the job finally starting, it still did not finish. The tool ran one step, then returned a status saying the work "continues in the background."

That sentence was false. With no sub-hourly trigger available, nothing continues in the background. The assistant read the status, relayed a percentage to the user, and stopped — leaving a job parked at 34,000 of 121,253 rows indefinitely.

The runtime now drives the job to completion itself, within a bounded budget, and every subsequent status call advances the work rather than merely reading it. If the budget is exhausted, the status text says plainly that the task is unfinished and that nothing else will advance it.

The principle is worth stating directly: a progress report is not a deliverable. A user asked for a table, not a percentage.

A request can only be served by tools the model can see

To keep tool selection accurate, GetSheetAI discloses a subset of its tools per turn based on the request. That mechanism was built around the Excel toolset, and the Google Sheets add-on registers 22 tools that exist only there. Those tools were being filtered out of every request.

The effect was specific and easy to miss. Asking for a bar chart disclosed 6 of 44 tools, with the chart tool among the hidden ones. Asking to sort a range disclosed 5 of 44, without the sort tool. The assistant was not refusing — it genuinely could not see the tool that does the job.

Sheets now has its own tool-to-bundle mapping, mirroring each Excel counterpart, and the filter passes through any tool it has no opinion about rather than dropping it. A test now reads the registered tool names straight from source, so adding a tool without classifying it fails the build instead of quietly making it unreachable.

The same class of gap appeared in phrasing. A Chinese request meaning "create a new sheet" matched no rule, because the pattern recognised only one of the two common words for a sheet. The sheet-creation tool stayed hidden and the assistant reported that creating a worksheet was impossible. It was not.

Error messages are part of the product

Several failures came down to a message that stated a problem without stating the way out.

Writing to a sheet that does not exist returned "The requested resource doesn't exist." That reads like a broken add-in. It now says the sheet does not exist, that the write tool does not create sheets, and which two calls do.

Refusing row-level classification on a very large range returned a bare refusal. Labelling an aggregated result works at any size, so the message now names that path: group first, then apply classification rules to the grouped result.

An overwrite guard returned nothing but blocked: true, which reads as failure. It now explains that the target already holds data and how to proceed.

None of these are cosmetic. In each case the previous message ended a task that was still completable.

Deterministic expressions needed more arithmetic

Deriving a year-month such as 201707 from an integer date key like 20170702 requires either floor(x / 100) or a modulo. Neither existed. Two attempts failed and the derived column was abandoned.

The expression layer now includes floor, round, abs, and mod, and the unsupported-function error lists the full set and gives that exact expression, rather than only naming what was rejected.

Where it stands

On the same workbook, the durable path now completes: all 121,253 rows processed, grouped, written, and formatted, with every result chunk verified. On Excel, the same request produced a classification across all 397 products — 132 with no recent sales, 101 flagged for restock risk, 99 declining, 65 normal — computed with native SUMIFS against the source table rather than moving the data anywhere.

The operating values worth knowing:

  • durable routing threshold: more than 100,000 cells;
  • source chunking: bounded reads, verified per chunk on writeback;
  • regression coverage: 826 tests across the shared runtime;
  • background execution on Google Sheets add-ons: hourly at best, so the sidebar drives long jobs while it is open.

That last point is a real limit rather than a temporary one. An installed add-on cannot schedule work more frequently, so a large job progresses while the sidebar is open. The durable checkpointing means closing it loses no completed work, but the honest description is that the work is driven, not scheduled.

The broader lesson from this session was not about scale. Every one of these failures was a case of the system knowing something the user could not see: a platform rule, a hidden tool, a supported path named nowhere in the error. Size was what made them all visible at once.