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:

  1. Copies the entire row to the destination tab
  2. 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:

Code
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":

Code
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:

  1. Add a few rows to the source tab
  2. Change the status cell of one row to your trigger value
  3. The row should disappear from the source tab and appear in the destination tab

If nothing happens, check:

  • The sheet name in SOURCE_SHEET_NAME matches exactly (case-sensitive)
  • The WATCH_COLUMN number matches the column you changed
  • The TRIGGER_VALUE matches 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:

Code
  // 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:

Code
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:

  1. Add a undo column — before deleting, write "moved to Done" in a helper column so you can find the row if needed
  2. 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

Don't want to set this up yourself? Describe your tab structure and it'll be configured for your sheet. Get it installed