How to Build ADHD Lifehack Tools with Python, Google Sheets, and Twilio SMS

November 15, 2022
Written by
Nick Piegari
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Mia Adjei
Twilion

How to Build ADHD Lifehack Tools with Python, Google Sheets, and Twilio SMS

I can say from experience that ADHD adds several layers of complexity to life on an already chaotic and distracting planet, and while we need all the help we can get, it’s often difficult to find tools that you stay in the habit of using and that actually help.

By building your own, not only will you be able to have a tool built custom for your brain, but the satisfaction of building something may encourage you to keep using and optimizing it!

In this tutorial, we'll be building three helpful tools:

  • An SMS task-inbox-and-notetaking app
  • A die roll app for moments of indecision
  • A timer that texts you when time is up

The theme of the project examples in this post are all about getting things out of your head, and getting yourself out of your own head, two very common challenges for ADHD brains.

How will these apps work?

All the functionality of this app will be handled by a handful of Python scripts running on a server of your choice (a newer Raspberry Pi model would do the job nicely), Google Sheets as a database of sorts, and Twilio SMS.

Prerequisites

You'll need...

Components of this project

Google Sheets

Because this project requires periodic storage and retrieval, we'll need some sort of database.

Since this is a relatively simple app, in lieu of a more robust solution like MySQL or SQLite, we'll be using Google Sheets as our "database" of sorts.

The biggest advantage of Google Sheets, other than its ease of use, is that it's inherently visual.  You can view, edit, sort, and quickly scan all your data without needing to write your own front end from scratch or install a third-party tool.

Visibility is important for ADHD brains!  In my case, if important information is difficult to visualize or quickly process, it might as well not exist.

Python

Python appeals to my brain immensely, in no small part because it's just so darn flexible and forgiving.

Each individual "app" will be a separate .py script, called from a main script which receives, routes, and returns data to Twilio SMS itself.

Twilio SMS

Twilio SMS will be our gateway for using the tools we'll build in this project.  We'll use it in two different ways:

  1. With ngrok, for receiving incoming webhooks from SMS when a message is received.
  2. Using the Twilio SMS REST API.  When responding immediately to an incoming message, we can simply include the content of the message as our response to the webhook.  However, some components of this project send messages asynchronously, separate from any given incoming message.  This is where the REST API comes in.

Set up your project

Make a new folder

Let’s make a new folder to contain our project files.  I’ll be developing this project in /home/nick/adhd-lifehacks.

mkdir adhd-lifehacks
cd adhd-lifehacks

Install your dependencies

Next, create a Python virtual environment where you can install the dependencies for this project. If you are working in a Mac or Unix environment, run the following commands:

python3 -m venv venv
source venv/bin/activate

If you are working in a Windows environment, run the commands below instead:

python -m venv venv
venv\Scripts\activate

We'll need the following dependencies for our project, which can be installed by entering the following in your command prompt:

pip3 install flask gspread oauth2client twilio
  • flask is a modern, powerful web application framework, which we’ll be using to build the back end.
  • gspread is a handy Python API for manipulating Google Sheets.
  • oauth2client handles account credentials so that we can securely access our Sheets.
  • twilio is a helper library for using the Twilio platform with Python!

Set up your server

We’ll be using ngrok as a straightforward way of opening our app to the public web, which is necessary for Twilio webhooks.

Create a new free account by following the steps here: https://dashboard.ngrok.com/signup.

Next, download the ngrok executable and unzip it.

To connect to your ngrok account, specify your token, displayed on the login page:

ngrok config add-authtoken [YOUR-TOKEN-HERE]

Finally, check to see whether ngrok works:

ngrok http 5000

You should see a screen that looks something like this:

ngrok log statements, showing connection details

You can verify that it worked by navigating to the https forwarding URL you see on your screen; it will be unique to your instance.

Following a prompt reminding you that you’re using the free tier of ngrok (we’ll address that later), you’ll see a screen like this:

"Connection refused" ngrok error message

The error is simply because we haven’t written or run any code yet!  Let’s get to that by starting out with a small test.

A small test

Let’s fire up our favorite code editor and create a new Python file called test.py

#!/usr/bin/python3

from flask import Flask

app = Flask(__name__)

@app.route("/")
def test():
    return "<h1>Hello world!</h1>"

To point Flask to this file, let’s open a new terminal window, navigate to the project directory, activate the virtual environment, and specify some environment variables:

export FLASK_APP=test.py
export FLASK_DEBUG=True

Now, start Flask with the following command.

flask run

With ngrok still running in the original window (type ngrok http 5000 to restart it if you closed it before), let’s try the forwarding URL again.

