Build an AI Agent That Summarizes Your Emails
Learn to build a local AI agent that fetches, summarizes, and digests your emails using Python and LangGraph.
Before you start
You have an inbox full of newsletters, notifications, and important messages. Reading every email wastes time, so you want an AI agent that fetches new emails, summarizes them, and sends you a daily digest. This tutorial builds a working agent using Python, the Gmail API, and LangGraph. You will run it locally with a cron job.
You need a Google Cloud project with the Gmail API enabled, a client ID and secret for a desktop app, and Python 3.10 or newer. You also need an OpenAI API key (or any LLM provider) for summarization. The agent uses LangGraph to orchestrate steps: fetch emails, filter, summarize, and compose a digest.
- Python 3.10+ installed
- Google Cloud project with Gmail API enabled
- OAuth 2.0 credentials (client ID and secret) for a desktop app
- OpenAI API key (or another LLM provider)
- Basic knowledge of Python and async programming
Step 1: Set up the project
Create a project directory and a virtual environment. Install the required packages: google-auth, google-auth-oauthlib, google-auth-httplib2, google-api-python-client, langgraph, langchain-openai, and python-dotenv.
mkdir email-summarizer && cd email-summarizer
python -m venv venv
source venv/bin/activate
pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client langgraph langchain-openai python-dotenvStep 2: Authenticate with Gmail
Google requires OAuth 2.0 for Gmail API access. You need a credentials.json file from Google Cloud Console. Place it in the project root. Then create a script to obtain and store a token for subsequent runs.
# auth.py
import os
import pickle
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']
def get_credentials():
creds = None
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
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.pickle', 'wb') as token:
pickle.dump(creds, token)
return creds
if __name__ == '__main__':
get_credentials()
print('Authentication successful. token.pickle saved.')- Run python auth.py once to generate token.pickle.
- Keep credentials.json and token.pickle secure; add them to .gitignore.
- The scope is read-only, so the agent cannot send emails.
Step 3: Fetch emails from Gmail
Now create a module to fetch emails from the last 24 hours. The Gmail API returns message metadata, and you need to fetch the full message to get the body. We will extract the subject and a plain-text snippet for summarization.
# gmail_client.py
import base64
from googleapiclient.discovery import build
from auth import get_credentials
class GmailClient:
def __init__(self):
creds = get_credentials()
self.service = build('gmail', 'v1', credentials=creds)
def get_recent_messages(self, hours=24):
import time
query = f'newer_than:{hours}h'
results = self.service.users().messages().list(userId='me', q=query).execute()
messages = results.get('messages', [])
return messages
def get_message(self, msg_id):
msg = self.service.users().messages().get(userId='me', id=msg_id, format='full').execute()
headers = msg['payload'].get('headers', [])
subject = ''
for h in headers:
if h['name'] == 'Subject':
subject = h['value']
break
body = ''
if 'parts' in msg['payload']:
for part in msg['payload']['parts']:
if part['mimeType'] == 'text/plain':
data = part['body'].get('data', '')
if data:
body = base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore')
break
else:
data = msg['payload'].get('body', {}).get('data', '')
if data:
body = base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore')
return {'id': msg_id, 'subject': subject, 'body': body[:2000]}Step 4: Build the summarization agent
We use LangGraph to define a simple agent graph. The agent has three nodes: fetch_emails, summarize, and compose_digest. The fetch node gets messages, the summarize node calls the LLM for each email, and the compose node aggregates summaries into a final digest.
# agent.py
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
from gmail_client import GmailClient
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import os
load_dotenv()
class AgentState(TypedDict):
messages: List[dict]
summaries: List[str]
digest: str
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def fetch_emails(state: AgentState):
client = GmailClient()
msg_list = client.get_recent_messages(hours=24)
messages = []
for msg in msg_list[:10]: # limit to 10 emails
messages.append(client.get_message(msg['id']))
return {"messages": messages}
def summarize(state: AgentState):
summaries = []
for msg in state['messages']:
prompt = f"Summarize this email in 2-3 sentences. Subject: {msg['subject']}\nBody: {msg['body']}"
resp = llm.invoke(prompt)
summaries.append(f"- {msg['subject']}: {resp.content}")
return {"summaries": summaries}
def compose_digest(state: AgentState):
digest = "Daily Email Digest:\n\n" + "\n".join(state['summaries'])
return {"digest": digest}
# Build graph
workflow = StateGraph(AgentState)
workflow.add_node("fetch", fetch_emails)
workflow.add_node("summarize", summarize)
workflow.add_node("compose", compose_digest)
workflow.set_entry_point("fetch")
workflow.add_edge("fetch", "summarize")
workflow.add_edge("summarize", "compose")
workflow.add_edge("compose", END)
app = workflow.compile()
if __name__ == "__main__":
result = app.invoke({})
print(result['digest'])- The agent processes up to 10 emails per run to avoid rate limits.
- You can change the model or prompt to suit your needs.
- The graph is minimal; you can extend it with filtering or deduplication nodes.
Step 5: Run the agent manually
Before scheduling, test the agent manually. Make sure your .env file contains your OpenAI API key. Run the script and check the output digest.
echo "OPENAI_API_KEY=your-key-here" > .env
python agent.py- If you get an authentication error, delete token.pickle and run auth.py again.
- If the summary is too long, adjust the prompt or model.
- The output is printed to stdout; you can redirect it to a file.
Step 6: Schedule the agent with cron
To get a daily digest, schedule the script with cron. Use the full path to your virtual environment's Python. Redirect output to a log file so you can check for errors.
crontab -e
# Add this line to run at 7 AM daily
0 7 * * * cd /path/to/email-summarizer && /path/to/venv/bin/python agent.py >> digest.log 2>&1- Use absolute paths in cron to avoid issues.
- Make sure the script has executable permissions if needed.
- Check digest.log for errors after the first run.
Verify it worked
After the first scheduled run, check the log file and the output. You should see a list of summarized emails. If the digest is empty, confirm that you received emails in the last 24 hours and that the Gmail API scope allows reading them.
cat digest.log
# Example output
# Daily Email Digest:
# - Welcome to our newsletter: This is a test...
# - Project update: The deployment was successful...Troubleshooting
Common issues and fixes for the email summarizer agent.
- Authentication errors: Delete token.pickle and re-run auth.py.
- No messages found: The query newer_than:24h might miss emails if your timezone differs; adjust the hours or use a date range.
- Rate limiting: The Gmail API has quotas; add a delay between requests or process fewer emails.
- LLM errors: Check your API key and model availability; ensure you have credits.
What I would do
For a production setup, I would add a database to track already-processed message IDs to avoid duplicates, and use a more robust summarization model like gpt-4o for better quality. I would also add error handling and retries for the LLM calls.
# Improved fetch with deduplication using a simple JSON file
import json
def fetch_emails(state: AgentState):
client = GmailClient()
msg_list = client.get_recent_messages(hours=24)
try:
with open('processed.json', 'r') as f:
processed = set(json.load(f))
except FileNotFoundError:
processed = set()
messages = []
for msg in msg_list[:10]:
if msg['id'] not in processed:
messages.append(client.get_message(msg['id']))
processed.add(msg['id'])
with open('processed.json', 'w') as f:
json.dump(list(processed), f)
return {"messages": messages}- Use a persistent store like SQLite for larger scale.
- Consider using a local LLM (e.g., Ollama) for privacy.
- Add a notification step to send the digest via email or Slack.
FAQ
Answers to the questions that come up most often on this topic.
- Q: Can I use this with Outlook? A: Yes, you would need to adapt the email fetching part to use Microsoft Graph API, but the agent logic remains the same.
- Q: How do I summarize attachments? A: The current code only reads the plain text body. You can extend it to download attachments and extract text using libraries like PyPDF2.
- Q: Is it safe to store my Gmail token? A: The token grants read-only access, but still keep it secure. Use environment variables or a secrets manager.
- Q: What if I want to send the digest to my phone? A: You can add a push notification via ntfy.sh or use a messaging API.
Key takeaways
- Apply one concrete change from this post before collecting more reading.
- Prefer browser-side tools when the work involves secrets, tokens, or PII.
- Document the why next to the how so the next reviewer inherits context.
FAQ
- Who is this guide on ai-agent for?
- Working developers who need a practical take on build an ai agent that summarizes your emails — not a marketing overview. Skim the sections, apply one tip, then come back when you hit an edge case.
- Do I need an account to use the related tools?
- No. code.live tools run in your browser with no signup. Nothing you paste is uploaded to a server for the client-side utilities linked from this post.
- How often is this article updated?
- This post was published November 6, 2026. Fundamentals stay stable; check linked tool pages and official docs when version-specific behavior matters.