Inbox Zero with Google Apps Script
3 Sep 2025 | Edited: 4 Sep 2025Recently I was watching a Notion Mail review–I really dislike Notion but I think they nailed their calendar and mail apps (they bought them)–and was enamored by the creator’s near-empty inbox. As I watched him triage what little emails he had, I had a revelation: this is “inbox zero”. The phrase that is always thrown around productivity circles as “the number 1 boost”. I already had 0 unread emails, something that I meticulously achieved through dedicated swiping-right to “mark as read” on my phone, so I thought my email journey was complete. However, in that moment I realized the power of inbox zero: total control over my inbox by using it like a physical mailbox. You never leave mail in a physical mailbox, you triage its contents, keeping what you need, taking action on important mail, and trashing the junk. In the same way, every email that enters your email inbox should be triaged: actioned, archived, or deleted. However, to make this system work, you first need an pristine, empty inbox. I figure any emails older than two weeks have either been actioned upon already or probably aren’t worth actioning on now, so they should all be archived. Yes, if you need to find a particular email you can scroll through the archive folder or just use the search bar like you were going to do anyway.
Getting to inbox zero can be done manually in Gmail by searching in:inbox older_than:14d
and archiving the first 100, refreshing, archiving the next 100, and repeating. However, we can leverage Google Apps Script to automate this process. In Google Apps Script, create a new project with any name, I called mine “Archive Emails”. Delete the code in the .gs script that is created by default and paste the code below:
function archiveEmails() {
const QUERY = 'in:inbox older_than:14d'; // test this query in Gmail before running the script
const COUNT = 100;
const threads = GmailApp.search(QUERY);
// Delete triggers when no more emails need to be archived
if (threads.length === 0) {
console.log('No emails found');
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(t => {
if (t.getHandlerFunction() === 'archiveEmails') ScriptApp.deleteTrigger(t);
});
console.log(`Deleted ${triggers.length} script triggers`)
return;
}
console.log(`Found ${threads.length} emails`);
for (let i = 0; i < threads.length; i += COUNT) {
let batch = threads.slice(i, i + COUNT);
GmailApp.moveThreadsToArchive(batch);
console.log(`Archived ${batch.length} emails`);
}
// Schedule script to run every minute
const triggers = ScriptApp.getProjectTriggers();
if (triggers.length === 0) {
ScriptApp.newTrigger('archiveEmails')
.timeBased()
.everyMinutes(5) // give the prior execution time to complete
.create();
console.log('New script trigger created');
}
return;
}
Once you have that script inside your GAS project, you can click the run button and let the magic begin.
There are some interesting things this little script does that greatly improve QoL. According to ChatGPT (I know, very reliable), consumer, or unpaid, Gmail accounts are limited to ~6 minutes max execution time per function. This means our script will error-out for exceeding max execution time before it can finish archiving all the emails we want from our inbox. To prevent this, I have it create a new app trigger at the end that calls itself every 5 minutes, but only create this if no other triggers for the project already exist, otherwise the number of triggers would grow exponentially. I also added a guard clause at the start of the function to check if there are any remaining emails that need to be archived, if not, the function can end and delete the trigger it created on the first run so that it doesn’t continue trying to archive emails in the background indefinitely.
If you want to view the triggered script logs, click on the “Triggers” button on the left in the GAS project and click on “Executions” from the 3-dot menu on the trigger. If you want to delete all emails older than 2 years, say you need more storage in Google Drive, just adjust the query to be 'older_than:2y'
and change the function call in the for loop from moveThreadsToArchive
to moveThreadsToTrash
. Then, simply empty the trash folder once the script deletes its trigger.
I think this solution is pretty clever and enabled me to get to inbox zero. Now remains the hard part: having the discipline to triage every new email.