"Hello world!" ngrok success message

Voila!

Connect to Google Sheets

Now it's time to get connected to Google Sheets.  This is a slightly complicated process, but you'll only need to do this once!

1. Create a new project in Google APIs & Services

 

2. Add the Google Drive and Sheets APIs to your project

  • Make sure your new project is selected in the drop-down menu.
  • Click on Library on the left-hand menu.
  • Search for "drive", then click the Google Drive API.
  • Click Enable.
  • Follow these steps to add the Sheets API.

3. Create credentials

  • Click Credentials, and then click Create Credentials.
  • Choose "Service Account"; our app is a robot that won't require a human to sign in.

4. Go through the credential configuration steps

  • Choose a name, then click Create and Continue.
  • Choose the Project -> Editor role to allow our app full editing capabilities.
  • The final step can be skipped.

5. Grab a private key file

  • Select your new service account.
  • Navigate to the Keys tab.
  • Click Add Key -> Create New Key.
  • The default JSON option is what we want.  Click Create.
  • A new private key file will be downloaded to your computer.

6. Move your secrets file to your project folder

  • Rename the file to client-secrets.json (or whatever you prefer).
  • If planning to track this project with Git or similar, be sure to add a line containing “client-secrets.json” to .gitignore (or the equivalent action in your version tracking system of choice).

7. Invite your "user" to be an editor of your Sheet

  • Open the client-secrets.json file in your text editor of choice.
  • Find the "client_email" field and copy it.
  • Open Google Sheets and make a new spreadsheet called "Todo".  This will be used for our first app.
  • Click Invite, then invite the email address you just copied.  Doing this will grant editor permissions to our scripts the same way you'd invite a human user.

Let's see if it worked

Phew! That was many steps. After all that, how can we be sure it all worked correctly?

Let's go back to our test.py script and add a few more lines of code.

#!/usr/bin/python3

from flask import Flask, request
import gspread
from oauth2client.service_account import ServiceAccountCredentials

scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
creds = ServiceAccountCredentials.from_json_keyfile_name('client-secrets.json', scope)
client = gspread.authorize(creds)

app = Flask(__name__)

@app.route("/", methods=['GET', 'POST'])
def test():
    sheet = client.open("Todo").sheet1
    body = request.form.get("Body")

    sheet.append_row([body])
    return body

Now, every request you send to the app under the Body field will be logged to the table!  Neat, huh?

Configure Twilio SMS

Almost to the finish line!  Once this is set up, we'll have a robust foundation for all sorts of SMS-powered apps, not just those of the ADHD lifehack variety.

Add a Twilio phone number

First, log into your Twilio account (or create one if you don't already have one).

Navigate to Phone Numbers > Manage > Buy a number:

Twilio dashboard with arrow pointing to "Buy a number" option

You can choose any number you want, provided it supports SMS.

"Buy a number" options; SMS is checked

Configure your webhook

Next, click on the phone number you just created.

List of all active numbers with arrow indicating the one we"ll be using

Scroll down to Messaging.

Under "A Message Comes In"...

  • Choose "Webhook" as our message type.
  • Enter your ngrok forwarding URL, as discussed earlier.
  • Choose "HTTP POST".

Section of configuration where you specify the ngrok URL

Click the Save button when finished.

Now, when you text this number, it shows up in the table!

Sample SMS messages sent to app

Sample SMS images appearing in spreadsheet

Pretty cool, huh?  :)

Now let's move on to the fun stuff!

Twilio secrets

For the purpose of asynchronously sending outgoing SMS messages (as opposed to immediately responding to incoming webhooks), we'll need to grab our account SID and auth token.

These can be found on the front page of your console.  Remember to keep them secret!

Account SID and Auth Token in Twilio Console

Back on our server, let's make a new file in the same directory as client-secrets.json.  We'll call it twilio-secrets.json, and it should look something like this:

{"twilio-sid": "[PASTE SID HERE]",
"twilio-token": "[PASTE TOKEN HERE]",
"from": "[NEW TWILIO NUMBER GOES HERE]",
"to": "[YOUR PERSONAL PHONE NUMBER GOES HERE]"}

Replace the placeholder text above with your own values.

App 1:  Todo Inbox

Thanks to the work we put in building our foundation, surprise surprise, our first app is basically already finished!

In keeping with the theme of getting things out of our brain and into a more concrete form, our first app is a simple inbox system!

The concept:  when an idea or task pops into your brain, instead of the thought vanishing away into the ether, simply text your thought to your Twilio number and it'll log it in the Todo sheet you just created.

Build a main script to act as a hub for your apps

