Build a Daily Briefing Agent with Claude

Build a Python agent that reads Gmail, Calendar, and weather to generate a daily briefing.

Introduction

30 Second Summary

Every morning you check a dozen different apps. Email, calendar, weather, bank notifications. By the time you have a full picture of your day, you've already lost 15 minutes.

In this project, you will build a Python agent that connects to your real Gmail, Google Calendar, and a weather API, then uses Claude to reason across all your data and produce an intelligent daily briefing.

What You'll Build

A command-line Python agent that uses Claude's tool-use capability to pull data from multiple real sources and synthesize a prioritized daily plan.

By the end of this project, you'll have:

  • A working agent loop where Claude decides which tools to call, you execute them, and Claude reasons over the results.
  • Four tool functions connected to your real Gmail, Google Calendar, a weather API, and your financial notification emails.
  • A complete OAuth2 authentication flow that securely connects your agent to Google services.
  • Secret Mission: Add a news headlines tool using a free RSS feed so the agent factors breaking news into your daily plan.

Are there any prerequisites?

You should have some Python experience, a Gmail or Google account, and VS Code installed on your machine. No prior experience with AI APIs or Google Cloud is needed.

Before We Start

Before your agent can read emails, check calendars, and fetch weather data, you need a working Python environment and access to Claude's API.

In this step, you'll create your project, install the required libraries, and verify that your connection to Claude is working.

In this step, get ready to:
  • Create a Python project with a virtual environment and install all dependencies.
  • Obtain an Anthropic API key and configure it as an environment variable.
  • Verify your setup by making a test API call to Claude.
Create your project and install dependencies

You need an isolated Python environment so the libraries for this project don't conflict with anything else on your machine.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir daily-briefing-agent
cd daily-briefing-agent
  • Confirm the folder exists by running:
ls

You should see the daily-briefing-agent folder listed in your Desktop output (if you ran ls before cd), or an empty directory listing now that you're inside it.

  • Create a Python virtual environment by running:
python3 -m venv venv

What is a virtual environment?

A virtual environment is an isolated space where Python installs packages just for this project. It prevents version conflicts between different projects on your machine.

  • Activate the virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell, then try activating again.

On macOS, make sure you typed source before the path. Without it, the activation script won't run in your current session.

help me activate my Python virtual environment

Now you'll create the file that lists all the packages your project needs.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the daily-briefing-agent folder on your Desktop, then click Open.
  • Create a new file by clicking File then New File.
  • Save it immediately as requirements.txt using Cmd+S (macOS) or Ctrl+S (Windows).
  • Paste the following content into requirements.txt:
anthropic==0.111.0
google-api-python-client==2.163.0
google-auth-httplib2==0.2.0
google-auth-oauthlib==1.2.1
requests==2.32.3

What does this file do?

A requirements.txt file pins the exact versions of every library your project uses. This ensures anyone (including future you) can recreate the same working environment.

  • anthropic is the official Python SDK for calling Claude's API.
  • google-api-python-client, google-auth-httplib2, and google-auth-oauthlib handle Gmail and Calendar authentication.
  • requests is a library for making HTTP calls to the weather API.
  • Save requirements.txt with Cmd+S (macOS) or Ctrl+S (Windows).
  • Return to your terminal and install all the dependencies by running:
pip install -r requirements.txt

You should see a stream of download progress messages followed by Successfully installed and a list of package names.

Seeing permission errors or install failures?

Make sure (venv) appears in your terminal prompt. If it doesn't, your virtual environment isn't active. Re-run the activation command above.

If you see a network error, check your internet connection and try running the install command again.

help me fix my pip install errors

✔️ Awesome, I've got everything!

Great. Double check you've saved requirements.txt and move on to the next substep.

ⓧ I'd like to double check the full code

anthropic==0.111.0
google-api-python-client==2.163.0
google-auth-httplib2==0.2.0
google-auth-oauthlib==1.2.1
requests==2.32.3
Get your Anthropic API key

Claude's API requires an API key for authentication. You'll create an account on the Anthropic console and add credit so your agent can make API calls.

  • Open your browser and go to console.anthropic.com.
  • Sign up for a new account (or sign in if you already have one).
  • Once logged in, navigate to Settings then Billing.
  • Add at least $5 in credit. This is the minimum purchase amount.
  • Navigate to API Keys in the Settings menu.
  • Click Create Key.
  • Give your key a name (like daily-briefing) and click Create Key.
  • Copy the key immediately. You won't be able to see it again after closing this dialog.

Keep your API key safe

Never share your API key or commit it to version control. You'll store it as an environment variable so it stays out of your code files.

Now set the API key as an environment variable in your terminal. The Anthropic SDK automatically reads from ANTHROPIC_API_KEY when you create a client.

  • In your terminal (where the virtual environment is active), set the environment variable:

macOS

export ANTHROPIC_API_KEY=your-api-key-here

Windows

set ANTHROPIC_API_KEY=your-api-key-here
  • Replace your-api-key-here with the key you just copied from the Anthropic console.

Why an environment variable?

Environment variables keep secrets out of your source code. The Anthropic Python SDK looks for ANTHROPIC_API_KEY automatically, so you never need to paste the key directly into a Python file.

Note that this variable only lasts for the current terminal session. If you close and reopen your terminal, you'll need to set it again.

Verify your setup with a test API call

Time to confirm everything is connected. You'll send a simple message to Claude and check that you get a response back.

  • Run this test command in your terminal:
python3 -c "import anthropic; client = anthropic.Anthropic(); response = client.messages.create(model='claude-haiku-4-5', max_tokens=100, messages=[{'role': 'user', 'content': 'Say hello in one sentence.'}]); print(response.content[0].text)"

