Reclaim Your Gmail Inbox with Google Scripts

 · 11 min
AI-generated by GPT-4o

Inbox Zero aims to declutter your mind and remove any anxiety over unread or missed emails lingering in your inbox. Instead of answering each email as soon as it arrives or letting it “rot” in the inbox, a tickler file relegates them until they become a priority and must be reviewed. So, let’s create our own tickler file with the help of Google Apps Scripts.

Even though Gmail already has a snooze function, I find it limited in a few ways, especially for a “keyboard-only” workflow. There are advantages to it, which I will talk about in the “Conclusion” section. But learning about Google Apps Scripts is worthwhile anyway, as it can be a powerful automation tool.


What’s a Tickler File?

A tickler file, a.k.a. 43 folders system, is an organizational system using date-based storage to postpone things until that date is reached.

Generated by GPT-4o
Generated by GPT-4o

The folders hold physical documents, such as to-dos, pending bills, reminders, etc., and store them based on their required reappearance.

The name “43 folders” is based on the 31+12 folders, one for each day and month. Each day, you pick up the current folder to retrieve the tasks to be done and either do them or postpone any of them again.

In essence, it’s a simple and structured approach to snoozing tasks.

But what if you want to plan something for a more specific month or even a year? You’d have to either add more physical folders, or sort it to the closest folder and re-sort the task when it emerges.

If you add more folders, you might end up with a “365+ folders system”, which requires quite a large filing cabinet. And if you need to re-sort tasks often, you have to spend cognitive capacity to get acquainted again with the snoozed task in front, even if it might not be due.

Thankfully, a digital tickler file can have as many folders as we want!


Digital Tickler File Workflow

Mails are a ubiquitous format that often contains tasks to act upon. So, it feels quite natural to bend our mail system to our will and make it into a tickler file.

The general setup of the “Google Apps Script”-based tickler doesn’t differ much from “43 folders”. There still will be folders for days, months, and even years or more unique “dates”.

Well, it’s going to be “labels”, as Gmail doesn’t have “folders”, but you get the gist.

Any mail that needs to be postponed is labeled and removed from the inbox.

Out of sight, out of mind.

Unlike a physical system, though, an automatically triggered script will take care of re-emerging any due tasks for us behind the scenes!

Generated by GPT-4o
Generated by GPT-4o

Using ZZZ as the root node moves the tree to the bottom of the label list. Then, there are categories for days, months, year, etc., which split up further for each subsequent label:

ZZZ
 |
 +-- D
 |   +-- D1
 |   +-- ...
 |   +-- D31
 +-- M
 |   +-- M1
 |   +-- ...
 +-- ...

Using labels gives a nice and straightforward keyboard-enabled workflow. At any time we have a thread selected or are reading the thread, we can press v and just start typing the label we want and press enter.

Don’t worry, you don’t need to create all the labels by hand!

The implementation we will create together will be a little over 100 lines and does only the bare minimum to be a tickler file. There’s room to improve, which I’ll discuss at the end of the article.


Google Apps Script Project

The first step is creating a Google App Script project.

Go to https://script.google.com/ and click the big “New project” button in the top left. This automatically opens up the web-based IDE with an unnamed project.

The workflow can be broken down into three parts, which will be represented by dedicated files:

  • Tickler.gs: Handling labels and moving mails around.
  • Triggers.gs: Logic for time-based mail moving.
  • Setup.gs: Setting up the project, like label creation and triggers.

Splitting the code into multiple files isn’t actually necessary; a single file would be fine. But separating the responsibilities into smaller chunks makes the code more manageable, and three files fit nicely into the three parts we’re going to look at.

How you organize the code inside these files is also up to you. As Google Apps Script runs on V8, it supports many modern ECMAScript features. That means there are many different ways to structure your code. I’ve decided to use const arrow functions and objects, but your preferences might be different.

Part 1: The Tickler

Tickler.gs has two responsibilities:

  • Determinate a label’s name based on a prefix and a corresponding value
  • Move mails back to the inbox

The label prefixes are defined as a const object, so they’re easier to use throughout the project:

javascript
const Prefix = {
  ROOT:  'ZZZ',
  HOUR:  'H',
  DAY:   'D',
  WEEK:  'W',
  MONTH: 'M',
  YEAR:  'Y',
  NEXT:  'N'
}

A label name consists of at least the ROOT and a maximum of the ROOT plus the category and value, e.g., ZZZ/D/D12.

