Apps Script example · 8 min read

Deduplicate Rows By Email: Copy-Paste Apps Script Pattern

Working deduplicate rows by email example in Apps Script—copy-paste code, common mistakes, and when to get it built professionally.

SheetsDedupeCleanup

Imported leads often repeat the same email. Deduping in Apps Script is faster than manual filters for recurring loads.

deduplicateRowsByEmail scans column B, lowercases/trims emails, and keeps the first row for each address.

Blank emails are preserved (not treated as duplicates of each other) so incomplete rows are not collapsed.

The sheet is rewritten with clearContents + setValues — back up first.

Need this built? Hire a Google Apps Script developer →

Sheet / project setup

SheetColumnPurpose
LeadsA NameLead name
LeadsB EmailDedupe key
LeadsC SourceAttribution
EMAIL_COL21-based column index

What this script does

First-wins dedupe by normalized email with a full sheet rewrite.

Prerequisites

Leads sheet; backup copy; email in column B.

  • Decide first vs last wins
  • Normalize Gmail dots only if required
  • Width stable across rows

Walkthrough

Insert two rows with same email different case; run; confirm one remains.

Edge cases

clearContents removes formatting — use clear({contentsOnly:true}) if you need formats kept.

  • Order preserved for first occurrences
  • Empty emails kept

How to test

Count unique emails with a Set before/after.

Hardening for production

Write duplicates to Leads_Dupes instead of dropping; soft-delete with a flag column.

Variations

Dedupe on compound keys like email+campaign.

Full code: deduplicateRowsByEmail()

Backup the spreadsheet, set EMAIL_COL, then run deduplicateRowsByEmail().

/**
 * Deduplicate rows by email, keeping the first occurrence.
 */
const SHEET = "Leads";
const EMAIL_COL = 2; // B

function deduplicateRowsByEmail() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET);
  const values = sheet.getDataRange().getValues();
  const seen = {};
  const kept = [values[0]];
  let removed = 0;

  for (let i = 1; i < values.length; i++) {
    const email = String(values[i][EMAIL_COL - 1] || "").trim().toLowerCase();
    if (!email) {
      kept.push(values[i]);
      continue;
    }
    if (seen[email]) {
      removed++;
      continue;
    }
    seen[email] = true;
    kept.push(values[i]);
  }

  sheet.clearContents();
  sheet.getRange(1, 1, kept.length, kept[0].length).setValues(kept);
  Logger.log("Removed %s duplicate email rows; kept %s", removed, kept.length - 1);
}
  1. Line 5: 1-based email column index.
  2. Line 15: Normalizes case before comparison.
  3. Line 20: Tracks emails already kept.
  4. Line 16: Blank emails are not deduped against each other.
  5. Line 28: Prepares the sheet for rewrite.
  6. Line 29: Writes the deduped matrix.

Deploy this example

  1. 01

    Open Apps Script

    In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.

  2. 02

    Paste and save

    Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.

  3. 03

    Authorize once

    Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.

  4. 04

    Add the trigger

    Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.

Before you run: dedupe

  • 1Backup or duplicate the file
  • 2Confirm email column index
  • 3Agree first-wins policy with stakeholders
  • 4Check blank email policy
  • 5Run on a copy with known duplicates first
  • 6Re-apply filters/frozen rows if needed after rewrite

Frequently asked questions

Iterate from the bottom or overwrite seen[email] with the latest row index, then rebuild.

Normalize by stripping +tag before @ if your business treats them as one person.

getValues snapshots calculated values; formulas are not restored. Use getFormulas if needed.

Works until memory pressure — process in chunks writing to a new sheet.

Lowercasing collapses those. Accented characters still differ.

Build a seen map from the master sheet, then filter the secondary sheet.

Only via version history or your backup — treat as destructive.

Related examples

Want this wired into your real workflow?

I adapt these patterns to your Sheet structure, APIs, and triggers — deployed in your Google account. Fixed-scope quotes from $500 · free 30-min consult · quote within 24 hours.