You should see a short greeting from Claude printed in your terminal (something like "Hello! How can I help you today?").

What just happened?

This one-liner imported the Anthropic SDK, created a client (which read your ANTHROPIC_API_KEY automatically), and sent a message to the claude-haiku-4-5 model.

Claude Haiku 4.5 is a fast, affordable model at $1 per million input tokens and $5 per million output tokens. For this project, your total cost will stay well under $0.50.

Seeing an authentication error?

Double check that you set the ANTHROPIC_API_KEY environment variable in the same terminal session where you're running the command.

Make sure you copied the full key from the Anthropic console (it starts with sk-ant-).

Verify you added billing credit. The API won't work without at least $5 loaded.

help me fix my Anthropic API authentication error

Your environment is locked and loaded. Next up, you'll connect to your real Gmail and Google Calendar using OAuth2 authentication.

Set Up Google Cloud OAuth2

Your agent needs permission to read your real Gmail and Google Calendar data. Google Cloud uses OAuth2 to let apps access user data securely without ever sharing your password.

In this step, you will create a Google Cloud project, enable the Gmail and Calendar APIs, set up OAuth2 credentials, and write the authentication code that connects your Python agent to your Google account.

In this step, get ready to:
  • Create a Google Cloud project and enable the Gmail and Calendar APIs.
  • Configure the OAuth consent screen and create Desktop App credentials.
  • Write the authentication module and generate your access token.
Create a Google Cloud project and enable APIs

Google APIs live inside Cloud projects. You need a project to enable the Gmail and Calendar APIs that your agent will call.

  • Open console.cloud.google.com in your browser and sign in with your Google account.
  • Click Select a project at the top of the page.
  • Click New Project.
  • Enter daily-briefing-agent as the project name.
  • Click Create.
  • Wait for the notification confirming your project is ready, then select it.

Now enable the two APIs your agent needs.

  • Open the navigation menu (hamburger icon in the top left).
  • Click APIs & Services, then click Library.
  • Search for Gmail API and click on it.
  • Click Enable.
  • Return to the Library page and search for Google Calendar API.
  • Click Enable.

What are these APIs?

The Gmail API lets your agent read email metadata (subject, sender, snippet) without needing your password. The Google Calendar API lets it fetch today's events.

Both require explicit permission through OAuth2, which you will configure next.

Configure the OAuth consent screen and create credentials

Before Google lets your app request user data, you must configure an OAuth consent screen. This is the page users see when granting access.

  • In the Google Cloud Console, open the navigation menu.
  • Click Google Auth platform.
  • Click Branding.
  • If you see a message that says the platform is not configured yet, click Get Started.
  • Under App Information, enter Daily Briefing Agent in the App name field.
  • In User support email, select your email address.
  • Click Next.
  • Under Audience, select External.
  • Click Next.
  • Under Contact Information, enter your email address.
  • Click Next.
  • Under Finish, review the Google API Services User Data Policy and select the checkbox to agree.
  • Click Continue.
  • Click Create.

Now add yourself as a test user so you can authorize the app.

  • Click Audience in the left sidebar.
  • Under Test users, click Add users.
  • Enter your Gmail address and click Save.

Why External and Testing mode?

Personal Gmail accounts cannot use the Internal user type. External in Testing mode means only users you explicitly add can authorize the app. No verification or app review is needed for personal testing.

Your refresh token will expire every 7 days in Testing mode. If that happens, delete token.json and re-authorize.

Next, create the OAuth client credentials your Python code will use.

  • In the left sidebar, click Clients (under Google Auth platform).
  • Click Create Client.
  • For Application type, select Desktop app.
  • In the Name field, type Daily Briefing Agent.
  • Click Create.
  • Download the JSON file and save it as credentials.json in your project folder (the same folder that contains requirements.txt).

What is credentials.json?

This file contains your app's client ID and client secret. Your Python code uses these to prove its identity to Google when requesting user authorization. Keep this file private and never commit it to version control.

  • Confirm credentials.json is in your project folder by running:
ls credentials.json

You should see credentials.json printed in the output.

Don't see credentials.json?

Check your Downloads folder. The file might have downloaded with a different name like client_secret_*.json. Rename it to credentials.json and move it into your project folder.

Help me find my credentials.json file

Write the authentication module and run the OAuth flow

Your agent needs a reusable way to authenticate with Google. You will write a module called auth.py that handles loading credentials, refreshing expired tokens, and building service objects for Gmail and Calendar.

  • Create a new file called auth.py in your project folder and add the following code:
import os
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

SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/calendar.readonly",
]

What does this code do?

The imports bring in Google's official authentication libraries (installed with your dependencies in Step 1). SCOPES defines what your app can access.

  • gmail.readonly lets you read emails without modifying them.
  • calendar.readonly lets you read calendar events without modifying them.
  • Below the SCOPES list, add the credential-loading function:

def get_google_credentials():
    creds = None
    if os.path.exists("token.json"):
        creds = Credentials.from_authorized_user_file("token.json", SCOPES)
    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", SCOPES)
            creds = flow.run_local_server(port=0)
        with open("token.json", "w") as token:
            token.write(creds.to_json())
    return creds

What does this code do?

This function handles the full authentication lifecycle:

  • First, it checks if token.json already exists (a saved token from a previous run).
  • If the token is expired but has a refresh token, it silently refreshes without opening a browser.
  • If no valid token exists, it launches a browser window for you to sign in and grant permission.
  • After authorization, it saves the token to token.json so future runs skip the browser step.
  • Below get_google_credentials, add two helper functions that create service objects:

def get_gmail_service(creds):
    return build("gmail", "v1", credentials=creds)


