Task guide
Copy unique rows to a clean tab
Remove duplicate people from a contact list. The script keeps the first row for each email address and writes those rows to a tab named Clean, leaving the original list as it is.
1 · Prepare
Put these pieces in place
- Create a tab named Contacts with headers in row 1 and one person per row.
- Put the email address in column B. That is the value the script uses to spot a repeat.
- Do not keep notes of your own on a tab named Clean, because that tab is rewritten on every run.
Change these values
- Source tab
ContactsThis must match the tab that holds your list.
- Output tab
CleanThe script creates this tab when it is missing and clears it before each write.
- Key column index
1Column B is index 1. Counting starts at 0, so column A is index 0 and column C is index 2.
2 · Review before running
What the script can access and change
Permissions you approve
- See, edit, create, and delete all your Google Sheets spreadsheets. The code only reads the Contacts tab and writes the Clean tab in this spreadsheet, adding Clean the first time.
Effects of one run
- Creates a tab named Clean when it is missing, and clears everything in it before each write.
- Writes the header row and the first row found for each email address, in the order they appear in Contacts.
- Does not change, move, or delete any row on the Contacts tab.
- Leaves out rows with an empty cell in the email column.
3 · Copy
Paste this into the Apps Script editor
Create or open a script file, replace the example values marked above, then save the project.
const SOURCE_TAB = 'Contacts'; // This must match the tab that holds your list.
const OUTPUT_TAB = 'Clean'; // The script creates this tab when it is missing and clears it before each write.
const KEY_COLUMN_INDEX = 1; // Column B is index 1. Counting starts at 0, so column A is index 0 and column C is index 2.
function copyUniqueRowsToCleanTab() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const source = spreadsheet.getSheetByName(SOURCE_TAB);
if (!source) throw new Error('Add a Sheet tab named ' + SOURCE_TAB + ' before running this function.');
const values = source.getDataRange().getValues();
if (values.length < 2) throw new Error('The ' + SOURCE_TAB + ' tab needs a header row and at least one row of data.');
const header = values[0];
const seenKeys = new Set();
/** @type {unknown[][]} */
const uniqueRows = [];
let duplicateCount = 0;
values.slice(1).forEach((row) => {
const key = String(row[KEY_COLUMN_INDEX]).trim().toLowerCase();
if (!key) return;
if (seenKeys.has(key)) {
duplicateCount = duplicateCount + 1;
return;
}
seenKeys.add(key);
uniqueRows.push(row);
});
const clean = spreadsheet.getSheetByName(OUTPUT_TAB) || spreadsheet.insertSheet(OUTPUT_TAB);
clean.clearContents();
const output = [header].concat(uniqueRows);
clean.getRange(1, 1, output.length, header.length).setValues(output);
Logger.log('Unique rows kept: ' + uniqueRows.length + '. Duplicate rows skipped: ' + duplicateCount + '.');
} 4 · Run
Follow these steps in order
Build a small test list
On the Contacts tab, add five or six rows and give two of them the same email address. That way you can see one row kept and one row skipped.
Paste the function and save
Choose Extensions, then Apps Script. Paste the function over the starter code and save.
Run it and read the log
Choose copyUniqueRowsToCleanTab in the function dropdown above the code, then click Run. Approve access when Google asks. The Execution log shows a line such as "Unique rows kept: 5. Duplicate rows skipped: 1."
Check both tabs
The Clean tab holds the header and one row per email address. Contacts is unchanged. Each later run clears Clean and writes it again from the current Contacts tab.
5 · Confirm
How to tell it worked
The Clean tab holds the header and one row per email address, and the Execution log reports how many rows were kept and how many duplicates were skipped.
If the result is different
Troubleshooting
- The run stops with "Add a Sheet tab named Contacts before running this function."
- Rename your tab to Contacts, or change SOURCE_TAB in the code to the name of your tab.
- Two rows that look the same both landed in Clean.
- Compare the two email addresses character by character. The script compares column B only, ignoring spaces around the address and capital letters, so a different domain or an extra character makes two rows different. If another column identifies a repeat, change KEY_COLUMN_INDEX to that column's index.
- The Clean tab lost rows I typed there myself.
- Keep your own notes on another tab, or change OUTPUT_TAB in the code before you run it. Every run clears the Clean tab and writes it again.
- The run stops with "The Contacts tab needs a header row and at least one row of data."
- Put your column titles in row 1 and at least one person in row 2. The tab holds fewer than two rows.
Technical details
Names used in this script
Open these details when you want to adapt the example or look up an exact service or method name.
Show Apps Script symbols and service pages
Apps Script symbols
Service pages
Keep going
Related tasks, basics, and apps
- Task guide Move finished rows to an Archive tab
Move every row marked Done from a Tasks tab to the bottom of an Archive tab. The script deletes rows from Tasks, so test it on a copy of the file first.
- Task guide Send an email for each row in a Google Sheet
Send one email to each person listed on a Customers tab. The script marks each row Sent as it goes, so a later run sends only to rows that are not marked yet.
- Task guide Write a custom function in Google Sheets
Write your own formula for Google Sheets. This one converts Celsius to Fahrenheit, works on one cell or a whole range, and shows a clear message when a cell holds text instead of a number.
- Object reference Spreadsheet
A Spreadsheet is the whole Google Sheets file. Use SpreadsheetApp.getActiveSpreadsheet() or openById() to get it, then call getSheetByName() or getActiveSheet().
- Object reference Sheet
A Sheet is one tab inside a Google spreadsheet. Get it with getSheetByName() or getActiveSheet(). Use getLastRow() to find the last row with content, or getRange() to select cells to read.
- Object reference Range
A Range represents one cell such as A2 or a rectangular block of rows and columns in a Sheet. Use getValue() for one cell and getValues() or setValues() for a 2D array of rows.
- Basics Execution log, Logger.log, and debugging
Most Apps Script debugging is printing a value with Logger.log or console.log and reading the error line. This page shows where the output goes and how to trace a failure.
- Basics Rows, columns, and 2D arrays in Sheets scripts
A range is a rectangle of cells. getValues() returns that rectangle as an array of rows, and each row is an array of cell values.
- Google app Google Sheets
SpreadsheetApp lets a Google Sheets script read a Sheet or Range, add a custom menu or formula, and act on each row, such as sending one email per row.