Reclaim Your Gmail Inbox with Google Scripts
![](/covers/2024/2024-06-gmail-tickler.webp)
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.
Table of Contents
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](/images/2024/2024-06-gmail-tickler-filing-cabinet.webp)
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](/images/2024/2024-06-gmail-tickler-robot.webp)
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:
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
:
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:
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:
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:
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:
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:
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:
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:
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
- Tickler Code Repo (github.com)
- Google Apps Script quickstart (developers.google.com)
- Google Apps Script: A Beginner’s Guide (benlcollins.com)