def get_calendar_service(creds):
    return build("calendar", "v3", credentials=creds)

What does this code do?

These two functions wrap Google's build() method to create pre-authenticated service objects. Other files in your project will import these to talk to Gmail (v1) and Calendar (v3) without re-doing the auth flow.

  • Save auth.py.

Now test the OAuth flow to generate your token.json.

  • Run the authentication test by entering this command in your terminal:
python3 -c "from auth import get_google_credentials; get_google_credentials()"

What will happen next?

A browser window will open asking you to sign in to your Google account. You will see a warning screen because the app is in Testing mode. This is expected and safe to proceed past.

  • In the browser window, sign in with the same Google account you added as a test user.
  • When you see the "Google hasn't verified this app" warning, click Continue.
  • Review the permissions (read Gmail and Calendar) and click Continue to grant access.
  • Close the browser tab when you see the success message.
  • Verify that token.json was created by running:
ls token.json

You should see token.json in the output. Your agent can now authenticate with Google without opening the browser again.

Browser didn't open or authorization failed?

  • Make sure credentials.json is in the same directory where you ran the command.
  • Check that you added your email as a test user in the Audience section of the Cloud Console.
  • If you see "Access blocked", verify you selected External for the audience type and that the Publishing Status is Testing.

Help me troubleshoot my OAuth flow

✔️ Awesome, I've got everything!

Great. Double check you have saved auth.py and that token.json exists in your project folder.

ⓧ I'd like to double check the full code

import os
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

SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/calendar.readonly",
]


def get_google_credentials():
    creds = None
    if os.path.exists("token.json"):
        creds = Credentials.from_authorized_user_file("token.json", SCOPES)
    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", SCOPES)
            creds = flow.run_local_server(port=0)
        with open("token.json", "w") as token:
            token.write(creds.to_json())
    return creds


def get_gmail_service(creds):
    return build("gmail", "v1", credentials=creds)


def get_calendar_service(creds):
    return build("calendar", "v3", credentials=creds)

Your Google Cloud project is fully configured. Next up, you will build the tool functions that your agent uses to fetch emails, calendar events, and weather data.

Build Core Tools

Your agent now has authenticated access to Gmail and Google Calendar. The next question is: what does it actually do with that access?

In this step, you'll build three tool functions that fetch real data from your email, calendar, and a weather API. These functions are what Claude will call when it decides it needs information to assemble your briefing.

In this step, get ready to:
  • Build a Gmail reader tool that fetches recent emails.
  • Build a Calendar reader tool that fetches today's events.
  • Build a Weather tool that fetches current conditions from Open-Meteo.
Build the Gmail reader tool function

The Gmail API returns email data in a two-step process. First you list message IDs, then you fetch metadata for each one. Your tool function wraps both steps into a single call that returns clean, structured data.

  • Create a new file called tools.py in your project folder.
  • Add the imports and the get_recent_emails function by pasting this code:
import base64
import datetime
import requests


def get_recent_emails(gmail_service, max_results=10):
    # List message IDs from the inbox
    results = (
        gmail_service.users()
        .messages()
        .list(userId="me", maxResults=max_results, labelIds=["INBOX"])
        .execute()
    )
    messages = results.get("messages", [])
    emails = []
    for msg in messages:
        # Fetch metadata for each message
        detail = (
            gmail_service.users()
            .messages()
            .get(userId="me", id=msg["id"], format="metadata", metadataHeaders=["Subject", "From", "Date"])
            .execute()
        )
        # Extract headers into a flat dictionary
        headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
        emails.append(
            {
                "subject": headers.get("Subject", "No Subject"),
                "from": headers.get("From", "Unknown"),
                "date": headers.get("Date", "Unknown"),
                "snippet": detail.get("snippet", ""),
            }
        )
    return emails

What does this code do?

This function takes the Gmail service object you built in auth.py and retrieves recent inbox messages.

  • gmail_service.users().messages().list() fetches a list of message IDs from your inbox.
  • For each message ID, a second call with format="metadata" fetches only the headers (Subject, From, Date) and a short snippet. This avoids downloading the full email body.
  • The function returns a clean list of dictionaries, each containing the email's subject, sender, date, and snippet.
  • Save tools.py.

You won't see output yet. The next chunk adds your calendar reader, and in Step 5 the agent will call both tools.

Seeing a syntax error when you save?

  • Check that the chained method calls (the lines starting with dots) are indented consistently. Python treats each parenthesized group as one expression, so line breaks inside parentheses are fine as long as indentation is uniform.
  • Make sure the closing parenthesis of each .execute() call aligns with the opening line.

Help me fix a syntax error in my get_recent_emails function

Build the Calendar reader tool function

The Google Calendar API lets you query events within a time range. Your tool calculates today's start and end timestamps in UTC, then fetches all events that fall within that window.

  • In tools.py, add the following function below get_recent_emails:

def get_todays_events(calendar_service):
    # Calculate today's time boundaries in UTC
    now = datetime.datetime.now(tz=datetime.timezone.utc)
    start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
    end_of_day = now.replace(hour=23, minute=59, second=59, microsecond=0).isoformat()
    # Query the primary calendar for today's events
    events_result = (
        calendar_service.events()
        .list(
            calendarId="primary",
            timeMin=start_of_day,
            timeMax=end_of_day,
            singleEvents=True,
            orderBy="startTime",
        )
        .execute()
    )
    events = events_result.get("items", [])
    # Return a clean list with summary, start, end, and location
    return [
        {
            "summary": event.get("summary", "No title"),
            "start": event["start"].get("dateTime", event["start"].get("date")),
            "end": event["end"].get("dateTime", event["end"].get("date")),
            "location": event.get("location", "No location specified"),
        }
        for event in events
    ]