Let's make a new file called lifehack.py.  We'll tell Twilio SMS to send incoming messages to this script.  Once received, lifehack.py will send the request to the appropriate app based on a keyword at the beginning of the message.

We'll add our apps to appList as we create them, starting with a test function.

#!/usr/bin/python3

from flask import Flask, request, make_response

def test(body):
    return "Test successful!"

# Define our apps here
appList = {'Test': test}

app = Flask(__name__)

@app.route("/", methods=['GET', 'POST'])
def hello_world():
    print (request.form.get("Body"))

    appRequest = request.form.get("Body").split(' ')[0]

    # See if the app request is in the app list
    appFound = False
    for appName, appHandler in appList.items():
        if appRequest == appName:
            appFound = True
            resp = make_response(appHandler(request.form.get("Body")), 200)

    if appFound == False:
        resp = make_response("App not found!", 404)

    resp.mimetype = "text/plain"      
    return resp

Next, stop your Flask server and change your environment variable to point to the new file before restarting Flask…

export FLASK_APP=lifehack.py
flask run

Now, when you text the word 'Test' to your number, you should see this:

"Test successful!" response message

Time for todo.py

Moving on, let's make a new file called todo.py.

#!/usr/bin/python3

import gspread, datetime
from oauth2client.service_account import ServiceAccountCredentials

def todo(body):
    scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
    creds = ServiceAccountCredentials.from_json_keyfile_name('client-secrets.json', scope)
    client = gspread.authorize(creds)

    sheet = client.open("Todo").sheet1

    # Timestamp our entry
    dt = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
    toAdd = [dt]

    # Add a pipe | symbol to split the message into multiple columns.
    # Can be useful as a means of categorizing your entries.
    for field in body[5:].split('|'):
        toAdd.append(field)

    sheet.append_row(toAdd)

    return "Logged: {message}".format(message = body)

NOTE: Remember to change the path of your keyfile!  (We'll move this into a separate config file later so it's not hard-coded.)

We'll need to add this new app to lifehack.py

#!/usr/bin/python3

from flask import Flask, request, make_response
from todo import *  # <—-- NEW APP!

def test(body):
    return "Test successful!"

# Define our apps here
appList = {'Todo': todo,  # <—-- NEW APP!
          'Test': test}

app = Flask(__name__)

@app.route("/")
def hello_world():
    print (request.form.get("Body"))

    appRequest = request.form.get("Body").split(' ')[0]

    # See if the app request is in the app list
    appFound = False
    for appName, appHandler in appList.items():
        if appRequest == appName:
            appFound = True
            resp = make_response(appHandler(request.form.get("Body")), 200)

    if appFound == False:
        resp = make_response("App not found!", 404)

    resp.mimetype = "text/plain"  
    return resp

Now, when you preface a message with Todo, followed by a message of your choice (where a pipe | symbol will indicate a column split, for convenient and flexible categorization), you should see this:

Sample todo dialogue between user and app

These entries should also appear in your Todo sheet:

Sample entries in Todo sheet

The benefit of having category columns is that you can conveniently turn a mess like this…

Sample todo entries with categories, unsorted

... into this, simply by sorting alphabetically by category!

Sample todo entries with categories, sorted by category

This illustrates one of my favorite things about using Google Sheets as your database:  you already have a basic front-end UI!

App 2: Random Number Generator (Die Roll)

Indecision is a key component of many flavors of ADHD.  I mean, how are most people so easily able to decide what to have for lunch, or what they should do to relax in the evening, or what tasks to work on?  I'll probably never know!  There are so many options and they're all awesome, or at least all need to get done.

I personally can attest to the power of the One Thing as a way to beat decision paralysis.  The process only has a few steps:

  • Make a list of all possible options.
  • Ask yourself, "Which one of these options would make the others easier or unnecessary?"
  • Go with that option, and then apply the One Thing principle to that, et cetera.

For instance, say you need to...

  • Schedule a doctor's appointment
  • Order dog food
  • Plan your vacation
  • Look into hiring a virtual assistant
  • Search for replacement components on Aliexpress

Given that a virtual assistant could do all those other tasks for you, it's the obvious choice as the option that would make the others easier or unnecessary.

But sometimes your to-do list looks a bit more like this:

  1. Finish bookkeeping before Q2 tax deadline
  2. Review lengthy email from attorney
  3. Approve invoices
  4. Prepare notes for weekly catch-up meeting with Bioviz Inc.

None of these are really more or less valuable or important than the others; you've gotta do them all.  But how do you decide?  Do the least boring one first?  The most urgent?  (Are any of them more urgent than the others?)  Do the most exciting one first?  (Are any of them exciting?)

