Gmail Follow-Up Tracker

Finds sent emails that never received a reply and drafts follow-ups.

followup_tracker.py
"""
Gmail Follow-Up Tracker
=======================
Finds sent emails that never received a reply and drafts follow-ups.

Key concepts:
- Multi-step reasoning: agent must compare sender vs user email per thread
- add_datetime_to_context: agent calculates days_waiting from message dates
- output_schema: structured report of pending follow-ups


Setup:
1. Create OAuth credentials at https://console.cloud.google.com (enable Gmail API)
2. Export GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_PROJECT_ID env vars
3. pip install openai google-api-python-client google-auth-httplib2 google-auth-oauthlib
4. First run opens browser for OAuth consent, saves token.json for reuse
"""

from typing import List

from agno.agent import Agent
from agno.models.openai import OpenAIResponses
from agno.tools.google.gmail import GmailTools
from pydantic import BaseModel, Field

# ---------------------------------------------------------------------------
# Output Schema
# ---------------------------------------------------------------------------


class PendingFollowUp(BaseModel):
    thread_id: str = Field(..., description="Gmail thread ID")
    subject: str = Field(..., description="Original email subject")
    recipient: str = Field(..., description="Who the email was sent to")
    sent_date: str = Field(..., description="When the original email was sent")
    days_waiting: int = Field(..., description="Days since the email was sent")
    draft_created: bool = Field(
        default=False, description="Whether a follow-up draft was created"
    )


class FollowUpReport(BaseModel):
    total_checked: int = Field(..., description="Number of sent threads checked")
    needs_followup: List[PendingFollowUp] = Field(
        default_factory=list, description="Threads that need follow-up"
    )
    summary: str = Field(..., description="Brief summary of findings")


# ---------------------------------------------------------------------------
# Create Agent
# ---------------------------------------------------------------------------

agent = Agent(
    name="Follow-Up Tracker",
    model=OpenAIResponses(id="gpt-5.5"),
    tools=[GmailTools(max_results=15)],
    instructions=[
        "Use search_threads with 'from:me' to find sent threads, then check if the last message is from you.",
        "A thread needs follow-up if the LAST message is FROM the user (no reply received).",
        "Compare the date of the last message against today to calculate days_waiting.",
        "Keep follow-up drafts short: reference the original subject and ask if they had a chance to review.",
        "Report all findings in the output schema.",
    ],
    output_schema=FollowUpReport,
    add_datetime_to_context=True,
    markdown=True,
)

# ---------------------------------------------------------------------------
# Run Demo
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    agent.print_response(
        "Check my sent emails from the last week and identify any that need a follow-up. "
        "Draft follow-ups for emails waiting more than 3 days.",
        stream=True,
    )

    # Check follow-ups for a specific recipient
    # agent.print_response(
    #     "Check if I have any unanswered emails to john@example.com from the past 2 weeks",
    #     stream=True,
    # )

Expose draft tools only

Replace GmailTools(...) with the following selection to enforce the drafting scope; the default toolkit also exposes sending methods:

GmailTools(
    include_tools=["search_threads", "get_thread", "get_message", "create_draft_email"],
    max_results=15,
)

search_threads returns IDs and snippets. Load each selected thread before deciding whether a reply is needed. The current readers omit the original RFC Message-ID header, while create_draft_email(message_id=...) copies its argument directly into reply headers. Do not substitute the Gmail API resource ID. To satisfy Gmail's threading requirements, obtain the original header through the Gmail API and supply it with the thread ID and matching subject. Otherwise create a standalone draft by omitting thread_id and message_id, then review it in Gmail.

Run the Example

Set up your virtual environment

uv venv --python 3.12
source .venv/bin/activate

Install dependencies

uv pip install -U agno google-api-python-client google-auth google-auth-httplib2 google-auth-oauthlib openai

Configure Google OAuth

Enable the Google API used by this example, configure the consent screen, and create a Desktop OAuth client in your Cloud project. Export its GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_PROJECT_ID, or place the downloaded client JSON at credentials.json in the directory where you run Python. Google's Python quickstart shows the Desktop client setup.

The first tool call opens a browser for consent and caches credentials in token.json. Use a separate token_path when switching accounts. Passing an Agno user_id does not switch the authenticated Google account.

Export environment variables

export GOOGLE_CLIENT_ID="your_google_client_id_here"
export GOOGLE_CLIENT_SECRET="your_google_client_secret_here"
export GOOGLE_PROJECT_ID="your_google_project_id_here"
export OPENAI_API_KEY="your_openai_api_key_here"

Run the example

Save the code above as followup_tracker.py, then run:

python followup_tracker.py

Full source: cookbook/91_tools/google/gmail/followup_tracker.py