What does this code do?

  • start_of_day and end_of_day define a 24-hour window starting at midnight UTC. The Calendar API uses ISO 8601 timestamps to filter events.
  • singleEvents=True expands recurring events into individual instances so you see each occurrence separately.
  • The list comprehension at the end extracts only the fields Claude needs: title, start/end times, and location.
  • Save tools.py.

You won't see output yet. The next chunk adds a weather tool you can test immediately.

Unsure about the time zone?

The function uses UTC timestamps because the Calendar API handles conversion to your local time zone automatically based on your calendar settings. You do not need to adjust these values for your location.

Help me understand the timezone handling in my calendar tool

Build the Weather tool function

Open-Meteo is a free weather API that requires no API key. You pass latitude and longitude, and it returns current conditions plus an hourly forecast. This makes it perfect for a learning project where you want zero setup friction.

  • In tools.py, add the following function below get_todays_events:

def get_weather(latitude, longitude):
    # Open-Meteo endpoint (free, no API key needed)
    url = "https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": latitude,
        "longitude": longitude,
        "current": "temperature_2m,apparent_temperature,weather_code,wind_speed_10m",
        "hourly": "temperature_2m,precipitation_probability",
        "forecast_days": 1,
        "temperature_unit": "fahrenheit",
    }
    # Make the HTTP request and return the JSON response
    response = requests.get(url, params=params)
    response.raise_for_status()
    return response.json()

What does this code do?

  • The params dictionary tells the API what data to return. current requests real-time conditions (temperature, feels-like, weather code, wind). hourly adds the day's hourly temperature and rain probability.
  • response.raise_for_status() throws an error if the API returns a non-200 status code, so failures surface immediately instead of silently.
  • The function returns raw JSON. Claude will interpret the weather codes and precipitation probabilities when generating the briefing.
  • Save tools.py.
  • Test the weather function by running this command in your terminal:
python3 -c "from tools import get_weather; import json; print(json.dumps(get_weather(37.7749, -122.4194), indent=2)[:500])"

You should see a JSON object with current temperature, wind speed, and hourly forecast data for San Francisco.

Seeing a connection error?

  • Make sure you are connected to the internet. The Open-Meteo API is a public endpoint that requires network access.
  • Check that your virtual environment is active and requests is installed. Run pip list to confirm.
  • If you see ModuleNotFoundError, you may be running python3 outside your virtual environment. Reactivate it with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).

Help me debug my weather tool function

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

Here is the complete tools.py file with all three functions:

import base64
import datetime
import requests


def get_recent_emails(gmail_service, max_results=10):
    results = (
        gmail_service.users()
        .messages()
        .list(userId="me", maxResults=max_results, labelIds=["INBOX"])
        .execute()
    )
    messages = results.get("messages", [])
    emails = []
    for msg in messages:
        detail = (
            gmail_service.users()
            .messages()
            .get(userId="me", id=msg["id"], format="metadata", metadataHeaders=["Subject", "From", "Date"])
            .execute()
        )
        headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
        emails.append(
            {
                "subject": headers.get("Subject", "No Subject"),
                "from": headers.get("From", "Unknown"),
                "date": headers.get("Date", "Unknown"),
                "snippet": detail.get("snippet", ""),
            }
        )
    return emails


def get_todays_events(calendar_service):
    now = datetime.datetime.now(tz=datetime.timezone.utc)
    start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
    end_of_day = now.replace(hour=23, minute=59, second=59, microsecond=0).isoformat()
    events_result = (
        calendar_service.events()
        .list(
            calendarId="primary",
            timeMin=start_of_day,
            timeMax=end_of_day,
            singleEvents=True,
            orderBy="startTime",
        )
        .execute()
    )
    events = events_result.get("items", [])
    return [
        {
            "summary": event.get("summary", "No title"),
            "start": event["start"].get("dateTime", event["start"].get("date")),
            "end": event["end"].get("dateTime", event["end"].get("date")),
            "location": event.get("location", "No location specified"),
        }
        for event in events
    ]


def get_weather(latitude, longitude):
    url = "https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": latitude,
        "longitude": longitude,
        "current": "temperature_2m,apparent_temperature,weather_code,wind_speed_10m",
        "hourly": "temperature_2m,precipitation_probability",
        "forecast_days": 1,
        "temperature_unit": "fahrenheit",
    }
    response = requests.get(url, params=params)
    response.raise_for_status()
    return response.json()

Your agent now has eyes on three data sources: your inbox, your schedule, and the weather. Next up, you'll build a finance scanner that digs into your spending notification emails.

Build the Finance Tool

Your agent can already read emails, check your calendar, and fetch the weather. Now you'll give it financial awareness.

Most banks and payment services send email notifications for transactions, statements, and alerts. Instead of connecting to a bank API (which requires complex integrations like Plaid), your agent will search Gmail for these financial notification emails and extract spending patterns directly from them.

In this step, get ready to:
  • Build a finance scanner that searches Gmail for bank and spending notifications.
  • Parse financial emails to extract transaction details.
  • Return structured spending data for Claude to analyze.
Build the finance scanner function

Gmail's search API supports the same query operators you use in the Gmail search bar. You can filter by subject keywords, sender, date range, and more.

Your finance tool will use a search query that targets subject lines containing words like "transaction", "payment", "charge", and "receipt". It also limits results to the past 7 days so Claude only sees recent spending.

  • In tools.py, scroll to the bottom of the file (after the get_weather function).
  • Add the finance scanner function by copying the following code at the bottom of the file:
def get_financial_emails(gmail_service, max_results=20):
    # Search Gmail for bank notifications, transaction alerts, and receipts from the past 7 days
    query = "subject:(transaction OR payment OR charge OR receipt OR statement OR alert) newer_than:7d"
    results = (
        gmail_service.users()
        .messages()
        .list(userId="me", q=query, maxResults=max_results)
        .execute()
    )
    messages = results.get("messages", [])
    finance_emails = []
    for msg in messages:
        # Fetch metadata for each matching email
        detail = (
            gmail_service.users()
            .messages()
            .get(userId="me", id=msg["id"], format="metadata", metadataHeaders=["Subject", "From", "Date"])
            .execute()
        )
        headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
        finance_emails.append(
            {
                "subject": headers.get("Subject", "No Subject"),
                "from": headers.get("From", "Unknown"),
                "date": headers.get("Date", "Unknown"),
                "snippet": detail.get("snippet", ""),
            }
        )
    return finance_emails

What does this code do?

This function reuses the same Gmail API and OAuth credentials from your get_recent_emails function. The key difference is the search query.

  • The query string uses Gmail search operators to find emails with financial keywords in their subject lines. The newer_than:7d part limits results to the past week.
  • Instead of filtering by labelIds like the general email reader, this function uses the q parameter to pass a Gmail search query.
  • The return structure (subject, from, date, snippet) gives Claude enough context to identify merchants, amounts, and spending patterns from the email previews.

Why reuse Gmail instead of a bank API?

Connecting directly to bank accounts would require a service like Plaid, which has complex onboarding and costs money. Most banks already send email notifications for transactions, so Gmail becomes a free, already-authenticated data source for spending patterns.

Claude is smart enough to parse amounts and merchant names from email snippets. You don't need perfectly structured data for the agent to find patterns.

  • Save tools.py.
  • Verify the function loads without syntax errors by running this command in your terminal:
python3 -c "from tools import get_financial_emails; print('Finance tool loaded successfully')"

You should see Finance tool loaded successfully printed in your terminal.

Seeing a syntax error?

  • Make sure you added the function after the closing return statement of get_weather, not inside another function.
  • Check that the triple-quoted string in query uses regular double quotes, not mismatched quote types.
  • Confirm you have a blank line between the end of get_weather and the start of get_financial_emails.

Help me debug a syntax error in my get_financial_emails function in tools.py

✔️ Awesome, I've got everything!

Great. Double check you've saved tools.py and the import test printed successfully.

ⓧ I'd like to double check the full code

Here's the complete tools.py file with all four tool functions:

import base64
import datetime
import requests


def get_recent_emails(gmail_service, max_results=10):
    results = (
        gmail_service.users()
        .messages()
        .list(userId="me", maxResults=max_results, labelIds=["INBOX"])
        .execute()
    )
    messages = results.get("messages", [])
    emails = []
    for msg in messages:
        detail = (
            gmail_service.users()
            .messages()
            .get(userId="me", id=msg["id"], format="metadata", metadataHeaders=["Subject", "From", "Date"])
            .execute()
        )
        headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
        emails.append(
            {
                "subject": headers.get("Subject", "No Subject"),
                "from": headers.get("From", "Unknown"),
                "date": headers.get("Date", "Unknown"),
                "snippet": detail.get("snippet", ""),
            }
        )
    return emails


def get_todays_events(calendar_service):
    now = datetime.datetime.now(tz=datetime.timezone.utc)
    start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
    end_of_day = now.replace(hour=23, minute=59, second=59, microsecond=0).isoformat()
    events_result = (
        calendar_service.events()
        .list(
            calendarId="primary",
            timeMin=start_of_day,
            timeMax=end_of_day,
            singleEvents=True,
            orderBy="startTime",
        )
        .execute()
    )
    events = events_result.get("items", [])
    return [
        {
            "summary": event.get("summary", "No title"),
            "start": event["start"].get("dateTime", event["start"].get("date")),
            "end": event["end"].get("dateTime", event["end"].get("date")),
            "location": event.get("location", "No location specified"),
        }
        for event in events
    ]


def get_weather(latitude, longitude):
    url = "https://api.open-meteo.com/v1/forecast"
    params = {
        "latitude": latitude,
        "longitude": longitude,
        "current": "temperature_2m,apparent_temperature,weather_code,wind_speed_10m",
        "hourly": "temperature_2m,precipitation_probability",
        "forecast_days": 1,
        "temperature_unit": "fahrenheit",
    }
    response = requests.get(url, params=params)
    response.raise_for_status()
    return response.json()


def get_financial_emails(gmail_service, max_results=20):
    query = "subject:(transaction OR payment OR charge OR receipt OR statement OR alert) newer_than:7d"
    results = (
        gmail_service.users()
        .messages()
        .list(userId="me", q=query, maxResults=max_results)
        .execute()
    )
    messages = results.get("messages", [])
    finance_emails = []
    for msg in messages:
        detail = (
            gmail_service.users()
            .messages()
            .get(userId="me", id=msg["id"], format="metadata", metadataHeaders=["Subject", "From", "Date"])
            .execute()
        )
        headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
        finance_emails.append(
            {
                "subject": headers.get("Subject", "No Subject"),
                "from": headers.get("From", "Unknown"),
                "date": headers.get("Date", "Unknown"),
                "snippet": detail.get("snippet", ""),
            }
        )
    return finance_emails

Your toolbox is complete. You have four functions that cover email, calendar, weather, and financial data. Next up, you'll wire all of these tools into Claude's agent loop and watch it reason across your real data to produce an intelligent daily briefing.

Build the Agent Loop and Generate Your Briefing

Your four tool functions are ready to fetch emails, calendar events, weather, and financial data. Now you need to connect them to Claude so the AI can decide which tools to call and when.

