Categories
Automation Google Workspace

How to Automatically Delete Old Gmail Messages with Google Apps Script

Last updated: October 17, 2026

Updated with a cleaner version of the script (one settings block at the top, plain-text report subjects) and answers to the questions people asked.

I keep seven years of email and delete anything older. For years that meant a monthly chore: search Gmail for something like before:2018/11/01, select everything, delete, repeat. It only took 5 to 10 minutes, but it annoyed me enough to automate it.

Here is the Google Apps Script I use. It runs on the 1st of every month, moves email older than seven years to Trash, and emails me a short report. It works with Google Workspace and personal Gmail accounts.

What the script does

  1. Works out the date seven years ago (or whatever retention you set)
  2. Searches Gmail for threads older than that date
  3. Moves them to Trash in batches of 100
  4. Emails you a report with the counts
  5. Runs itself every month on a time-driven trigger

Step 1: Create the script

Go to script.google.com, click New project, name it “Email Pruner”, delete the starter code, and paste this in:

// Email Pruner: moves Gmail threads older than RETENTION_YEARS to Trash.
const RETENTION_YEARS = 7;
const BATCH_SIZE = 100;   // GmailApp can trash at most 100 threads per call
const EXTRA_QUERY = '';   // optional, e.g. '-is:starred -has:attachment'

function getSearch_() {
  const cutoffDate = new Date();
  cutoffDate.setFullYear(cutoffDate.getFullYear() - RETENTION_YEARS);
  const cutoff = Utilities.formatDate(cutoffDate, Session.getScriptTimeZone(), 'yyyy/MM/dd');
  return { cutoff: cutoff, query: ('before:' + cutoff + ' ' + EXTRA_QUERY).trim() };
}

function pruneOldEmails() {
  const search = getSearch_();
  const startTime = Date.now();
  let threadCount = 0;
  let messageCount = 0;

  try {
    let threads = GmailApp.search(search.query, 0, BATCH_SIZE);
    while (threads.length > 0) {
      threads.forEach(function (thread) {
        messageCount += thread.getMessageCount();
      });
      GmailApp.moveThreadsToTrash(threads);
      threadCount += threads.length;

      // Apps Script stops a run at 6 minutes, so quit at 5 and finish next time
      if (Date.now() - startTime > 5 * 60 * 1000) {
        sendReport_('PARTIAL', threadCount, messageCount, search.cutoff,
          'Stopped early to stay under the time limit. The next run will continue.');
        return;
      }
      threads = GmailApp.search(search.query, 0, BATCH_SIZE);
    }
    sendReport_('SUCCESS', threadCount, messageCount, search.cutoff, '');
  } catch (error) {
    sendReport_('ERROR', threadCount, messageCount, search.cutoff, error.toString());
  }
}

// Run this first: it only counts, it never deletes
function testPruneOldEmails() {
  const search = getSearch_();
  const threads = GmailApp.search(search.query, 0, 500);
  let messageCount = 0;
  threads.forEach(function (thread) {
    messageCount += thread.getMessageCount();
  });
  const note = threads.length === 500 ? ' (500 is the search cap, so there are probably more)' : '';
  Logger.log('Query: ' + search.query);
  Logger.log('Found ' + threads.length + ' threads, about ' + messageCount + ' messages' + note);
  GmailApp.sendEmail(Session.getActiveUser().getEmail(),
    'Test: Email Pruning Preview',
    'Nothing was deleted.\n\nQuery: ' + search.query +
    '\nThreads: ' + threads.length + note +
    '\nMessages: about ' + messageCount);
}

function sendReport_(status, threads, messages, cutoff, details) {
  const subject = 'Email Pruning ' + status + ': ' + threads + ' threads moved to Trash';
  let body = 'Email Pruning Report\n\n' +
    'Status: ' + status + '\n' +
    'Cutoff date: ' + cutoff + '\n' +
    'Threads moved to Trash: ' + threads + '\n' +
    'Approximate messages: ' + messages + '\n' +
    'Run finished: ' + new Date() + '\n';
  if (details) {
    body += '\nDetails: ' + details + '\n';
  }
  body += '\nGmail permanently deletes items in Trash after 30 days.';
  GmailApp.sendEmail(Session.getActiveUser().getEmail(), subject, body);
}

