DEV Community

Cover image for Solved: Create Asana Tasks from Starred Gmail Messages automatically
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Create Asana Tasks from Starred Gmail Messages automatically

🚀 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, and python-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.

  1. 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.json and place it in your project folder. This file is your key to the Google kingdom—keep it safe.
  2. 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'
Enter fullscreen mode Exit fullscreen mode

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 ---
Enter fullscreen mode Exit fullscreen mode

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 []
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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.json can expire if the script doesn’t run for a long time. If you see authentication errors, the simplest fix is to delete token.json and 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.env file. 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!


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)