An inbox that never gets tidied grows into a permanent to-do list of things that were dealt with months ago, and archiveOldGmailThreads keeps that from happening by moving anything past a defined age out of the inbox automatically on a schedule.
Manually archiving old email means scrolling back through pages of an inbox, selecting threads a handful at a time, and repeating that process regularly enough that it never actually becomes a habit for most people.
This example uses Gmail's own older_than search operator combined with an explicit label exclusion, so anything intentionally marked to keep is skipped, and loops through matching threads in batches until none remain or a safety cap on iterations is reached.
You'll finish with an inbox that keeps itself under control automatically, plus a persisted record in script properties showing exactly when the last cleanup ran and how many threads it moved.
Need this built? Hire a Google Apps Script developer →
Sheet / project setup
| Setting | Value | Purpose |
|---|---|---|
| retentionDays | 180 | How old an inbox thread must be before it's eligible for archiving |
| -label:keep | Search exclusion | Protects any thread manually labeled 'keep' from being archived |
| pageSize | 100 | Threads processed per search call, batched until the inbox is clear |
What it does
The function repeatedly searches the inbox for threads older than the retention period, excluding anything labeled 'keep', and archives every match found in each batch. Because moveThreadsToArchive removes matching threads from the inbox, each successive search naturally surfaces the next batch rather than re-finding threads already moved.
- The 'keep' label exclusion gives users an explicit way to protect specific threads from automatic archiving
- Batching in pageSize chunks keeps each individual search and archive call fast
- A maxIterations safety cap prevents a runaway loop if something unexpected keeps returning results
Prerequisites
Users who want to exempt specific threads from this cleanup need to apply a 'keep' label to them before the retention window catches up to those threads' age.
- Gmail read and modify scopes granted during authorization
- A 'keep' label created and understood by users as the opt-out mechanism
- A time-driven trigger calling archiveOldGmailThreads on a regular schedule, such as daily
Walkthrough
The while loop's exit condition is simply an empty search result, which works correctly here because archiving removes matched threads from the 'in:inbox' scope of the query - each iteration's search genuinely reflects the current, shrinking state of the inbox rather than a static snapshot taken once at the start.
maxIterations exists purely as a safety valve; at 25 iterations of 100 threads each, a single run can archive up to 2,500 threads before stopping, which comfortably covers normal daily volume while still bounding worst-case execution time.
Edge cases
A thread that receives a new message the moment before archiving runs will have its age reset for the purposes of older_than, since Gmail measures a thread's age by its most recent message, so an old thread that just got a new reply won't be archived even though most of it is old.
- Threads already outside the inbox (previously archived manually) are never matched, since the query is scoped with in:inbox
- A user unlabeling 'keep' from a thread makes it eligible for archiving on the very next scheduled run
- Extremely high email volume days could hit the maxIterations cap and leave some eligible threads for the following day's run to catch
Testing
Label a deliberately old test thread with 'keep' and confirm it survives a run, then remove the label and confirm the same thread gets archived on a subsequent run.
- Check the LAST_ARCHIVE_RUN and LAST_ARCHIVE_COUNT script properties after a run to confirm they're updating correctly
- Verify a thread that received a message earlier today is correctly excluded from archiving despite being part of an old conversation
- Test with retentionDays set very low against a test account to confirm the batching loop behaves correctly across multiple iterations
Hardening
Because this only ever grows in scope with mailbox size, keeping an eye on how many iterations a typical run actually uses helps decide whether maxIterations or pageSize needs adjusting well before a real cleanup run ever gets cut short.
- Log the iteration count on every run, not just the total archived, to catch a pattern approaching the maxIterations cap before it becomes a problem
- Add a notification email if a run hits the maxIterations cap, signaling that a shorter retention period or a second run within the same day may be needed
- Consider excluding starred threads in addition to 'keep'-labeled ones as another built-in protection
Variations
The same older_than search operator combines naturally with label-based rules from the sender-labeling example, so a specific automated sender's threads could be archived on a much shorter retention window than the general inbox.
- Apply a shorter retention window specifically to threads from known automated senders, combined with the label-gmail-by-sender example
- Archive by label instead of purely by age, for categories of email known to be safely disposable after a set period
- Send a weekly summary email listing how many threads were archived and their general senders, for visibility into inbox activity
Full code: archiveOldGmailThreads()
The function relies entirely on Gmail's own search semantics to make repeated batches converge naturally, without needing to track which specific threads it has already processed.
function archiveOldGmailThreads() {
var retentionDays = 180;
var query = 'older_than:' + retentionDays + 'd -label:keep in:inbox';
var pageSize = 100;
var totalArchived = 0;
var maxIterations = 25;
var iteration = 0;
while (iteration < maxIterations) {
var threads = GmailApp.search(query, 0, pageSize);
if (threads.length === 0) break;
GmailApp.moveThreadsToArchive(threads);
totalArchived += threads.length;
iteration++;
}
var properties = PropertiesService.getScriptProperties();
properties.setProperty('LAST_ARCHIVE_RUN', new Date().toISOString());
properties.setProperty('LAST_ARCHIVE_COUNT', String(totalArchived));
Logger.log('Archived ' + totalArchived + ' threads older than ' + retentionDays + ' days over ' + iteration + ' batches');
}- Line 3: Builds the search query combining the age threshold with an explicit exclusion for 'keep'-labeled threads.
- Line 6: Caps the number of batches processed in a single run as a safety valve against unexpected runaway behavior.
- Line 10: Searches for the next batch of eligible threads still sitting in the inbox.
- Line 11: Stops the loop as soon as a search returns no more matching threads.
- Line 13: Archives the entire batch of matched threads in one call, removing them from the inbox.
- Line 18: Persists the timestamp of this run into script properties for later reference.
- Line 22: Logs a summary showing how many threads were archived and across how many batches.
Deploy this example
- 01
Open Apps Script
In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.
- 02
Paste and save
Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.
- 03
Authorize once
Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.
- 04
Add the trigger
Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.
Before scheduling inbox archiving
- 1Retention period agreed upon and communicated to inbox users
- 2'keep' label created and its purpose explained to users who want to opt threads out
- 3Gmail authorization scopes granted during first run
- 4maxIterations and pageSize reviewed against typical daily email volume
- 5Time-driven trigger scheduled at an appropriate frequency, such as daily
- 6LAST_ARCHIVE_RUN and LAST_ARCHIVE_COUNT properties confirmed to update correctly
Frequently asked questions
No - archiving only removes a thread from the inbox view; the messages remain fully accessible under 'All Mail' and in search results, and can be moved back to the inbox manually at any time.
It's based on the timestamp of the most recent message in the thread, not the first one, so a years-old conversation that just received a new reply today is treated as new for this purpose and won't be archived.
Change the query to search a specific label instead of, or in addition to, the older_than clause, for example combining 'label:newsletters older_than:30d' to archive only newsletter threads past a shorter age threshold.
An unbounded loop risks running into Apps Script's six-minute execution limit on a mailbox with an unusually large backlog, so the cap ensures the function always terminates cleanly within a predictable amount of time, picking up any remainder on the next scheduled run.
Yes - archived threads are still fully visible and searchable under All Mail, and a user can simply move a thread back to the inbox manually at any point after it's archived.
No - the query is scoped with in:inbox, so only threads currently sitting in the inbox are considered; sent messages, drafts, and already-archived threads are never touched by this function.