This is when it can be handy to simply assign each task a number and roll the dice.  In this instance, if you roll a 3, it's invoice time!

Unfortunately, dice aren't always available, especially not with the same number of sides as the options available.  As such, let's build an electronic die roller with only a few lines of code

rng.py

Let's make a new file called rng.py.  (RNG, for the record, stands for “random number generator”.)

#!/usr/bin/python3

import random

def rng(body):
    d = int(body.split(' ')[1])

    roll = random.randint(1, d)
    return "Your roll is: {roll}".format(roll = roll)

Just five lines of code!  Next, let's add this script to lifehack.py

#!/usr/bin/python3

from flask import Flask, request, make_response
from todo import *  
from rng import *   # <—-- NEW APP!

def test(body):
    return "Test successful!"

# Define our apps here
appList = {'Todo': todo,  
           'RNG': rng,     # <—-- NEW APP!
           'Test': test}

app = Flask(__name__)

@app.route("/")
def hello_world():
    print (request.args["Body"])

    appRequest = request.args["Body"].split(' ')[0]

    # See if the app request is in the app list
    appFound = False
    for appName, appHandler in appList.items():
        if appRequest == appName:
            appFound = True
            resp = make_response(appHandler(request.args["Body"]), 200)

    if appFound == False:
        resp = make_response("App not found!", 404)

    resp.mimetype = "text/plain"  
    return resp

Now, send a text to the bot with the format “RNG x”, where “x” is a number from one to the maximum number of options; you can think of it like a six-sided die, a twenty-sided die, et cetera:

Sample RNG requests and resulting rolls

This would, of course, also make an excellent tool for tabletop roleplaying.

App 3: Timer

Now that you've had a machine decide which task you're working on, how can you be confident you'll actually get it done?  How much do you need to get done now?  The whole thing?  A certain part?

I've found that timeboxing is an extremely effective strategy for keeping myself on task.  I know what I'm doing, and now I know how long I'll be doing it.

First, let's make a new spreadsheet called Timers.

New sheet called "Timers"