Press Ctrl+S (Cmd+S on a Mac) or click the save icon to save the project.

Step 2: Test it before you delete anything

  1. In the function dropdown next to the Debug button, choose testPruneOldEmails
  2. Click Run
  3. Authorize it the first time: click Review permissions, pick your account, click Advanced, then “Go to Email Pruner (unsafe)”, then Allow. The warning appears because this is your own unpublished script.
  4. Wait a minute or two, then check your inbox for “Test: Email Pruning Preview”

The preview tells you how many threads match without deleting anything. Searching seven years of my email took about 90 seconds. Gmail search returns at most 500 threads at a time, so if you see exactly 500, you have more than that.

Step 3: Run the real cleanup

  1. Choose pruneOldEmails in the function dropdown
  2. Click Run
  3. Wait for the completion email

Everything older than your cutoff moves to Trash. Gmail keeps Trash for 30 days, so you have a month to rescue anything you didn’t mean to delete.

Step 4: Schedule it monthly

  1. Click Triggers (the alarm clock icon) in the left sidebar
  2. Click Add Trigger in the bottom right
  3. Set: function pruneOldEmails, deployment Head, event source Time-driven, type Month timer, day 1, and an off-hours time (I use 2am to 3am)
  4. Click Save

That’s it. On the 1st of each month the script runs and you get a report.

Customizing it

All the settings live in the first four lines, so you only edit one place.

  • Keep a different number of years: change RETENTION_YEARS. For five years, set it to 5.
  • Protect some email: put Gmail search operators in EXTRA_QUERY, for example '-is:starred', '-label:taxes' or '-has:attachment'. Combine them with spaces.
  • Simpler query: Gmail also understands older_than:7y. The date version is easier to check in the report, which is why I use it.
  • Timeouts on a huge mailbox: the 5-minute guard handles this. You can also run pruneOldEmails by hand a few times for the first big cleanup.

Notes for Google Workspace users

  • Admin retention rules: if your admin uses Google Vault retention rules, they win. Ask before you automate deletion on a work account.
  • Trash: trashed mail is permanently deleted after 30 days.
  • Limits: Apps Script runs are capped at 6 minutes, and there are daily quotas. Google lists them on the Apps Script quotas page.

Troubleshooting

  • The test seems to hang: it’s searching years of mail. Give it a couple of minutes, then open Executions in the left sidebar to see the log.
  • Authorization errors: make sure you authorize the same account whose mail you want to clean.
  • No report email: check the Executions page for an error, and look in Spam the first time.

Was it worth it?

Setup took about 10 minutes. It saves me roughly an hour a year, which is not a lot, but I never think about it anymore, and the monthly report is a nice reminder that it’s still working.

FAQ

Does this work with a personal Gmail account?

Yes. Apps Script runs on personal Google accounts and Google Workspace accounts. Workspace accounts get higher daily quotas, and a Workspace admin may have retention rules of their own, so check with them first.

Can I get deleted emails back?

For 30 days, yes. The script only moves threads to Trash, and Gmail permanently deletes Trash after 30 days. Open Trash, select the messages, and move them back to your inbox.

Can Gmail delete old emails automatically without a script?

Not for a personal account. Gmail filters only act on new mail as it arrives. Google Workspace admins can set retention rules in Google Vault that purge mail after a set period, which is the better option for a company.

Why did I get a PARTIAL report?

Apps Script stops any single run after 6 minutes. The script quits at 5 minutes, emails you a partial report, and the next monthly run picks up where it left off. On a huge first cleanup you can just run it by hand a few times.

Will it delete starred or important emails?

By default it moves every thread older than the cutoff. Add -is:starred or -is:important to EXTRA_QUERY to protect them.

Related posts