🚀 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`, and `config.env` with `python-dotenv` to avoid hardcoding sensitive information.
- Utilize the Gmail API with `gmail.modify` scope to query for `labelIds=[‘STARRED’]` messages and crucially `removeLabelIds=[‘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!
🤖 Frequently Asked Questions
âť“ How does the script prevent duplicate Asana tasks from being created for the same starred email?
The script prevents duplicates by automatically un-starring the email in Gmail immediately after successfully creating the corresponding Asana task, marking it as processed.
âť“ What are the essential prerequisites for implementing this Gmail-to-Asana automation?
Key prerequisites include a Google Account with Gmail API access, an Asana Account with a Personal Access Token (PAT), Python 3.8+, and specific Python packages such as `google-api-python-client`, `asana`, and `python-dotenv`.
âť“ What is a common pitfall with Google API authentication, and how can it be resolved?
A common pitfall is an expired `token.json` file, which stores Google’s refresh token. This is resolved by deleting `token.json` and re-running the script, which will trigger a new OAuth2 flow to generate fresh credentials.
Leave a Reply