Task guide
Move finished rows to an Archive tab
Rows with the status Done are copied to the bottom of an Archive tab and then removed from Tasks. This one deletes rows, so test it on a copy of the file first.
1 · Prepare
Put these pieces in place
- Make a copy of your spreadsheet to test with. Choose File, then Make a copy.
- In the copy, create a tab named Tasks with headers in row 1 and the status in column C.
- Mark one or two test rows as Done. You do not need to create the Archive tab yourself.
Change these values
- Source tab
TasksThis must match the tab that rows are moved out of.
- Archive tab
ArchiveThe script creates this tab with the same header row when it does not exist yet.
- Status word
DoneOnly rows whose status cell holds exactly this word are moved. The check is case sensitive.
- Status column index
2Column C is index 2. Counting starts at 0, so column A is index 0 and column B is index 1.
2 · Review before running
What the script can access and change
Permissions you approve
- Read, update, and delete rows in this Google Sheet.
- Create a new tab in this spreadsheet when the Archive tab does not exist yet.
Effects of one run
- Copies every row whose column C says Done to the bottom of the Archive tab, keeping all of the columns.
- Deletes those same rows from the Tasks tab.
- Deletes from the bottom up, so the row numbers of the rows still waiting stay correct while the script works.
- Leaves rows with any other status where they are, and never changes rows already sitting in Archive.
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 = 'Tasks'; // This must match the tab that rows are moved out of.
const ARCHIVE_TAB = 'Archive'; // The script creates this tab with the same header row when it does not exist yet.
const STATUS_WORD = 'Done'; // Only rows whose status cell holds exactly this word are moved. The check is case sensitive.
const STATUS_COLUMN_INDEX = 2; // Column C is index 2. Counting starts at 0, so column A is index 0 and column B is index 1.
function archiveCompletedRows() {
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 firstDataRow = 2;
const lastRow = source.getLastRow();
const lastColumn = source.getLastColumn();
if (lastRow < firstDataRow) return;
const rows = source.getRange(firstDataRow, 1, lastRow - firstDataRow + 1, lastColumn).getValues();
/** @type {unknown[][]} */
const rowsToMove = [];
/** @type {number[]} */
const rowNumbersToDelete = [];
rows.forEach((row, index) => {
if (String(row[STATUS_COLUMN_INDEX]).trim() !== STATUS_WORD) return;
rowsToMove.push(row);
rowNumbersToDelete.push(firstDataRow + index);
});
if (rowsToMove.length === 0) {
Logger.log('No row had the status ' + STATUS_WORD + '. Nothing was moved.');
return;
}
const archive = spreadsheet.getSheetByName(ARCHIVE_TAB) || spreadsheet.insertSheet(ARCHIVE_TAB);
if (archive.getLastRow() === 0) {
archive.getRange(1, 1, 1, lastColumn).setValues(source.getRange(1, 1, 1, lastColumn).getValues());
}
archive.getRange(archive.getLastRow() + 1, 1, rowsToMove.length, lastColumn).setValues(rowsToMove);
SpreadsheetApp.flush();
rowNumbersToDelete.reverse().forEach((rowNumber) => source.deleteRow(rowNumber));
Logger.log('Rows moved to ' + ARCHIVE_TAB + ': ' + rowsToMove.length + '.');
} 4 · Run safely
Follow these steps in order
Work on a copy of the spreadsheet
This script deletes rows. Choose File, then Make a copy, and do your first run in the copy. Move to the real file only when you are happy with the result.
Set up the test data
In the copy, name the tab Tasks, keep the headers in row 1, and mark two rows as Done in column C. Write down how many rows the tab has so you can check the count afterwards.
Paste the function and save
Open Extensions, choose Apps Script, paste the function, and save it.
Run it and compare both tabs
Choose archiveCompletedRows and run it. Approve access when Google asks. The Done rows should sit at the bottom of the new Archive tab and be gone from Tasks, and the row counts should add up. The log line reads "Rows moved to Archive: 2."
Use it on the real file
Once the copy behaves, paste the same function into the real spreadsheet and run it there. Running it again later moves only the rows marked Done since the last run.
5 · Confirm
How to tell it worked
The Archive tab ends with the moved rows below a matching header, the Tasks tab no longer lists them, and the execution log shows a line such as "Rows moved to Archive: 2."
If the result is different
Troubleshooting
- A row that says Done stayed on the Tasks tab.
- The check trims spaces but is case sensitive, so done, DONE, and Done! are not a match. Retype the word Done in column C, or change the word in the code to the one you use.
- The wrong rows disappeared from Tasks.
- This happens when rows are deleted from the top down, because every delete shifts the rows below it up by one. Keep the reverse() call so the deleting starts at the bottom. To recover, press Ctrl+Z in the spreadsheet, or use File, then Version history, then See version history, and restore the version from before the run.
- The script says "Add a Sheet tab named Tasks before running this function."
- Rename your tab to Tasks, or change the name in the code and in the editable values.
- The Archive tab has data but no header.
- The header is copied only when the Archive tab is completely empty. Insert a new first row in Archive, then copy the header from Tasks into it. This leaves the archived rows in place below the header.
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 Copy unique rows to a clean tab
The script reads your list, keeps the first row for each email address, and rewrites a tab named Clean. The source tab is never changed.
- Task guide Add a timestamp when a row changes
Change a status cell and the script stamps the same row with the current date and time. There is nothing to install beyond saving the script.
- Task guide Send an email for each row in a Google Sheet
Use one row per person, then let the script send only the rows that have not already been marked as sent.
- 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. Reach it with getSheetByName() or getActiveSheet(), then inspect getLastRow() or read cells with getRange().
- 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 Rows, columns, and 2D arrays in Sheets scripts
A range is a rectangle of cells. getValues() hands you that rectangle as an array of rows, and each row is an array of cell values.
- Basics Google Apps Script quotas and limits
Google caps how long one run may take and how much your scripts may do in a day. Knowing the caps changes how you write anything that touches a lot of rows.
- Google app Google Sheets
SpreadsheetApp lets a Google Sheets script read a Sheet or Range, add a custom menu or formula, and run repeatable actions across rows.