Once again (and for any other table you'll want to incorporate into your code), you'll need to invite your client_email as an editor:

Dialog box for choosing where this document should be shared

Next up, let's make a new file called timer.py. Add the following code to the file:

#!/usr/bin/python3

import gspread
from oauth2client.service_account import ServiceAccountCredentials
from datetime import datetime, timedelta

def timer(body):
    scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
    creds = ServiceAccountCredentials.from_json_keyfile_name('client-secrets.json', scope)
    client = gspread.authorize(creds)

    try:
        minutes = int(body.split(' ')[1])
    except IndexError:
        return "Please specify a time in minutes!  Syntax:  'Timer [minutes] [expiration message]"
  
    try:
        message = body.split(' ', 2)[2]
    except IndexError:
        return "Please specify a message to be sent to you upon expiration!  Syntax:  'Timer [minutes] [expiration message]"
  
  
    now = datetime.now()
    expirationTime = now + timedelta(minutes = minutes)
    sheet = client.open("Timers").sheet1
  
    sheet.append_row([minutes, message, str(expirationTime), 'active'])

    return "You have requested a timer for {minutes} minutes, with the message '{message}'".format(minutes = minutes, message = message)

Now go ahead and add the new app to lifehack.py, as before:

#!/usr/bin/python3

from flask import Flask, request, make_response
from todo import *  
from rng import *
from timer import *   # <—-- NEW APP!

def test(body):
    return "Test successful!"

# Define our apps here
appList = {'Todo': todo,  
           'RNG': rng,
           'Timer': timer,    # <—-- NEW APP!
           'Test': test}

app = Flask(__name__)

@app.route("/")
def hello_world():
    print (request.args["Body"])

    appRequest = request.args["Body"].split(' ')[0]

    # See if the app request is in the app list
    appFound = False
    for appName, appHandler in appList.items():
        if appRequest == appName:
            appFound = True
            resp = make_response(appHandler(request.args["Body"]), 200)

    if appFound == False:
        resp = make_response("App not found!", 404)

    resp.mimetype = "text/plain"  
    return resp

Sample timer dialogue

Going asynchronous

But wait!, you might be saying.  Sure, we've added a timer to a list, but how are we supposed to tell the user when the timer is up?  Obviously we can't send that message as part of our response!

To do this, we'll need to use the Twilio API directly.

Let's make a new Python file called timer-sender.py. Add the following code to the file:

import gspread, json, argparse
from oauth2client.service_account import ServiceAccountCredentials
from datetime import datetime
from twilio.rest import Client

parser = argparse.ArgumentParser()
parser.add_argument("--twiliosecrets", "-t", nargs='?', default="twilio-secrets.json")
parser.add_argument("--clientsecrets", "-c", nargs='?', default="client-secrets.json")
args = parser.parse_args()

# Secrets file needs to include "twilio-sid" and "twilio-token"; you can grab these
# from your Twilio console.  From and To numbers are also specified here so you're
# not hard-coding your private numbers
with open(args.twiliosecrets, "r") as secrets_file:
   secrets = json.load(secrets_file)

twilioClient = Client(secrets["twilio-sid"], secrets["twilio-token"])

def sendTwilioTextMessage(twClient, message):
   '''Allows you to send an SMS message from / to the number you specify,
   using the power of Twilio SMS.'''
   message = twClient.messages \
       .create(
               body = message,
               from_ = secrets["from"],
               to = secrets["to"]
           )

scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive']
creds = ServiceAccountCredentials.from_json_keyfile_name(args.clientsecrets, scope)
gClient = gspread.authorize(creds)

sheet = gClient.open("Timers").sheet1
timers = sheet.get_all_values()
currentTime = datetime.now()

timerRow = 0
for timer in timers:
   timerRow += 1
  
   minutes   = timer[0]
   message   = timer[1]
   timestamp = timer[2]
   active    = timer[3]
  
   if active == "active":
       expirationTime = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S.%f")
      
       if currentTime > expirationTime:
           sendTwilioTextMessage(twilioClient, message)
           sheet.update_cell(timerRow, 4, "expired")

This script, when executed, will scan the entire list of timers and see if any are 1. active, and 2. past their expiration time.  If so, this is a newly expired task!  We send the message to the user, and then mark that timer as expired.

This, however, only happens once, which obviously will not do.  We need to check for expired tasks at least every minute in order for the timer to be accurate.

I’ve added a section to this bit of code allowing you to specify the absolute location of the secrets files.  This will become necessary in the next step.

Setting up a cron job

On Unix machines (including Linux and Mac OS), we can run tasks at set times using a daemon called cron.  Cron runs continuously in the background and runs tasks based on a file called a crontab.

Let's set up our cron job!  Go to the command line and enter:

sudo crontab -e

The format for cron jobs is structured thusly:

  • Minute (m)
  • Hour (h)
  • Day of month (dom)
  • Month (mon)
  • Day of week (dow)
  • Command

Because we want our bot to run every minute, our cron job will be exceedingly simple:  five asterisks for each field, where asterisks represent "every" (every minute of every hour of every day etc.)

* * * * * /home/nick/adhd-lifehacks/venv/bin/python /home/nick/adhd-lifehacks/timer-sender.py —t /home/nick/adhd-lifehacks/twilio-secrets.json -c /home/nick/adhd-lifehacks/client-secrets.json

With this line added to the bottom of the crontab, this script should execute at the top of every minute.

You can quickly test whether it works by sending a timer with a duration of 0 (that is, it'll expire immediately the next time the script runs).

Timer SMS messages working

Now, you may also be asking, what's the point of this app when you can just use the timer on your phone?

The answer is simple:  accountability!

By leaving a paper trail of all the timers you've set, you can get a quick glance of what you did and when.

Example of timer paper trail

This could even be used for simple client time-tracking just by multiplying billed tasks by your hourly rate:

Timer paper trail transformed into an invoice

Conclusion

As stated at the beginning, the point of all this DIY work is so that you can build a system that works for your brain.  No brain is alike, neurodivergent or not, and what works wonderfully for one person can be a train wreck for another.

  • Are you finding yourself not sticking with some part of your system?  Try something else, even if only for the sake of novelty!  ADHD brains are notorious for needing something to change for no other reason than that it's new and different.
  • Feeling overwhelmed by some component?  Tone it down, or try a different approach!
  • Be creative!  I hope this article serves as inspiration and not just as a series of steps to follow.

Coming up in part 2 of this series...

We'll expand on what we've already built!  We will...

  • Tidy up and abstract-ify some of the code we've already written and add a few quality-of-life improvements
  • Build a system for recurring tasks and keeping track of habit streaks, a la Wordle or Duolingo
  • Make a bot that summarizes everything we accomplished yesterday and reminds us of what's on deck for today
  • Make another bot that sends us gentle reminders if we've forgotten to finish a task by the end of the day

See you then!

Nick Piegari is a Swiss-army-knife (but primarily web) software developer, recovering video producer and motion graphics designer, and mad scientist.  When not engaged in some big project like programmatically converting an entire site from one CMS to another, you can probably find him tinkering with an ESP8266 dev board or (very, very slowly) learning to fly helicopters.

https://www.twitch.tv/nicksmadscience