The pattern you're about to build is called an agent loop. Claude receives a prompt, decides it needs data, requests specific tool calls, you execute them and send results back, and Claude keeps looping until it has enough information to write your briefing.

In this step, get ready to:
  • Define Claude-compatible tool schemas that describe each tool's purpose and parameters.
  • Implement the agent loop that lets Claude call tools repeatedly until it produces a final answer.
  • Run the agent and watch Claude reason across your real data to generate a daily briefing.
Define your tool schemas for Claude

Claude's tool use feature works by giving the model a list of tool definitions. Each definition describes a tool's name, what it does, and what parameters it accepts. Claude reads these schemas and decides which tools to call based on the user's request.

  • Create a new file called agent.py in your project folder.
  • Add the imports and client setup by pasting this code:
import json
import anthropic
from auth import get_google_credentials, get_gmail_service, get_calendar_service
from tools import get_recent_emails, get_todays_events, get_weather, get_financial_emails

# Create the Anthropic client (reads ANTHROPIC_API_KEY from your environment)
client = anthropic.Anthropic()

What does this code do?

The import json line lets you convert Python dictionaries to JSON strings (needed when sending tool results back to Claude).

The anthropic.Anthropic() call creates a client that automatically reads your ANTHROPIC_API_KEY environment variable for authentication.

  • Confirm the file exists by running:
ls agent.py

You should see agent.py listed in the output.

  • Next, add the TOOLS list below the client line. This defines all four tool schemas that Claude can call:
TOOLS = [
    {
        "name": "get_recent_emails",
        "description": "Fetches the most recent emails from the user's Gmail inbox. Returns subject, sender, date, and a snippet for each email.",
        "input_schema": {
            "type": "object",
            "properties": {
                "max_results": {
                    "type": "integer",
                    "description": "Maximum number of emails to retrieve. Default is 10.",
                }
            },
            "required": [],
        },
    },
    {
        "name": "get_todays_events",
        "description": "Fetches all calendar events for today from the user's Google Calendar. Returns event title, start time, end time, and location.",
        "input_schema": {
            "type": "object",
            "properties": {},
            "required": [],
        },
    },
    {
        "name": "get_weather",
        "description": "Gets current weather conditions and hourly forecast for a given location. Returns temperature, apparent temperature, weather code, and precipitation probability.",
        "input_schema": {
            "type": "object",
            "properties": {
                "latitude": {
                    "type": "number",
                    "description": "Latitude of the location (e.g., 37.7749 for San Francisco).",
                },
                "longitude": {
                    "type": "number",
                    "description": "Longitude of the location (e.g., -122.4194 for San Francisco).",
                },
            },
            "required": ["latitude", "longitude"],
        },
    },
    {
        "name": "get_financial_emails",
        "description": "Searches Gmail for recent bank notifications, transaction alerts, payment confirmations, and spending receipts from the past 7 days. Returns subject, sender, date, and snippet for each financial email.",
        "input_schema": {
            "type": "object",
            "properties": {
                "max_results": {
                    "type": "integer",
                    "description": "Maximum number of financial emails to retrieve. Default is 20.",
                }
            },
            "required": [],
        },
    },
]

What are tool schemas?

Each schema has three parts: a name (must match your Python function), a description (tells Claude what the tool does), and an input_schema (defines what parameters Claude can pass in).

Claude reads these descriptions to decide which tools are relevant for a given request. The better your descriptions, the smarter Claude's tool choices become.

  • Below the TOOLS list, add the execute_tool function that dispatches Claude's tool requests to your actual tool functions:
def execute_tool(tool_name, tool_input, gmail_service, calendar_service):
    # Route each tool call to the correct function
    if tool_name == "get_recent_emails":
        max_results = tool_input.get("max_results", 10)
        return get_recent_emails(gmail_service, max_results)
    elif tool_name == "get_todays_events":
        return get_todays_events(calendar_service)
    elif tool_name == "get_weather":
        return get_weather(tool_input["latitude"], tool_input["longitude"])
    elif tool_name == "get_financial_emails":
        max_results = tool_input.get("max_results", 20)
        return get_financial_emails(gmail_service, max_results)
    else:
        return {"error": f"Unknown tool: {tool_name}"}

What does execute_tool do?

When Claude requests a tool call, it sends a tool_name and tool_input dictionary. This function maps those to your real Python functions and returns the result.

The else branch handles unexpected tool names gracefully by returning an error message instead of crashing.

  • Save agent.py. You won't see a visible change yet. The next section adds the agent loop that uses these schemas.
Implement the agent loop

The agent loop is the core pattern that makes an AI agent different from a simple chatbot. Instead of one request and one response, the loop keeps running until Claude has gathered enough data to produce a final answer.

What is an agent loop?

The loop follows this cycle: send a message to Claude, check if Claude wants to call a tool (indicated by a tool_use stop reason), execute the requested tool, send the results back, and repeat. Claude keeps requesting tools until it decides it has enough information to generate a text response.

This is the exact pattern used in production agent systems. It transfers directly to any LLM provider that supports tool use.

  • Add the run_agent function below execute_tool in agent.py:
