n8n logon8n Automation Hub
Debugging notes

I Told n8n to Delete 16 Rows and It Deleted One, Silently, Every Time

Debugging notes · n8n workflow · September 1, 2026

A bug upstream had already written 16 duplicate rows into a tracking Sheet. Deleting them looked like a one-parameter job: give the node a start row and a count. The count was the part that quietly didn't work.

Workflow at a glance
  1. An earlier bug (a node re-triggering multiple times per run) had written 16 duplicate rows into a Google Sheets tracking tab, rows 66 through 81
  2. Built a disposable webhook-triggered n8n workflow to read the Sheet, confirm the exact duplicate range, then delete it — created, fired once, deactivated, deleted immediately after, same as every other one-off Sheets edit
  3. Deletion attempt: Google Sheets node, operation: delete, startIndex: 66, numberOfRows: 16
  4. Result: only row 66 was actually removed. No error, no partial-count warning — the call reported success
  5. Re-reading the Sheet confirmed it: 15 of the 16 duplicate rows were still there
  6. Workaround: called the same single-row delete 15 more times in a row — each call deletes whatever now occupies startIndex: 66, since every prior delete shifts the remaining rows up by one
  7. Final re-read confirmed a clean Sheet: back down to exactly the legitimate rows, duplicates gone
Diagram of the pipeline whose Sheets tab needed the duplicate-row cleanup

Node diagram reconstructed from the live workflow via the n8n API.

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.

A bulk operation in n8n that reports success but only did part of the job?

I debug n8n data-integrity issues where the API response looks clean but the underlying data doesn't match what was asked for.