The NEXT prefix is a special consideration to make it simpler to label a thread for the “next day” or the “next week” and so forth.

Let’s create our first arrow function in a new object called Tickler:

javascript
const Tickler = {

  labelName: (prefix, value) => {
    if (prefix === undefined) {
      return Prefix.ROOT
    }

    if (value === undefined)  {
      return `${Prefix.ROOT}/${prefix}`
    }

    return `${Prefix.ROOT}/${prefix}/${prefix}${value}`
  }
}

By accepting undefined arguments, we won’t need multiple labelName variants, and overall, the code is simple enough to remain straightforward.

The second responsibility of Tickler is moving mails. Or, as Gmail sees it, moving “threads”.

We use the GmailApp class to get labels and move threads around:

javascript
const Tickler = {

  // ...

  moveToInbox: (labelPrefix, labelValue) => {
    try {
      // STEP 1: GET LABEL
      const labelName = Tickler.labelName(labelPrefix, labelValue)
      const label = GmailApp.getUserLabelByName(labelName)
      if (!label) {
        Logger.log(`Label '${labelName}' not found`)
        return
      }

      // STEP 2: GET ASSOCIATED THREADS
      const threads = label.getThreads()
      if (threads.length === 0) {
        return
      }

      // STEP 3: MOVE THREADS, MARK AS UNREAD
      GmailApp.moveThreadsToInbox(threads)
      GmailApp.markThreadsUnread(threads)

      // STEP 4: REMOVE THREADS FROM TICKLER LABEL
      label.removeFromThreads(threads)
    }
    catch (e) {
      const msg = `Moving '${labelName}' failed`
      Logger.log(msg)
      GmailApp.sendEmail('doe@example.com', `[TICKLER-ERROR] ${msg}`, msg)
    }
  }
}

The logic is quite simple: get a label, find its threads, move them to the inbox, mark them as unread, and finally, remove the tickler label.

The code is wrapped in a try-catch, as it sporadically fails due to “internal errors”, or other unspecified errors. Usually, the next run will catch missing mails, but some edge-cases might lead to mails remaining snoozed. Sending a custom error mail is more informative that the generic “an error occurred” one Google sends you.

Now that we have a tool to move mails, it’s time to look at the actual logic of how to trigger the Tickler.

Part 2: Triggering the Tickler

The naive approach to triggering the tickler would be checking all the labels constantly in an endless loop. However, Gmail doesn’t like to be hammered via Google Scripts constantly, and the tickler will fail eventually. That’s why a more sensible approach of doing the bare minimum makes sense by utilizing multiple time-based triggers.

To support hours, days, months, and years, we actually only need two well-defined points in time we need to trigger the processing of the different labels:

  • Hourly
  • Once a day

Hourly is needed to support the H prefix:

javascript
const processHourly = () => {
  const hour = new Date().getHours()
  Tickler.moveToInbox(Prefix.HOUR, hour)
}

Processing the labels daily naturally handles D, but could also handle M and Y, as they are both based on a specific day (1st of month, 1st of year). However, it makes sense to split up the logic for days and months/years so as not to get rate-limited.

First, let’s take a look at processing day-related threads:

javascript
const processDaily = () => {
  const now = new Date()

  // FOR CURRENT DAY
  const day = now.getDate()
  Tickler.moveToInbox(Prefix.DAY, day)

  // FROM NEXT DAY
  Tickler.moveToInbox(Prefix.NEXT, Prefix.DAY)

  // FROM NEXT WEEK
  if (now.getDay() == 1) {
    Tickler.moveToInbox(Prefix.NEXT, Prefix.WEEK)
  }
}

