Build an AI Email-to-Todo Agent
Use CrewAI and Gemini to turn unread Gmail into a prioritized todo list.
Introduction
30 Second Summary
Your inbox is full of tasks hiding in plain sight. Buried in those unread emails are meeting requests, follow-ups, and action items that never make it onto a proper list.
In this project, you will build an AI agent crew that reads your Gmail inbox and produces a prioritized todo list as a markdown file. The entire system runs with a single command and costs nothing thanks to Google Gemini's free tier.
What You'll Build
A two-agent AI system built with CrewAI that fetches unread emails via the Gmail API and uses Google Gemini to analyze them into an actionable, prioritized todo list.
By the end of this project, you'll have:
- A CrewAI project with two specialized agents working in sequence to process your emails.
- A custom Gmail Reader tool that authenticates with the Gmail API using OAuth2.
- A one-command workflow that generates a prioritized markdown todo list from your unread emails.
- Secret Mission: Add smart categorization that groups tasks by type and includes a summary count header.
Are there any prerequisites?
You'll need basic Python knowledge, a Gmail account, and a free Google Cloud account. No paid subscriptions or credit cards are required.
Install Tools and Create Your Project
Before you can build your AI agent crew, you need the right tools in place. CrewAI uses uv as its package manager, and the CrewAI CLI to scaffold and run projects.
In this step, you will install both tools, scaffold your project folder, grab a free Gemini API key, and add the libraries your agents will need.
In this step, get ready to:
- Install uv and the CrewAI CLI.
- Scaffold a new CrewAI project with the classic YAML structure.
- Get a Gemini API key and add project dependencies.
Install uv and the CrewAI CLI
uv is a fast Python package manager that CrewAI relies on for dependency management and running tools. The CrewAI CLI gives you commands to scaffold, install, and run agent projects.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Install uv by running this command:
macOS
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
What does this command do?
This downloads and runs the official uv installer from Astral. It places the uv binary on your system PATH so you can use it from any terminal session.
- Close and reopen your terminal so the PATH changes take effect.
- Install the CrewAI CLI by running this command:
uv tool install crewai
What does this command do?
uv tool install installs a Python CLI tool globally so you can run crewai commands from any directory.
- Verify both tools are installed by running:
uv tool list
✔️ I see crewai in the list
You are all set. The output shows crewai with a version number, confirming the CLI is ready to use.
ⓧ Command not found or crewai not listed
If uv itself is not found, make sure you closed and reopened your terminal after the install step.
If uv tool list runs but crewai is not in the output, re-run uv tool install crewai and check for error messages.
help me troubleshoot why crewai is not showing up in uv tool list
Scaffold your CrewAI project
CrewAI's CLI can generate an entire project structure for you in one command. You will use the --classic flag to get the YAML-based scaffold that uses crew.py, config/agents.yaml, and config/tasks.yaml.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your CrewAI project by running this command:
crewai create crew email_todo_crew --classic
What happens during this command?
The CLI will prompt you interactively. Here is what to expect:
- It shows a list of LLM providers. Select Google Gemini.
- It shows available models for that provider. Pick the suggested model.
- It asks for your API key. You can press Enter to skip this for now (you will add it manually in the next substep).
- Move into your new project folder by running:
cd email_todo_crew
- Confirm the project structure exists by running:
ls src/email_todo_crew/
You should see files including __init__.py, main.py, crew.py, and folders tools/ and config/.
Don't see the expected files?
Make sure you included the --classic flag when running the create command. Without it, CrewAI may generate a JSON-first structure instead of the YAML-based one this project uses.
help me fix my CrewAI project scaffold
Get a Gemini API key and add dependencies
Google Gemini offers a completely free API tier for its Flash models. No credit card is required.
- Open your browser and go to Google AI Studio.
- Sign in with your Google account if prompted.
- Click Create API Key.
- Copy the generated key to your clipboard.
Why is this free?
Google Gemini provides free input and output tokens for select models including gemini-2.0-flash. This means your entire project runs at zero cost.
- Open PyCharm by pressing Cmd+Space (macOS) or the Windows key (Windows), typing PyCharm, and pressing Enter.
- In PyCharm, click File in the top menu bar.
- Click Open.
- Navigate to your Desktop and select the email_todo_crew folder, then click OK.
- In the project sidebar, find and open the .env file at the project root.
- Replace the contents of .env with the following:
GEMINI_API_KEY=your-gemini-api-key-here
- Replace your-gemini-api-key-here with the API key you copied from Google AI Studio.
- Save the file.
What is GEMINI_API_KEY?
CrewAI's native Gemini integration looks for this environment variable automatically. When your agents run, CrewAI reads GEMINI_API_KEY from .env and uses it to authenticate with Google's API.
- Back in your terminal (still inside the email_todo_crew folder), add the native Gemini integration by running:
uv add "crewai[gemini]"
What does this install?
The crewai[gemini] extra installs CrewAI's native Google Gemini SDK integration. This connects directly to the Gemini API without needing LiteLLM as a middleman.
- Add the Gmail API libraries by running:
uv add google-api-python-client google-auth-httplib2 google-auth-oauthlib
What are these libraries for?
- google-api-python-client is the core Google API client that lets you interact with Gmail.
- google-auth-httplib2 handles HTTP authentication transport.
- google-auth-oauthlib manages the OAuth2 flow that lets your tool read Gmail on your behalf.
- Verify the packages were added by opening pyproject.toml in PyCharm. You should see crewai[gemini], google-api-python-client, google-auth-httplib2, and google-auth-oauthlib listed under dependencies.
Seeing a build error on Windows?
If you see an error mentioning a failed build for a C++ dependency, you may need to install Visual Studio Build Tools with the "Desktop development with C++" workload. This project does not use ChromaDB or memory features, so this error is unlikely.
help me fix a build error when running uv add
✔️ Awesome, I've got everything!
Great. Double check you have saved your .env file with your Gemini API key.
ⓧ I'd like to double check the full code
Your .env file should look like this (with your real key in place of the placeholder):
GEMINI_API_KEY=your-gemini-api-key-here
Your development environment is ready and your project has everything it needs to connect to both Gemini and Gmail. Next up, you will set up Gmail API access so your agent can actually read your inbox.
Set Up Gmail API Access
Your CrewAI project has all the code dependencies it needs. Now you need to give it permission to read your Gmail inbox.
Google protects access to your email with OAuth2 authentication. In this step, you will create a Google Cloud project, turn on the Gmail API, and generate the credentials your agent needs to read emails securely.
In this step, get ready to:
- Create a Google Cloud project with the Gmail API enabled.
- Create OAuth 2.0 credentials and download them to your project.
- Authorize your app and confirm it can access Gmail.
Create a Google Cloud project and enable the Gmail API
The Gmail API lives inside Google Cloud. You need a project there to organize access and credentials.
- Open your browser and go to the Google Cloud Console.
- Sign in with the Google account that has the Gmail inbox you want to read.
- At the top of the page, click the project dropdown (it may say Select a project).
- Click New Project.
- Enter Email Todo Agent as the project name.
- Click Create.
- Wait a few seconds for the project to be created, then select it from the notification that appears at the top right.
Now enable the Gmail API for this project.
- Click the hamburger Menu (three horizontal lines) in the top left corner.
- Navigate to APIs & Services, then click Library.
- In the search bar, type Gmail API and press Enter.
- Click the Gmail API result.
- Click Enable.
What is OAuth2?
OAuth2 is an industry-standard protocol that lets applications access user data without ever seeing the user's password. Instead, the user grants permission through a browser-based consent screen, and the app receives a temporary token.
For this project you will use the gmail.readonly scope, which means your agent can read emails but never send, modify, or delete them.
Next, configure the OAuth consent screen so Google knows what your app is and who can use it.
- Click the hamburger Menu again.
- Navigate to Google Auth platform, then click Branding.
- If you see a message that says Google Auth platform not configured yet, click Get Started.
- Under App Information, enter Email Todo 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. If you agree, select the checkbox.
- Click Continue, then click Create.
Because you selected External, you need to add yourself as a test user.
- Click Audience in the left sidebar.
- Under Test users, click Add users.
- Enter your Gmail address and click Save.
Why External with Testing status?
Setting the audience to External with a Testing publishing status means your app does not need to go through Google's verification process. Up to 100 test users can authorize the app, which is perfect for personal projects.
The publishing status defaults to Testing for new projects, so you do not need to change anything else here.
Create OAuth 2.0 credentials and download them
Now you need a credentials file that your Python script can use to identify itself to Google during the OAuth flow.
- In the left sidebar under Google Auth platform, click Clients.
- Click Create Client.
- For Application type, select Desktop app.
- In the Name field, enter Email Todo Agent Desktop.
- Click Create.
- A dialog appears showing your new client ID. Click the Download button to download the JSON file.
- Rename the downloaded file to credentials.json.
- Move credentials.json into your email_todo_crew project folder (the top-level folder, next to pyproject.toml).
Keep credentials.json private
This file contains your client secret. Never commit it to version control. The .gitignore file in your CrewAI project already ignores common credential files, but double check that credentials.json is listed there.
Don't see the download button?
If you dismissed the dialog, click the credential name under OAuth 2.0 Client IDs in the Clients list. On the details page, use the download icon at the top to get the JSON file.
help me find and download my OAuth credentials JSON
Run a test script to authorize access
The first time your app connects to Gmail, Google opens a browser window asking you to grant permission. After you approve, a token.json file is saved so future runs skip this step entirely.
- In PyCharm, create a new file called test_gmail.py in the email_todo_crew project root (the same folder where credentials.json lives).
- Paste the following code into test_gmail.py:
import os.path
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
# Only requesting read-only access to Gmail
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
def main():
creds = None
# Check if we already have a saved token
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
# If no valid credentials, run the OAuth flow
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)
# Save the token for future runs
with open("token.json", "w") as token:
token.write(creds.to_json())
# Build the Gmail service and list labels to confirm access
service = build("gmail", "v1", credentials=creds)
results = service.users().labels().list(userId="me").execute()
labels = results.get("labels", [])
if not labels:
print("No labels found.")
else:
print("Gmail labels:")
for label in labels:
print(f" - {label['name']}")
if __name__ == "__main__":
main()
What does this script do?
- It checks whether a token.json file already exists. If not, it opens a browser window for you to log in and authorize access.
- After authorization, it saves the token so you never need to approve again (until the token expires after 7 days in Testing mode).
- It calls the Gmail API to list your inbox labels, proving that authentication works end to end.
- Run the test script from the project root by running this command in the PyCharm terminal:
cd email_todo_crew
python test_gmail.py
Your default browser will open a Google sign-in page.
- Sign in with the same Google account you added as a test user.
- You may see a warning saying the app is not verified. Click Continue (you may need to click Advanced first to reveal this option).
- Grant the requested Gmail readonly permission by clicking Continue.
- Return to your terminal. You should see a list of your Gmail labels printed (for example: INBOX, SENT, DRAFT, SPAM).
Why does it say the app is unverified?
Because your project uses the Testing publishing status, Google shows a warning to anyone who is not in the test users list. Since you added your own email as a test user, clicking Continue is safe. This warning disappears if you publish and verify the app, but that is unnecessary for personal use.
- Confirm that token.json now exists in the email_todo_crew folder by running:
ls token.json
You should see token.json listed in the output.
Browser did not open or script hangs?
- Make sure you are running the script from inside the email_todo_crew folder where credentials.json lives. The script looks for it in the current working directory.
- If the browser opened but showed an error about a redirect URI mismatch, confirm your credential type is Desktop app (not Web application).
- On Windows, if the port is blocked by a firewall, try temporarily disabling it for the authorization step.
help me debug my Gmail OAuth flow
One-time setup, permanent access
Now that token.json exists, every future run of your CrewAI agent will authenticate silently. This is what makes the final product "plug and play" for stakeholders who just run crewai run without touching Google Cloud.
Your credentials are locked in and tested. Next up, you will build the custom Gmail Reader tool that your CrewAI agent uses to fetch real emails.
Build the Gmail Reader Tool
Your Gmail API credentials are ready and tested. Now it's time to give your AI agent the ability to actually use them.
In CrewAI, agents interact with the outside world through tools. You'll build a custom tool class that wraps all the Gmail API logic into a single, reusable component your Email Reader agent can call.
In this step, get ready to:
- Create the GmailReaderTool class that extends CrewAI's BaseTool.
- Implement the _run method to authenticate and fetch unread emails.
- Export the tool from the package's __init__.py module.
Create the GmailReaderTool class file
CrewAI custom tools extend BaseTool and require two class-level fields: a name and a description. The description tells the agent when and how to use the tool.
- In PyCharm, navigate to src/email_todo_crew/tools/ in the project sidebar.
- Create a new file called gmail_tool.py inside this folder.
- Paste the following code to define the tool's imports, scope, and class shell:
import os.path
from crewai.tools import BaseTool
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
# The readonly scope limits access to reading emails only
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
class GmailReaderTool(BaseTool):
name: str = "Gmail Reader"
description: str = (
"Reads the latest unread emails from Gmail inbox. "
"Returns sender, subject, and preview for each unread email. "
"Pass any string to invoke (e.g. 'fetch')."
)
What does this code do?
The imports bring in the same Google auth libraries you tested earlier, plus CrewAI's BaseTool base class.
SCOPES restricts the tool to read-only Gmail access. The name and description fields tell CrewAI's agent executor what this tool does and when to use it.
You won't see a change yet. The next chunk adds the method that makes this tool functional.
Implement the _run method
The _run method is what CrewAI calls when the agent decides to use your tool. It has two jobs: authenticate with Gmail using your saved token, then fetch and format unread emails.
- In gmail_tool.py, add the following method inside the GmailReaderTool class, directly below the description field:
def _run(self, argument: str = "fetch") -> str:
# Load existing OAuth credentials from token file
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
# Refresh or re-authorize if credentials are invalid
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())
What does this authentication block do?
This block loads the token.json file you created in the previous step. If the token has expired, it refreshes it automatically using the refresh token.
If no valid token exists at all, it falls back to the browser-based OAuth2 flow. This means your first run always works silently thanks to the token you already generated.
You won't see a change yet. The next chunk adds the API logic that fetches your emails.
- Still inside the _run method, add this code directly below the line token.write(creds.to_json()) (keeping the same indentation level as the creds = None line above):
try:
# Build the Gmail API service client
service = build("gmail", "v1", credentials=creds)
# Fetch up to 10 unread inbox messages
results = (
service.users()
.messages()
.list(userId="me", labelIds=["INBOX"], q="is:unread", maxResults=10)
.execute()
)
messages = results.get("messages", [])
if not messages:
return "No unread emails found in inbox."
# Extract sender, subject, and preview from each message
email_summaries = []
for message in messages:
msg = (
service.users()
.messages()
.get(userId="me", id=message["id"])
.execute()
)
headers = msg.get("payload", {}).get("headers", [])
subject = next(
(h["value"] for h in headers if h["name"] == "Subject"),
"No Subject",
)
sender = next(
(h["value"] for h in headers if h["name"] == "From"),
"Unknown Sender",
)
snippet = msg.get("snippet", "")
email_summaries.append(
f"From: {sender}\nSubject: {subject}\nPreview: {snippet}\n---"
)
return "\n".join(email_summaries)
except HttpError as error:
return f"An error occurred while reading Gmail: {error}"
What does this API logic do?
The try block builds a Gmail service client, then queries for up to 10 unread messages in your inbox using the is:unread filter.
For each message, it fetches the full details and extracts the Subject, From header, and snippet (preview text). Everything gets formatted into a readable string the agent can analyze.
- Save gmail_tool.py.
- Check that your file has no red underlines or syntax errors in PyCharm's editor.
Seeing red underlines on the imports?
If PyCharm shows unresolved import errors for crewai.tools or the Google libraries, make sure your PyCharm interpreter is set to the project's virtual environment (the .venv folder inside email_todo_crew).
help me fix unresolved imports in my CrewAI project in PyCharm
✔️ Awesome, I've got everything!
Great. Double check you've saved gmail_tool.py before moving on.
ⓧ I'd like to double check the full code
import os.path
from crewai.tools import BaseTool
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
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
class GmailReaderTool(BaseTool):
name: str = "Gmail Reader"
description: str = (
"Reads the latest unread emails from Gmail inbox. "
"Returns sender, subject, and preview for each unread email. "
"Pass any string to invoke (e.g. 'fetch')."
)
def _run(self, argument: str = "fetch") -> str:
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())
try:
service = build("gmail", "v1", credentials=creds)
results = (
service.users()
.messages()
.list(userId="me", labelIds=["INBOX"], q="is:unread", maxResults=10)
.execute()
)
messages = results.get("messages", [])
if not messages:
return "No unread emails found in inbox."
email_summaries = []
for message in messages:
msg = (
service.users()
.messages()
.get(userId="me", id=message["id"])
.execute()
)
headers = msg.get("payload", {}).get("headers", [])
subject = next(
(h["value"] for h in headers if h["name"] == "Subject"),
"No Subject",
)
sender = next(
(h["value"] for h in headers if h["name"] == "From"),
"Unknown Sender",
)
snippet = msg.get("snippet", "")
email_summaries.append(
f"From: {sender}\nSubject: {subject}\nPreview: {snippet}\n---"
)
return "\n".join(email_summaries)
except HttpError as error:
return f"An error occurred while reading Gmail: {error}"
Export the tool from __init__.py
Python packages use __init__.py to declare what's available when other modules import from the package. You need to register your new tool here so CrewAI can find it.
- Open src/email_todo_crew/tools/__init__.py in PyCharm.
- Replace any existing content with this single import line:
from email_todo_crew.tools.gmail_tool import GmailReaderTool
What does this line do?
This makes GmailReaderTool available at the package level. When crew.py imports from email_todo_crew.tools, it can access the tool directly without knowing the internal file structure.
- Save __init__.py.
- Open the terminal in PyCharm (click **Terminal** at the bottom of the window).
- Verify the import works by running this command:
cd src && python -c "from email_todo_crew.tools.gmail_tool import GmailReaderTool; print('Import successful')" && cd ..
- You should see Import successful printed in the terminal.
Seeing a ModuleNotFoundError?
Make sure you're running the command from the email_todo_crew project root (the folder containing pyproject.toml). The cd src prefix in the command navigates to the right location.
Also verify that your terminal is using the project's virtual environment. If not, activate it first with the command shown in your project's README.
help me fix ModuleNotFoundError when importing my GmailReaderTool
- Now test that the tool can actually fetch your emails by running:
cd src && python -c "from email_todo_crew.tools.gmail_tool import GmailReaderTool; tool = GmailReaderTool(); print(tool._run())" && cd ..
- You should see your actual unread email summaries printed in the terminal, each showing the sender, subject, and preview text.
What if I have no unread emails?
If your inbox is empty, you'll see No unread emails found in inbox. printed. That's a valid success response. Send yourself a test email and re-run the command to see the full output.
Your custom tool is built and tested. Next up, you'll define your two AI agents and wire them together into a crew that turns those raw email summaries into an actionable todo list.
Define Agents, Tasks, and Run the Crew
Your custom Gmail tool is built and tested. Now it is time to put it to work inside an AI agent crew.
In this step, you will define two specialized agents in YAML configuration files, give them tasks, and wire everything together so a single command produces a prioritized todo list from your unread emails.
In this step, get ready to:
- Define two agents (Email Reader and Task Extractor) in the agents configuration file.
- Define sequential tasks that fetch emails and extract action items.
- Wire the crew together and run it with a single command.
Define your two agents in the configuration
CrewAI agents are defined in a YAML file. Each agent has a role, a goal, a backstory, and an LLM to power its reasoning.
- In PyCharm, open src/email_todo_crew/config/agents.yaml.
- Select all the existing scaffold content and delete it.
- Replace it with the following agent definitions:
email_reader:
role: >
Email Reader
goal: >
Fetch all unread emails from the Gmail inbox and provide their
complete details including sender, subject, and preview text
backstory: >
You are an efficient email assistant that quickly retrieves and
organizes incoming emails for review. You never miss an email
and always present information in a clear, structured format.
llm: gemini/gemini-2.0-flash
task_extractor:
role: >
Task Extractor and Prioritizer
goal: >
Analyze emails and extract actionable tasks, organizing them into
a clear prioritized todo list that a busy stakeholder can act on
immediately
backstory: >
You are an executive assistant with years of experience helping
busy professionals stay on top of their workload. You excel at
identifying action items hidden in emails and presenting them
in a clear, actionable format with appropriate priority levels.
You know the difference between urgent requests and FYI messages.
llm: gemini/gemini-2.0-flash
What does this configuration do?
You are defining two agents that work as a team. The email_reader agent is responsible for fetching raw email data using the Gmail tool you built. The task_extractor agent analyzes those emails and produces a prioritized todo list.
The backstory field gives each agent a persona that guides how it reasons. The llm field tells CrewAI to use Google Gemini's free Flash model for both agents.
The > character in YAML is a folded scalar. It lets you write multi-line text that gets treated as a single string.
- Save agents.yaml.
Seeing indentation errors later?
YAML is sensitive to indentation. Make sure each field (role, goal, backstory, llm) is indented with exactly two spaces under its agent name. Do not mix tabs and spaces.
help me fix YAML indentation errors in my CrewAI agents.yaml file
✔️ Awesome, I've got everything!
Great. Make sure agents.yaml is saved before moving on.
ⓧ I'd like to double check the full code
email_reader:
role: >
Email Reader
goal: >
Fetch all unread emails from the Gmail inbox and provide their
complete details including sender, subject, and preview text
backstory: >
You are an efficient email assistant that quickly retrieves and
organizes incoming emails for review. You never miss an email
and always present information in a clear, structured format.
llm: gemini/gemini-2.0-flash
task_extractor:
role: >
Task Extractor and Prioritizer
goal: >
Analyze emails and extract actionable tasks, organizing them into
a clear prioritized todo list that a busy stakeholder can act on
immediately
backstory: >
You are an executive assistant with years of experience helping
busy professionals stay on top of their workload. You excel at
identifying action items hidden in emails and presenting them
in a clear, actionable format with appropriate priority levels.
You know the difference between urgent requests and FYI messages.
llm: gemini/gemini-2.0-flash
Define the sequential tasks
Tasks tell each agent what to do and what output to produce. Your two tasks form a pipeline: the first fetches emails, and the second analyzes them.
- Open src/email_todo_crew/config/tasks.yaml.
- Select all the existing scaffold content and delete it.
- Replace it with the following task definitions:
fetch_emails_task:
description: >
Use the Gmail Reader tool to fetch the latest unread emails from
the inbox. Return all email details including sender, subject,
and preview text exactly as the tool provides them.
expected_output: >
A complete list of unread emails with sender, subject line, and
preview content for each email, separated by dividers.
agent: email_reader
extract_todos_task:
description: >
Analyze the emails provided and extract all actionable tasks.
For each task identify:
- What needs to be done (clear action item)
- Who requested it (sender name)
- Any deadline mentioned
- Priority level (High, Medium, or Low)
Rules for priority:
- High: explicit deadlines, urgent language, from leadership
- Medium: requests that need a response, meeting invites
- Low: FYI messages, newsletters, non-urgent updates
Format the output as a clean markdown todo list organized by
priority that a busy stakeholder can review in under 2 minutes.
expected_output: >
A markdown-formatted todo list with tasks grouped by priority
(High, Medium, Low). Each task includes a checkbox, the action
item, requester name, and deadline if mentioned. Example format:
## High Priority
- [ ] Review Q3 budget proposal (from: Jane Smith, due: Friday)
## Medium Priority
- [ ] Reply to client meeting request (from: Bob Lee)
## Low Priority
- [ ] Read team newsletter (from: HR Department)
agent: task_extractor
What does this configuration do?
The fetch_emails_task tells the email_reader agent to use the Gmail Reader tool and return raw email data. The extract_todos_task tells the task_extractor agent to analyze those emails and produce a markdown todo list.
The expected_output field is critical. It gives the LLM a concrete example of the format you want. The more specific this is, the more consistent your output will be.
The agent field links each task to the agent responsible for completing it. CrewAI runs them sequentially, passing the output of one task as context to the next.
- Save tasks.yaml.
Task not producing the right format?
If the todo list output does not match the expected format, check that the expected_output field in tasks.yaml includes the example with checkboxes and priority headers. The LLM uses this as a template.
help me debug my CrewAI task output format
✔️ Awesome, I've got everything!
Great. Make sure tasks.yaml is saved before moving on.
ⓧ I'd like to double check the full code
fetch_emails_task:
description: >
Use the Gmail Reader tool to fetch the latest unread emails from
the inbox. Return all email details including sender, subject,
and preview text exactly as the tool provides them.
expected_output: >
A complete list of unread emails with sender, subject line, and
preview content for each email, separated by dividers.
agent: email_reader
extract_todos_task:
description: >
Analyze the emails provided and extract all actionable tasks.
For each task identify:
- What needs to be done (clear action item)
- Who requested it (sender name)
- Any deadline mentioned
- Priority level (High, Medium, or Low)
Rules for priority:
- High: explicit deadlines, urgent language, from leadership
- Medium: requests that need a response, meeting invites
- Low: FYI messages, newsletters, non-urgent updates
Format the output as a clean markdown todo list organized by
priority that a busy stakeholder can review in under 2 minutes.
expected_output: >
A markdown-formatted todo list with tasks grouped by priority
(High, Medium, Low). Each task includes a checkbox, the action
item, requester name, and deadline if mentioned. Example format:
## High Priority
- [ ] Review Q3 budget proposal (from: Jane Smith, due: Friday)
## Medium Priority
- [ ] Reply to client meeting request (from: Bob Lee)
## Low Priority
- [ ] Read team newsletter (from: HR Department)
agent: task_extractor
Wire the crew together and run it
The YAML files define what the agents are and what they do. Now you need Python code to load those definitions, attach your custom tool, and orchestrate the whole crew.
- Open src/email_todo_crew/crew.py.
- Select all the existing scaffold content and delete it.
- Replace it with the following crew definition:
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from email_todo_crew.tools.gmail_tool import GmailReaderTool
@CrewBase
class EmailTodoCrew:
"""Email Todo Crew - reads Gmail and creates a prioritized todo list."""
agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml"
@agent
def email_reader(self) -> Agent:
return Agent(
config=self.agents_config["email_reader"],
tools=[GmailReaderTool()],
verbose=True,
)
@agent
def task_extractor(self) -> Agent:
return Agent(
config=self.agents_config["task_extractor"],
verbose=True,
)
@task
def fetch_emails_task(self) -> Task:
return Task(
config=self.tasks_config["fetch_emails_task"],
)
@task
def extract_todos_task(self) -> Task:
return Task(
config=self.tasks_config["extract_todos_task"],
output_file="output/todo_list.md",
)
@crew
def crew(self) -> Crew:
"""Creates the Email Todo crew."""
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential,
verbose=True,
)
What does this code do?
The @CrewBase decorator automatically loads your YAML config files. When you reference self.agents_config["email_reader"], CrewAI pulls the role, goal, and backstory from agents.yaml.
The email_reader agent gets the GmailReaderTool attached via the tools parameter. The task_extractor agent does not need tools because it only analyzes text that the first agent provides.
The extract_todos_task has an output_file parameter that saves the final result to output/todo_list.md. The Process.sequential setting means tasks run one after another in the order they are listed.
- Save crew.py.
✔️ Awesome, I've got everything!
Great. Make sure crew.py is saved before moving on to main.py.
ⓧ I'd like to double check the full code
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from email_todo_crew.tools.gmail_tool import GmailReaderTool
@CrewBase
class EmailTodoCrew:
"""Email Todo Crew - reads Gmail and creates a prioritized todo list."""
agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml"
@agent
def email_reader(self) -> Agent:
return Agent(
config=self.agents_config["email_reader"],
tools=[GmailReaderTool()],
verbose=True,
)
@agent
def task_extractor(self) -> Agent:
return Agent(
config=self.agents_config["task_extractor"],
verbose=True,
)
@task
def fetch_emails_task(self) -> Task:
return Task(
config=self.tasks_config["fetch_emails_task"],
)
@task
def extract_todos_task(self) -> Task:
return Task(
config=self.tasks_config["extract_todos_task"],
output_file="output/todo_list.md",
)
@crew
def crew(self) -> Crew:
"""Creates the Email Todo crew."""
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential,
verbose=True,
)
Now set up the entry point that launches the crew.
- Open src/email_todo_crew/main.py.
- Select all the existing scaffold content and delete it.
- Replace it with the following entry point code:
#!/usr/bin/env python
from email_todo_crew.crew import EmailTodoCrew
def run():
"""Run the Email Todo crew."""
crew = EmailTodoCrew()
result = crew.crew().kickoff()
print("\n=== YOUR TODO LIST HAS BEEN GENERATED ===")
print("Check: output/todo_list.md")
print("\n--- Preview ---\n")
print(result.raw)
if __name__ == "__main__":
run()
What does this code do?
The run() function is the entry point that the crewai run command calls. It creates an instance of your crew class, kicks off the sequential pipeline, and prints a preview of the result to your terminal.
The full output is also saved to output/todo_list.md by the output_file setting you configured in crew.py.
- Save main.py.
✔️ Awesome, I've got everything!
Great. Make sure main.py is saved.
ⓧ I'd like to double check the full code
#!/usr/bin/env python
from email_todo_crew.crew import EmailTodoCrew
def run():
"""Run the Email Todo crew."""
crew = EmailTodoCrew()
result = crew.crew().kickoff()
print("\n=== YOUR TODO LIST HAS BEEN GENERATED ===")
print("Check: output/todo_list.md")
print("\n--- Preview ---\n")
print(result.raw)
if __name__ == "__main__":
run()
All four files are ready. Before running the crew, you need to install the project dependencies so CrewAI can resolve all imports.
- Install the project dependencies by running this command in your terminal (from the email_todo_crew project root):
crewai install
- Run the crew by executing this command:
crewai run
The crew will take 30 to 60 seconds to complete. You will see verbose output in your terminal as each agent thinks, acts, and passes its results to the next.
- When it finishes, you should see the message === YOUR TODO LIST HAS BEEN GENERATED === followed by a preview of your todo list.
- Open output/todo_list.md in PyCharm to see the full prioritized todo list derived from your actual unread emails.
What should I see?
The generated todo_list.md file contains a markdown-formatted list with tasks grouped under High Priority, Medium Priority, and Low Priority headings. Each task has a checkbox, the action item, the sender's name, and a deadline if one was mentioned in the email.
The exact content depends on what unread emails are in your inbox. If you have no unread emails, the output will state that no action items were found.
Seeing an error when running the crew?
- If you see a module not found error, make sure you ran crewai install first. This command syncs your project's virtual environment with all required packages.
- If the error mentions token.json or credentials.json, make sure you are running the command from the project root directory (the email_todo_crew folder that contains pyproject.toml).
- If you see an API key error, double check that your .env file contains GEMINI_API_KEY with a valid key from Google AI Studio.
help me troubleshoot my CrewAI crew run error
Secret mission
Add Smart Categorization
Your todo list groups tasks by priority, but a busy stakeholder also needs to know what type of work is waiting. In this mission, you'll upgrade your Task Extractor agent to classify every task into categories (Meetings, Follow-ups, Action Requests, FYI) and generate a summary count header at the top of the output.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely for free. The Gemini API free tier has no billing, the Gmail API has no charges, and nothing is running in the background when idle. Decide whether to keep your resources available, pause them, or delete them entirely.
Resources you used:
- Google Cloud project with Gmail API enabled (free, no charges).
- OAuth credentials stored locally (credentials.json, token.json).
- Gemini API key stored in .env (free tier, no billing required).
- Local project folder (email_todo_crew) on your Desktop.
- CrewAI CLI installed via uv.
Keep everything running
No action needed. Choose this if you plan to keep using the agent or want to modify it later.
- The project costs nothing to keep. No services are running when idle.
- Run crewai run anytime you want a fresh todo list from your inbox.
- Your token.json will automatically refresh, so the agent keeps working without re-authorization.
Pause - I'll come back to this later
Nothing is running or costing money when idle. There is nothing to pause for this project.
- Your Google Cloud project stays available at no cost.
- Your local files remain on your Desktop until you delete them.
- When you return, open your terminal, navigate to the email_todo_crew folder, and run crewai run.
Delete - I don't want to use this again
Remove all project resources and revoke access. Follow each step below.
- Revoke the app's access to your Gmail account by visiting https://myaccount.google.com/permissions.
- Find the app name you authorized and click Remove Access.
- Delete the Google Cloud project by opening the Google Cloud Console.
- Select IAM & Admin from the navigation menu, then click Settings.
- Click Shut down, enter your project ID, then click Shut down anyway.
- Delete your Gemini API key by visiting Google AI Studio, opening the API keys page, and deleting the key you created for this project.
- Uninstall the CrewAI CLI by running this command:
uv tool uninstall crewai
- Delete the local project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf email_todo_crew
- On Windows, run:
cd ~\Desktop
rmdir /s /q email_todo_crew
That's a Wrap!
That's a Wrap!
Nice work! You just built a production-ready AI agent crew that turns inbox chaos into an actionable, prioritized todo list with a single command.
You've learned how to:
- Build a multi-agent AI system using CrewAI with two specialized agents that collaborate in a sequential workflow.
- Create a custom CrewAI tool that authenticates with the Gmail API via OAuth2 to fetch real email data.
- Configure Google Gemini as a free LLM provider for intelligent text analysis and task extraction.
- Produce a stakeholder-ready markdown todo list from unread emails using crewai run.
- Secret Mission: Added smart categorization with task type grouping (Meetings, Follow-ups, Action Requests, FYI) and a summary count header.
Ready to quiz yourself?