Find and flag duplicate rows before you push upstream

TL;DR — Scan a Google Sheet key column for duplicates, mark Dup / keep-first, then UrlFetch only clean rows — outbound hygiene before you push upstream.

Inbound imports often get an idempotency story ("re-run without duplicating"). Outbound pushes are the opposite footgun: two sheet rows with the same email and your API creates two contacts. Flag duplicates on the sheet before UrlFetch, then sync only clean rows.

TL;DR

  • Pick a key column (email, external id, SKU).
  • Scan once; first win stays keep; later hits get Dup (or similar) in a Status column.
  • Push loop skips anything marked Dup / empty key.
  • Complements "re-run import without duplicating" — that protects inbound; this protects outbound.
  • Use a document lock if humans edit while the scanner runs.

Using the Apps Script editor a lot? NitroGAS drops free themes & snippets right into script.google.com — optional Co-Pilot when you want a boost.

What does a simple scanner look like?

function flagDuplicateKeys(sheet, keyHeader, statusHeader, options) { options = options || {}; var headerRow = options.headerRow || 1; var keepLabel = options.keepLabel || 'keep'; var dupLabel = options.dupLabel || 'Dup'; var lastCol = sheet.getLastColumn(); var lastRow = sheet.getLastRow(); if (lastRow <= headerRow) return { kept: 0, dups: 0 }; var headers = sheet.getRange(headerRow, 1, 1, lastCol).getValues()[0]; var keyCol = headers.indexOf(keyHeader); var statusCol = headers.indexOf(statusHeader); if (keyCol === -1) throw new Error('Missing key header: ' + keyHeader); if (statusCol === -1) throw new Error('Missing status header: ' + statusHeader); var numRows = lastRow - headerRow; var values = sheet.getRange(headerRow + 1, 1, numRows, lastCol).getValues(); var seen = {}; var kept = 0; var dups = 0; for (var i = 0; i < values.length; i++) { var key = String(values[i][keyCol] == null ? '').trim().toLowerCase(); if (!key) { values[i][statusCol] = values[i][statusCol] || ''; continue; } if (seen[key]) { values[i][statusCol] = dupLabel; dups++; } else { seen[key] = true; // don't overwrite a richer status unless empty / previous dup mark var cur = String(values[i][statusCol] == null ? '').trim(); if (!cur || cur === dupLabel) values[i][statusCol] = keepLabel; kept++; } } sheet.getRange(headerRow + 1, 1, numRows, lastCol).setValues(values); return { kept: kept, dups: dups }; }

Normalize keys the same way your API will (normalizeEmail for emails).

How do you push only clean rows?

function pushCleanRows(sheet, keyHeader, statusHeader) { flagDuplicateKeys(sheet, keyHeader, statusHeader); var data = sheet.getDataRange().getValues(); var headers = data[0]; var keyCol = headers.indexOf(keyHeader); var statusCol = headers.indexOf(statusHeader); for (var r = 1; r < data.length; r++) { if (String(data[r][statusCol]).toLowerCase() === 'dup') continue; var key = String(data[r][keyCol] || '').trim(); if (!key) continue; // UrlFetchApp.fetch(...) with muteHttpExceptions + backoff as needed } }

Keep-first vs keep-latest

Default keep-first is safest for "first spreadsheet row wins." If you need keep-latest, scan bottom-up or sort by Updated At before flagging — document the rule on the sheet so humans don't fight the bot.

Wrap-up

Duplicates are a sheet problem until they become an API problem. Flag early, push late.

NitroGAS snippets for locks, header maps, and UrlFetch backoff fit around this; Co-Pilot is optional when the upstream payload shape is messy. Happy Coding.