Sixteen rows that shouldn't have existed
A separate wiring bug (documented in a different write-up on this same pipeline) had caused one report-building node to re-execute four times in a single run, and each of those four executions wrote four rows to a Google Sheets tracking tab instead of one. Sixteen duplicate rows, all sharing the same timestamp, sitting in rows 66 through 81 of an otherwise clean append-only log. Cleaning them up was supposed to be the easy part.
The parameter that looks like it should just work
The Google Sheets node's delete operation takes a startIndex and a numberOfRows — read literally, "start at row 66, delete 16 rows" is exactly the right call for this cleanup. The node accepted it, ran without error, and returned what looked like a normal success response. Nothing in the response distinguished this from a genuine 16-row deletion.
What actually happened: one row, every time
A follow-up read of the Sheet told the real story: row 66 was gone. Rows 67 through 81 — fifteen more duplicates — were exactly as they'd been before the delete call. On this node/version, numberOfRows appears to have no effect on the delete operation at all; it silently behaves as if it were always 1, regardless of what value is actually set. There's no warning, no partial-success flag, no count mismatch reported anywhere — a caller has no way to detect this from the API response alone. The only way to know is to read the data back afterward and count.
The workaround: call it sixteen times, not once
Since a single delete call reliably removes exactly the row at startIndex, the fix was to stop trying to delete a range in one call and instead call the same single-row delete repeatedly — sixteen times total, always targeting startIndex: 66. This works because Google Sheets shifts every row below a deletion up by one automatically: after the first call removes what was row 66, the next duplicate row slides into that same position, so the identical startIndex: 66 call removes the next one, and the next, without needing to track a shrinking range manually.
Confirming it actually worked
A fresh read of the Sheet after all sixteen calls showed exactly the legitimate rows and nothing else — the duplicate block was gone, and no unrelated rows above or below it had shifted incorrectly. The disposable cleanup workflow was deactivated and deleted immediately after, the same hygiene applied to every one-off Sheets edit on this instance, so nothing was left active that could accidentally fire again later.
Why this one's worth writing down
A parameter that's silently ignored is worse than one that throws an error, because nothing about the call's own response gives any reason to suspect it. numberOfRows reads as unambiguous — it's a documented field on a delete operation with an obvious literal meaning — and it still didn't do what its name says on this node/version. The only defense is the same one that applies everywhere in a pipeline that writes or deletes real data: verify the actual state afterward by reading it back, don't infer success from a clean-looking API response.
n8n Automation Hub