The most common version of this request: you have an active task list and a "Done" archive. When someone marks a task complete, you want it to move out of the active list and into the archive — automatically, without anyone having to cut and paste.
Apps Script handles this with an onEdit trigger. When a cell changes to a value you define, the row moves to the destination tab.
---
What This Does
When a cell in a specific column changes to a specific value (like "Done"), the script:
- Copies the entire row to the destination tab
- Deletes the row from the source tab
The row disappears from the active list and appears in the archive. No manual intervention.
---
Setup
Open your Google Sheet. Go to Extensions > Apps Script, delete the placeholder, and paste:
function onEdit(e) {
// -- SETTINGS -- adjust these to match your sheet
var SOURCE_SHEET_NAME = "Tasks"; // Tab where rows currently live
var DEST_SHEET_NAME = "Done"; // Tab where rows should move to
var WATCH_COLUMN = 5; // Column to watch (5 = column E)
var TRIGGER_VALUE = "Done"; // Value that triggers the move
// Only act on the source tab
var sheet = e.source.getActiveSheet();
if (sheet.getName() !== SOURCE_SHEET_NAME) return;
// Only act on the trigger column
if (e.range.getColumn() !== WATCH_COLUMN) return;
// Only act if the new value matches
if (e.value !== TRIGGER_VALUE) return;
var row = e.range.getRow();
// Don't move the header row
if (row <= 1) return;
var ss = e.source;
var destSheet = ss.getSheetByName(DEST_SHEET_NAME);
// Create destination tab if it doesn't exist
if (!destSheet) {
destSheet = ss.insertSheet(DEST_SHEET_NAME);
}
// Get the full row data
var lastCol = sheet.getLastColumn();
var rowData = sheet.getRange(row, 1, 1, lastCol).getValues();
// Append to destination tab
destSheet.appendRow(rowData[0]);
// Delete from source tab
sheet.deleteRow(row);
}Save the script. Changes to the settings at the top of the script control the behavior.
---
How to Configure It
| Setting | What to change | |---------|----------------| | SOURCE_SHEET_NAME | Name of the tab where rows start | | DEST_SHEET_NAME | Name of the tab where rows should go | | WATCH_COLUMN | Column number to watch (A=1, B=2, C=3, D=4, E=5...) | | TRIGGER_VALUE | The exact value that triggers the move |
Example: Move rows to "Archive" when column C says "Cancelled":
var SOURCE_SHEET_NAME = "Orders";
var DEST_SHEET_NAME = "Archive";
var WATCH_COLUMN = 3;
var TRIGGER_VALUE = "Cancelled";---
Testing the Script
Before relying on this in a real sheet, test it:
- Add a few rows to the source tab
- Change the status cell of one row to your trigger value
- The row should disappear from the source tab and appear in the destination tab
If nothing happens, check:
- The sheet name in
SOURCE_SHEET_NAMEmatches exactly (case-sensitive) - The
WATCH_COLUMNnumber matches the column you changed - The
TRIGGER_VALUEmatches exactly (case-sensitive — "done" won't match "Done")
---
Adding a Timestamp When a Row Moves
To record when each row was archived, add a timestamp in the last column when the row is moved:
// Add timestamp before appending
var timestamp = new Date();
rowData[0].push(timestamp);
destSheet.appendRow(rowData[0]);Add this just before the destSheet.appendRow(rowData[0]); line. The timestamp appears as the last column in the destination tab.
---
Moving to Multiple Destinations Based on Value
To route rows to different tabs based on which status is selected:
function onEdit(e) {
var sheet = e.source.getActiveSheet();
if (sheet.getName() !== "Tasks") return;
if (e.range.getColumn() !== 5) return;
var destName = null;
if (e.value === "Done") destName = "Completed";
if (e.value === "Cancelled") destName = "Cancelled";
if (!destName) return;
var row = e.range.getRow();
if (row <= 1) return;
var ss = e.source;
var destSheet = ss.getSheetByName(destName) || ss.insertSheet(destName);
var lastCol = sheet.getLastColumn();
var rowData = sheet.getRange(row, 1, 1, lastCol).getValues();
destSheet.appendRow(rowData[0]);
sheet.deleteRow(row);
}---
Important Limitation: Simple Triggers Can't Be Undone
onEdit is a simple trigger — it fires immediately when the cell changes. There's no confirmation step. If someone accidentally selects "Done," the row moves before they can undo it.
Two ways to mitigate this:
- Add a undo column — before deleting, write "moved to Done" in a helper column so you can find the row if needed
- Move instead of delete — the script already copies before deleting, so the data is in the Done tab. Nothing is lost.
---
Common Problems
Nothing happens when I change the value
onEdit simple triggers don't require any setup — they fire automatically just from the function name. But they only fire for manual edits, not for values set by formulas or other scripts. If the status is being set by a script (not by hand), use an installable trigger instead of onEdit.
The wrong row moved
Check WATCH_COLUMN. Column E is 5, column F is 6, etc. Count from A=1.
Rows pile up without headers in the Done tab
The script copies rows but not the header. Open the Done tab and manually add the header row in row 1. Future moved rows will append below it.
---
What to Build on Top of This
- How to Create a Project Tracker in Google Sheets — add this script to auto-archive completed tasks
- Apps Script for Beginners — if this is your first script, start with the basics
- How to Send Slack Notifications From Google Sheets — combine with this script to post to Slack when a task is marked Done
Don't want to set this up yourself? Describe your tab structure and it'll be configured for your sheet. Get it installed