def run_agent(user_prompt, gmail_service, calendar_service):
    messages = [{"role": "user", "content": user_prompt}]

    # System prompt tells Claude its role and how to format the briefing
    system_prompt = (
        "You are a personal daily briefing agent. You have access to the user's real Gmail, "
        "Google Calendar, weather data, and financial email notifications. "
        "Use the available tools to gather information, then synthesize an intelligent daily briefing. "
        "Always call multiple tools to cross-reference data and provide actionable insights. "
        "Format your final briefing with clear sections: Schedule, Email Priorities, Weather, "
        "Financial Summary, and Action Items."
    )

    # The agent loop: keep going until Claude produces a text response
    while True:
        response = client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=4096,
            system=system_prompt,
            tools=TOOLS,
            messages=messages,
        )

        # If Claude wants to call tools, execute them and send results back
        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    print(f"  Calling tool: {block.name}")
                    result = execute_tool(
                        block.name, block.input, gmail_service, calendar_service
                    )
                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": json.dumps(result, default=str),
                        }
                    )

            # Append Claude's response and the tool results to the conversation
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

        # If Claude is done reasoning, extract the final text
        else:
            final_text = ""
            for block in response.content:
                if hasattr(block, "text"):
                    final_text += block.text
            return final_text

How does the loop work?

The while True loop sends your prompt plus any accumulated tool results to Claude. When response.stop_reason == "tool_use", Claude is asking to call one or more tools. The code executes each requested tool and appends the results to the conversation history.

When Claude's stop_reason is anything other than tool_use (typically end_turn), it means Claude has finished gathering data and produced a text response. The loop exits and returns that text.

  • Save agent.py. You won't see output yet. The next section adds the main function that triggers the loop.
Run the agent and generate your briefing

Everything is in place. The main function authenticates with Google, writes a prompt asking Claude for a daily briefing, and kicks off the agent loop.

  • Add the main function at the bottom of agent.py:
def main():
    print("Authenticating with Google...")
    creds = get_google_credentials()
    gmail_service = get_gmail_service(creds)
    calendar_service = get_calendar_service(creds)

    print("Starting Daily Briefing Agent...")
    print("=" * 50)

    # Prompt that tells Claude what to gather and synthesize
    prompt = (
        "Please generate my daily briefing for today. Check my calendar for today's events, "
        "review my recent emails for anything urgent or important, check the weather for my area "
        "(use latitude 37.7749, longitude -122.4194 for San Francisco - adjust if needed), "
        "and scan my financial emails for recent spending patterns. "
        "Then synthesize everything into an actionable daily plan."
    )

    briefing = run_agent(prompt, gmail_service, calendar_service)
    print(briefing)


if __name__ == "__main__":
    main()

What does main do?

It reuses your auth.py functions to authenticate with Google, then passes a detailed prompt to run_agent. The prompt explicitly asks Claude to check all four data sources and synthesize the results.

The if __name__ == "__main__" guard ensures main() only runs when you execute the file directly, not when importing from it.

  • Save agent.py.
  • Run the agent by executing this command:
python3 agent.py

You should see Claude calling each tool one by one, followed by a formatted daily briefing with sections for Schedule, Email Priorities, Weather, Financial Summary, and Action Items.

What should I see?

The output starts with "Authenticating with Google..." and "Starting Daily Briefing Agent...", then shows lines like Calling tool: get_todays_events as Claude requests each data source. After gathering all the data, Claude produces a multi-section briefing tailored to your actual email, calendar, and weather.

The exact content depends on your real data. If you have no calendar events today, Claude will note that. If you have financial notification emails, Claude will summarize your spending patterns.

Seeing an authentication error?

If you see an error about ANTHROPIC_API_KEY, make sure your environment variable is still set in your current terminal session. Re-export it if needed.

If you see a Google authentication error, your token.json may have expired. Delete it and run agent.py again to re-authenticate through the browser.

Help me debug my agent error

✔️ Awesome, I've got everything!

Great. Double check you've saved agent.py and your agent is producing a daily briefing.

ⓧ I'd like to double check the full code

Here is the complete agent.py file:

import json
import anthropic
from auth import get_google_credentials, get_gmail_service, get_calendar_service
from tools import get_recent_emails, get_todays_events, get_weather, get_financial_emails

client = anthropic.Anthropic()

TOOLS = [
    {
        "name": "get_recent_emails",
        "description": "Fetches the most recent emails from the user's Gmail inbox. Returns subject, sender, date, and a snippet for each email.",
        "input_schema": {
            "type": "object",
            "properties": {
                "max_results": {
                    "type": "integer",
                    "description": "Maximum number of emails to retrieve. Default is 10.",
                }
            },
            "required": [],
        },
    },
    {
        "name": "get_todays_events",
        "description": "Fetches all calendar events for today from the user's Google Calendar. Returns event title, start time, end time, and location.",
        "input_schema": {
            "type": "object",
            "properties": {},
            "required": [],
        },
    },
    {
        "name": "get_weather",
        "description": "Gets current weather conditions and hourly forecast for a given location. Returns temperature, apparent temperature, weather code, and precipitation probability.",
        "input_schema": {
            "type": "object",
            "properties": {
                "latitude": {
                    "type": "number",
                    "description": "Latitude of the location (e.g., 37.7749 for San Francisco).",
                },
                "longitude": {
                    "type": "number",
                    "description": "Longitude of the location (e.g., -122.4194 for San Francisco).",
                },
            },
            "required": ["latitude", "longitude"],
        },
    },
    {
        "name": "get_financial_emails",
        "description": "Searches Gmail for recent bank notifications, transaction alerts, payment confirmations, and spending receipts from the past 7 days. Returns subject, sender, date, and snippet for each financial email.",
        "input_schema": {
            "type": "object",
            "properties": {
                "max_results": {
                    "type": "integer",
                    "description": "Maximum number of financial emails to retrieve. Default is 20.",
                }
            },
            "required": [],
        },
    },
]