There are two different types of labels we need to check, marked by their comments:

  • The current label (// FOR ...)
  • Related “next” labels (// FROM ...)

“Next day” is checked unconditionally because there’s always a “next day”. But “next week” is gated behind an if to check if it’s actually Monday.

You might need to change the “next week” check if your calendar system doesn’t start weeks on Monday.

The processMonthly function works similarly to the daily processing:

javascript
const processMonthly = () => {
  const now = new Date()

  // FOR CURRENT MONTH
  Tickler.moveToInbox(Prefix.MONTH, month)

  // FROM NEXT MONTH
  const month = now.getMonth() + 1

  // FROM NEXT MONTH
  Tickler.moveToInbox(Prefix.NEXT, Prefix.MONTH)

  // CHECK YEAR IN JANUARY
  if (month === 1) {
    const year = now.getFullYear()
    // FOR CURRENT YEAR
    Tickler.moveToInbox(Prefix.YEAR, year)

    // FROM NEXT YEAR
    Tickler.moveToInbox(Prefix.NEXT, Prefix.YEAR)
  }
}

Now that we have the Tickler itself and the possibility to trigger it, let’s set up the whole thing!

Part 3: The Setup

The file Setup.gs has 2 responsibilities:

  • Creating labels
  • Setting up the time-based triggers

Label-creation is aggregated in the Label object and it can create a single label based on a prefix or a range of labels:

javascript
const Label = {

  // UPSERTS A SINGLE LABEL
  upsert: (prefix, value) => {
    const name = Tickler.labelName(prefix, value)

    let label = GmailApp.getUserLabelByName(name)
    if (!!label) {
      return
    }

    GmailApp.createLabel(name)
  },

  // UPSERTS MULTIPLE LABEL BASED A NUMERIC RANGE
  upsertRange: (prefix, from, to) => {
    for (let val = from; val <= to; val++) {
      Label.upsert(prefix, val)
    }
  } 
}

These two methods are enough to create the label tree with all its nodes:

java
const run_once_setup = () => {

  Label.upsert()

  Label.upsertRange(Prefix.HOUR, 0, 23)
  Label.upsertRange(Prefix.DAY, 1, 31)
  Label.upsertRange(Prefix.MONTH, 1, 12)

  const year = new Date().getFullYear()
  Label.upsertRange(Prefix.YEAR, year, year + 3)

  Label.upsert(Prefix.NEXT, Prefix.DAY)
  Label.upsert(Prefix.NEXT, Prefix.WEEK)
  Label.upsert(Prefix.NEXT, Prefix.MONTH)
  Label.upsert(Prefix.NEXT, Prefix.YEAR)
}

The final piece missing is setting up the triggers.

Thankfully, ScriptApp got us covered:

javascript
const run_once_setup = () => {

  // ...

  // DELETE ALL EXISTING TRIGGERS
  ScriptApp.getProjectTriggers().forEach(ScriptApp.deleteTrigger)
  
  // HOURLY TRIGGER: EVERY 10 MINUTES
  ScriptApp.newTrigger(processHourly.name)
    .timeBased()
    .everyMinutes(10)
    .create()

  // DAILY TRIGGER: AT 03:30
  ScriptApp.newTrigger(processDaily.name)
    .timeBased()
    .atHour(3)
    .nearMinute(30)
    .everyDays(1)
    .create()

  // MONTHLY TRIGGER: AT 1ST
  ScriptApp.newTrigger(processMonthly.name)
    .timeBased()
    .onMonthDay(1)
    .create()
  }
}

That’s it.

Choose run_once_setup in the toolbar and, well, run it once, and the Tickler is ready to go!


Conclusion

The custom tickler solution works quite well for my personal use case, but it has some downsides compared to other solutions, like the Gmail Snooze function.

The Snooze function treats mails a “new” when they come back, with all that entails (top of the list, notifications, etc.), whereas the Tickler only moves threads around and they sort themselves to their original position on the timeline. If a lot is going on in the Inbox already, you might overlook the “new but not really” thread, so “Inbox Zero” is kind of a must-have.

The next problem is exact snoozing. With the current setup, only a single label is respected, so you must choose between H, D, M, or Y. That means you can schedule a thread for the 23rd of August at 16:00. However, the logic could be adapted to check for multiple labels.

Like looking for special “dates”.

I’m a workaholic, so I spend most weekends at the office. That’s why my personal variant of the Tickler also has a “next weekend” option via a NWE label.

It’s a simple additional check for Saturday in the daily processing. But it highlights how straightforward it is to customize the Tickler.

There are more possibilities for improvements, like defining “next day” as the next work-day by checking CalendarApp if there’s something in the holiday calendar.

Or, based on the Tickler code, you could build a custom sorter script. For example, one of my other scripts checks mails under a specific label and resorts them based on the subject line. Combined with a filter that auto-labels mails that are sent to a specific address, like ben+sort@..., I can send any idea to my personal “filing cabinet” built directly into GMail.

New article idea? I just sent a mail with the subject “Article: My best idea ever” to myself and the script sorts it away under the “ARTICLE-IDEAS” label until I got time to actually work on it.

What will you build with Google App Script?


Resources