đ Executive Summary
TL;DR: This guide provides a robust Python script to automatically create Asana tasks from starred Gmail messages. It solves the problem of important emails getting lost in the inbox by integrating Gmailâs starring feature with Asanaâs task management, ensuring actionable items are never forgotten.
đŻ Key Takeaways
- Securely manage API credentials for Gmail and Asana using
credentials.json,token.json, andconfig.envwithpython-dotenvto avoid hardcoding sensitive information. - Utilize the Gmail API with
gmail.modifyscope to query forlabelIds=[âSTARREDâ]messages and cruciallyremoveLabelIds=[âSTARREDâ]after processing to prevent duplicate task creation. - Automate the scriptâs execution using cron jobs on Linux-based systems (e.g.,
0 \* \* \* \* python3 /path/to/script.py) to ensure continuous synchronization between starred emails and Asana tasks.
Create Asana Tasks from Starred Gmail Messages automatically
Hey there, Darian Vance here. As a Senior DevOps Engineer at TechResolve, my inbox is a constant flood of alerts, requests, and CI/CD notifications. For a long time, my process was to star important emails on my phone, promising myself Iâd âget to them later.â Of course, âlaterâ often meant ânever,â and actionable items would get buried. I realized I was wasting a solid hour or two every week just re-finding and manually tracking these things. Thatâs a huge waste of focus.
This tutorial is the solution I built for myself. Itâs a simple, robust Python script that scans your Gmail for starred messages and automatically creates tasks for them in an Asana project. Itâs a classic âset it and forget itâ automation that bridges the gap between your inbox and your actual work queue. Letâs get this set up so you can reclaim some of your time.
Prerequisites
Before we start, make sure you have the following ready:
- A Google Account with API access enabled.
- An Asana Account (a free one works just fine).
- Python 3.8+ installed on the machine where this will run.
- The ability to install a few Python packages. Youâll need
google-api-python-client,google-auth-httplib2,google-auth-oauthlib,asana, andpython-dotenv. You can install these using pip.
The Guide: Step-by-Step
Step 1: Get Your API Credentials
First things first, we need to give our script permission to talk to Google and Asana.
-
For Gmail: Go to the Google Cloud Console, create a new project, and enable the âGmail APIâ. Then, under âCredentials,â create an âOAuth 2.0 Client IDâ for a âDesktop appâ. Download the JSON file. Rename it to
credentials.jsonand place it in your project folder. This file is your key to the Google kingdomâkeep it safe. - For Asana: Log in to Asana, go to âMy Settings,â then the âAppsâ tab, and click âManage Developer Apps.â From there, you can create a âPersonal Access Tokenâ (PAT). Give it a descriptive name like âGmail-Task-Botâ. Copy this token immediately; you wonât see it again.
Pro Tip: Never, ever hardcode secrets like API tokens directly in your script. Weâre going to use a configuration file for this. In my production setups, I use a proper secrets manager like HashiCorp Vault or AWS Secrets Manager, but for this, a local config file is perfectly fine.
Step 2: Project Setup
Iâll skip the standard virtual environment setup since you likely have your own workflow for that. Letâs jump straight to the project structure. In your project directory, create two files:
-
gmail_to_asana.py: This will be our main Python script. -
config.env: This file will store our sensitive credentials.
Inside your config.env file, add the following, replacing the placeholders with your actual Asana token and the ID of the Asana project you want to add tasks to.
ASANA_PAT='YOUR_PERSONAL_ACCESS_TOKEN_HERE'
ASANA_PROJECT_GID='YOUR_PROJECT_ID_HERE'
You can find the Project GID by looking at the URL when youâre in an Asana project. Itâs the long number after /project/.
Step 3: The Python Script â Authentication & Setup
Letâs start building our script. The first part handles loading our configuration and authenticating with both services. Googleâs OAuth2 flow is a bit involved the first time you run itâit will open a browser window and ask you to authorize the application. After that, it creates a token.json file to store the refresh token so you donât have to log in every time.
import os
import base64
from email import message_from_bytes
import asana
from dotenv import load_dotenv
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# Load environment variables from config.env
load_dotenv('config.env')
# --- CONFIGURATION ---
ASANA_PAT = os.getenv('ASANA_PAT')
ASANA_PROJECT_GID = os.getenv('ASANA_PROJECT_GID')
GMAIL_SCOPES = ['https://www.googleapis.com/auth/gmail.modify'] # .modify to un-star emails
def authenticate_gmail():
"""Authenticates with the Gmail API and returns a service object."""
creds = None
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', GMAIL_SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', GMAIL_SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.json', 'w') as token:
token.write(creds.to_json())
try:
service = build('gmail', 'v1', credentials=creds)
print("Gmail API authentication successful.")
return service
except HttpError as error:
print(f'An error occurred during Gmail authentication: {error}')
return None
def authenticate_asana():
"""Authenticates with the Asana API and returns a client object."""
try:
client = asana.Client.access_token(ASANA_PAT)
# Test connection by getting user info
client.users.me()
print("Asana API authentication successful.")
return client
except Exception as e:
print(f"An error occurred during Asana authentication: {e}")
return None
# --- We will add more functions below ---
Step 4: Fetching Starred Emails
Now for the core logic. We need a function to query Gmail for any messages that are starred but havenât been processed yet. The key here is the query is:starred. Once we get the list of message IDs, weâll fetch the full content for each one.
Pro Tip: An inbox with hundreds of starred emails can be noisy. Make your Gmail query more specific to only catch actionable items. For example, use
'is:starred from:no-reply@github.com in:inbox'to only process starred notifications from GitHub.
def get_starred_emails(service):
"""Fetches a list of starred emails from Gmail."""
try:
# We search for messages with the 'STARRED' label.
results = service.users().messages().list(userId='me', labelIds=['STARRED']).execute()
messages = results.get('messages', [])
if not messages:
print("No new starred emails found.")
return []
print(f"Found {len(messages)} starred email(s).")
return messages
except HttpError as error:
print(f'An error occurred fetching emails: {error}')
return []
Step 5: Creating Asana Tasks and Un-starring Emails
This is where the magic happens. Weâll loop through our list of emails. For each one, weâll parse the subject and body to create a nicely formatted Asana task. The most critical step is at the end: we **un-star the email**. This is our simple, effective way to mark the email as âprocessedâ and prevent the script from creating duplicate tasks every time it runs.
def process_emails(gmail_service, asana_client, messages):
"""Processes each email: creates an Asana task and then un-stars the email."""
if not messages:
return
for message_info in messages:
msg_id = message_info['id']
try:
# Get the full message details
msg = gmail_service.users().messages().get(userId='me', id=msg_id, format='raw').execute()
# Decode the raw email data
raw_email = base64.urlsafe_b64decode(msg['raw'].encode('ASCII'))
email_message = message_from_bytes(raw_email)
subject = email_message['subject']
from_address = email_message['from']
# Construct Asana task details
task_name = f"Gmail: {subject}"
task_notes = f"New task from a starred email.\n\nFrom: {from_address}\n\n--- Start of Email ---\n"
# Find the plain text body part
if email_message.is_multipart():
for part in email_message.walk():
if part.get_content_type() == 'text/plain':
body = part.get_payload(decode=True).decode()
task_notes += body
break
else:
body = email_message.get_payload(decode=True).decode()
task_notes += body
task_notes += "\n\n--- End of Email ---"
# Create the Asana task
print(f"Creating Asana task for: '{subject}'")
asana_client.tasks.create_task({
'name': task_name,
'notes': task_notes,
'projects': [ASANA_PROJECT_GID]
})
# IMPORTANT: Un-star the email to prevent duplicates
gmail_service.users().messages().modify(
userId='me',
id=msg_id,
body={'removeLabelIds': ['STARRED']}
).execute()
print(f"Successfully processed and un-starred email ID: {msg_id}")
except HttpError as error:
print(f"An error occurred processing email ID {msg_id}: {error}")
except Exception as e:
print(f"A general error occurred: {e}")
Step 6: Putting It All Together and Running the Script
Finally, letâs create a main execution block to tie all our functions together.
def main():
"""Main function to run the entire workflow."""
print("Starting Gmail to Asana sync process...")
gmail_service = authenticate_gmail()
asana_client = authenticate_asana()
if not gmail_service or not asana_client:
print("Authentication failed. Aborting script.")
return # Replaced sys.exit() with return
starred_emails = get_starred_emails(gmail_service)
process_emails(gmail_service, asana_client, starred_emails)
print("Sync process finished.")
if __name__ == '__main__':
main()
The first time you run this script from your terminal (python3 gmail_to_asana.py), it will open a browser for you to approve the Google permissions. After that, it should run silently.
Step 7: Automate It!
A script is only useful if you donât have to remember to run it. On a Linux-based system, a cron job is the perfect tool for this. We can set it to run, say, every hour.
You can set up a cron job to run the script automatically. Here is an example that runs the script at the top of every hour:
0 * * * * python3 /path/to/your/project/gmail_to_asana.py
Just be sure to use the full path to your Python executable and script file. A less frequent schedule, like once a day, might be better to start with:
0 2 * * * python3 /path/to/your/project/gmail_to_asana.py
Common Pitfalls
Here are a few places Iâve tripped up in the past:
-
Expired Google Token: Googleâs
token.jsoncan expire if the script doesnât run for a long time. If you see authentication errors, the simplest fix is to deletetoken.jsonand re-run the script manually to generate a new one. - API Rate Limits: If you have thousands of starred emails, donât run the script in a rapid loop. Process them in batches. My script fetches a limited number by default, which is usually safe. Running it once an hour is more than enough to stay under the limits.
-
Incorrect Asana Project GID: Double-check that GID in your
config.envfile. If itâs wrong, the script will fail when trying to create a task, and itâs not always an obvious error.
Conclusion
And thatâs it. You now have a reliable, automated bridge between your inbox and your task list. This simple automation has genuinely improved my workflow, ensuring that no critical, actionable email ever gets lost in the shuffle again. It lets me use my inbox for what itâs forâcommunicationâand Asana for what itâs forâaction. Feel free to customize the script to add assignees, due dates, or custom fields to your Asana tasks. Happy automating!
đ Read the original article on TechResolve.blog
â Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)