def execute_tool(tool_name, tool_input, gmail_service, calendar_service):
    if tool_name == "get_recent_emails":
        max_results = tool_input.get("max_results", 10)
        return get_recent_emails(gmail_service, max_results)
    elif tool_name == "get_todays_events":
        return get_todays_events(calendar_service)
    elif tool_name == "get_weather":
        return get_weather(tool_input["latitude"], tool_input["longitude"])
    elif tool_name == "get_financial_emails":
        max_results = tool_input.get("max_results", 20)
        return get_financial_emails(gmail_service, max_results)
    else:
        return {"error": f"Unknown tool: {tool_name}"}


def run_agent(user_prompt, gmail_service, calendar_service):
    messages = [{"role": "user", "content": user_prompt}]

    system_prompt = (
        "You are a personal daily briefing agent. You have access to the user's real Gmail, "
        "Google Calendar, weather data, and financial email notifications. "
        "Use the available tools to gather information, then synthesize an intelligent daily briefing. "
        "Always call multiple tools to cross-reference data and provide actionable insights. "
        "Format your final briefing with clear sections: Schedule, Email Priorities, Weather, "
        "Financial Summary, and Action Items."
    )

    while True:
        response = client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=4096,
            system=system_prompt,
            tools=TOOLS,
            messages=messages,
        )

        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    print(f"  Calling tool: {block.name}")
                    result = execute_tool(
                        block.name, block.input, gmail_service, calendar_service
                    )
                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": json.dumps(result, default=str),
                        }
                    )

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

        else:
            final_text = ""
            for block in response.content:
                if hasattr(block, "text"):
                    final_text += block.text
            return final_text


def main():
    print("Authenticating with Google...")
    creds = get_google_credentials()
    gmail_service = get_gmail_service(creds)
    calendar_service = get_calendar_service(creds)

    print("Starting Daily Briefing Agent...")
    print("=" * 50)

    prompt = (
        "Please generate my daily briefing for today. Check my calendar for today's events, "
        "review my recent emails for anything urgent or important, check the weather for my area "
        "(use latitude 37.7749, longitude -122.4194 for San Francisco - adjust if needed), "
        "and scan my financial emails for recent spending patterns. "
        "Then synthesize everything into an actionable daily plan."
    )

    briefing = run_agent(prompt, gmail_service, calendar_service)
    print(briefing)


if __name__ == "__main__":
    main()

Your daily briefing agent is alive. You've built the exact agent loop pattern that powers production AI systems. Every major agent framework (LangChain, CrewAI, Anthropic's Agent SDK) uses this same cycle under the hood. Now you know how it works from scratch.

Secret mission

Add News Headlines

Your agent knows about your emails, calendar, weather, and finances. But what about the outside world? Extend your briefing agent with a news headlines tool that pulls from a live RSS feed, so Claude can factor breaking news into your daily plan.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project has no ongoing costs. The Anthropic API only charges when you actively run the agent, and Google Cloud in testing mode is free.

Resources you used:

  • Google Cloud project (with Gmail API and Calendar API enabled, OAuth2 credentials).
  • Anthropic API key (stored as the ANTHROPIC_API_KEY environment variable).
  • Local project folder containing agent.py, auth.py, tools.py, credentials.json, token.json, and the Python virtual environment.

Keep everything running

No action needed. Choose this if you plan to keep running your daily briefing agent or want to extend it with more tools.

  • Your project folder stays on your Desktop, ready to run with python agent.py whenever you want a fresh briefing.
  • Your credentials.json and token.json files contain OAuth secrets. Keep them private and never commit them to Git.
  • Your Anthropic API key remains active. Each run of the agent costs roughly $0.01 to $0.02 with Claude Haiku 4.5, so there is no cost unless you actively use it.

Pause - I'll come back to this later

Remove sensitive tokens so nothing stays active while you are away. You can re-authenticate later when you return.

  • Delete token.json from your project folder. This revokes your cached Google OAuth session. Next time you run the agent, it will open the browser for you to sign in again.
  • Unset your Anthropic API key from your current terminal session by running:
unset ANTHROPIC_API_KEY
  • If you added ANTHROPIC_API_KEY to your shell profile (.zshrc or .bashrc), remove or comment out that line so it does not reload next session.
  • Your Google Cloud project and Anthropic account can stay as-is. Neither incurs charges while idle.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each item below to fully clean up.

Delete your local project folder
  • Open your terminal and run the following command to remove the project folder from your Desktop:

macOS

rm -rf ~/Desktop/daily-briefing-agent

Windows

rmdir /s /q %USERPROFILE%\Desktop\daily-briefing-agent

This removes all Python files, the virtual environment, credentials.json, and token.json.

Delete your Anthropic API key
  • Go to console.anthropic.com and sign in.
  • Click API Keys in the left sidebar.
  • Find the key you created for this project and click the delete icon next to it.
  • Unset the environment variable from your terminal and shell profile as described in the Pause tab above.
Delete your Google Cloud project
  • Go to console.cloud.google.com and select the project you created for this tutorial.
  • Open the navigation menu and go to IAM & Admin, then Settings.
  • Click Shut Down and confirm. Google keeps the project in a pending-deletion state for 30 days before permanent removal.

Nice Work!

Nice Work!

You just built a personal daily briefing agent powered by Claude Haiku 4.5. Your agent connects to real data sources, reasons about what it finds, and produces an intelligent daily plan tailored to your life.

You've learned how to:

  • Build a working agent loop where Claude decides which tools to call, you execute them, return results, and repeat until the LLM produces a final response.
  • Connect to real Gmail and Google Calendar using OAuth2 authentication, plus a free weather API with zero configuration.
  • Build a finance scanner tool that searches Gmail for bank notification emails and extracts spending patterns for Claude to analyze.
  • Secret Mission: Extended the agent with a news headlines tool that fetches and parses RSS feeds using XML parsing.

Ready to quiz yourself?