
crewai email tool: how to build a custom integration that actually works
Step-by-step guide to building a custom email tool in CrewAI, covering SMTP, API-based sending, inbound parsing, and the trade-offs between Gmail, Composio, and dedicated email infrastructure.
CrewAI ships with a Gmail integration and a handful of Composio connectors. For basic prototyping, they work. But the moment your agent needs to send transactional email at volume, handle inbound replies, or operate without a human's Gmail credentials, those built-in options fall apart fast.
I spent a week building email tools for a CrewAI crew that needed to send onboarding sequences, parse incoming verification codes, and manage threaded conversations. Here's what I learned about building a custom email tool that holds up in production.
How to create a custom email tool in CrewAI (step-by-step)#
- Install CrewAI and your preferred email SDK (e.g.,
pip install crewai lobstermailor your SMTP library). - Define a new tool function using the
@tooldecorator fromcrewai_tools. - Add your email credentials as environment variables (API keys, SMTP passwords, or tokens).
- Implement the send logic inside the decorated function, returning a success/failure string.
- Register the tool on your agent by passing it to the
toolsparameter. - Test with a mock SMTP server or sandbox mode before enabling live sending.
- Add error handling for bounces, rate limits, and authentication failures.
That's the skeleton. Now let's look at what each step actually involves and where things get complicated.
The @tool decorator pattern#
CrewAI's custom tool system revolves around a single decorator. Here's a minimal email-sending tool using SMTP:
import smtplib
from email.mime.text import MIMEText
from crewai_tools import tool
import os
@tool("send_email")
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to the specified recipient."""
msg = MIMEText(body)
msg["Subject"] = subject
msg["From"] = os.environ["SMTP_FROM"]
msg["To"] = to
with smtplib.SMTP(os.environ["SMTP_HOST"], int(os.environ["SMTP_PORT"])) as server:
server.starttls()
server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
server.send_message(msg)
return f"Email sent to {to}"
This works. It also has no retry logic, no bounce handling, no rate limiting, and no way for the agent to receive replies. For a demo, fine. For anything running unattended, you need more.
Gmail vs. Composio vs. custom tool: the real trade-offs#
CrewAI's built-in Gmail integration uses OAuth tokens tied to a human's Google account. That means your agent inherits that person's sending reputation, their rate limits (roughly 500 emails per day for consumer Gmail, 2,000 for Workspace), and their inbox. If the agent sends something embarrassing, it comes from your actual email address.
Composio adds a middleware layer. You get pre-built connectors for Gmail, Outlook, and a few others. The upside is faster setup. The downside is another dependency, another authentication flow, and opaque error handling when things break between Composio's proxy and the email provider.
A custom tool gives you full control. You pick the transport (SMTP, REST API, SDK), you handle credentials your way, and you can swap providers without rewriting your crew logic. The cost is that you're responsible for everything: connection pooling, error handling, deliverability.
Here's my honest take: if your agent sends fewer than 20 emails per day and only needs outbound, Gmail through Composio is fine. Beyond that, you want a dedicated email API.
Handling inbound email in CrewAI#
This is where most tutorials stop. Sending email is straightforward. Receiving it, parsing it, and feeding it back into your crew's workflow is a different problem entirely.
CrewAI doesn't have a native "wait for email" primitive. Your options:
Polling approach: Build a tool that checks an inbox on each invocation. The agent calls it periodically or between tasks.
@tool("check_inbox")
def check_inbox(inbox_address: str) -> str:
"""Check for new emails in the specified inbox."""
Your email SDK's receive logic here#
emails = fetch_new_emails(inbox_address) if not emails: return "No new emails." return "\n".join([f"From: , Subject: " for e in emails]) Webhook approach: Set up a webhook endpoint that receives email notifications and triggers a new crew run. This requires infrastructure outside CrewAI itself (a web server, a queue, something to bridge the event into a crew execution).
MCP server approach: If you're using CrewAI with an MCP-compatible client, you can connect an email MCP server that exposes send/receive as tools without writing Python at all. LobsterMail's MCP server, for example, gives any MCP-compatible agent full email capabilities (send, receive, create inboxes) with zero custom code.
Credentials and security#
Never hardcode email credentials. Use environment variables or a secrets manager. For a CrewAI project, a .env file with python-dotenv is the minimum:
SMTP_HOST=smtp.your-provider.com
SMTP_PORT=587
SMTP_USER=your-api-key
SMTP_PASS=your-api-secret
SMTP_FROM=agent@yourdomain.com
Load them in your crew file:
from dotenv import load_dotenv
load_dotenv()
If you're using an API-based email service instead of raw SMTP, the pattern is identical. Swap the SMTP logic for an HTTP call to your provider's endpoint.
Rate limits and deliverability at scale#
Gmail caps you at roughly 2,000 messages per day on Workspace accounts. Hit that limit and your agent is dead in the water until the next day. Worse, if Google detects automated sending patterns, they may throttle you further or flag the account.
Dedicated email APIs (SendGrid, Postmark, Mailgun, or agent-native services like LobsterMail) give you explicit rate limits, bounce notifications, and reputation monitoring. You know exactly where you stand.
For CrewAI agents sending more than 100 emails per day, I'd recommend:
- Use a dedicated sending domain (not your personal one)
- Warm the domain gradually over 2-4 weeks
- Implement exponential backoff on 4xx errors
- Track bounces and remove invalid addresses from your agent's contact list
- Monitor your domain reputation through Google Postmaster Tools
Passing email output between agents in a crew#
A common pattern: one agent researches prospects, another drafts the email, a third sends it. CrewAI's task output system handles this naturally. The drafting agent's output becomes the sending agent's input.
from crewai import Agent, Task, Crew
researcher = Agent(name="Researcher", tools=[search_tool])
writer = Agent(name="Writer", tools=[])
sender = Agent(name="Sender", tools=[send_email])
research_task = Task(
description="Find contact info for {company}",
agent=researcher
)
write_task = Task(
description="Draft a short intro email using the research",
agent=writer,
context=[research_task]
)
send_task = Task(
description="Send the drafted email to the contact",
agent=sender,
context=[write_task]
)
crew = Crew(agents=[researcher, writer, sender], tasks=[research_task, write_task, send_task])
The key is the context parameter. It chains task outputs without you manually passing strings between agents.
Testing without sending real emails#
Before letting your agent loose, test with one of these approaches:
- MailHog or Mailpit: Local SMTP servers that capture all outgoing mail. Point your SMTP credentials at
localhost:1025and inspect messages in the web UI. - Sandbox modes: Many email APIs offer a sandbox that accepts all sends but doesn't deliver. Check your provider's docs.
- Dry-run flag: Add a
dry_runparameter to your custom tool that logs the email instead of sending it.
When to skip the custom tool entirely#
If your agent only needs to send and receive email (not integrate with an existing crew's task flow), an MCP server might be simpler than writing custom Python tools. MCP servers expose email capabilities as tools that any compatible agent can call. No @tool decorators, no SMTP configuration, no credential management in your codebase.
This is the approach I'd recommend for agents built with Claude Code, Cursor, or any MCP-aware framework. You connect the email server once and the agent gets full inbox access without custom code.
For agents tightly coupled to CrewAI's task-routing system, custom tools still make sense. You get finer control over how email integrates with your crew's workflow, retry logic, and inter-agent communication.
Pick based on your architecture, not on what's trendy.
Frequently asked questions
What is the simplest way to add email-sending capability to a CrewAI agent?
Use the @tool decorator from crewai_tools to wrap a function that calls your email provider's API or SMTP server. Register the tool on your agent with the tools parameter. Five lines of code gets you basic sending.
How do I write a custom @tool decorator in CrewAI that sends an email via SMTP?
Import smtplib and the @tool decorator from crewai_tools. Define a function that accepts to, subject, and body parameters, constructs a MIMEText message, connects to your SMTP server using environment variables, and calls server.send_message(). Return a success string so the agent knows the task completed.
Can I use SendGrid, Mailgun, or Postmark inside a CrewAI custom tool?
Yes. Replace the SMTP logic with an HTTP request to your provider's REST API. Store the API key as an environment variable. The @tool decorator doesn't care what happens inside the function, only that it returns a string result.
How do I securely store email credentials for CrewAI tools?
Use environment variables loaded via python-dotenv or your platform's secrets manager. Never commit credentials to source control. For production, use a vault service or encrypted environment variables in your deployment platform.
What is the difference between using Composio and writing a native CrewAI email tool?
Composio provides pre-built connectors with OAuth flows managed for you, reducing setup time. A native custom tool gives you full control over the transport, error handling, and provider choice. Composio adds a dependency and abstraction layer; custom tools add maintenance responsibility.
Does CrewAI support inbound email parsing, or only outbound sending?
CrewAI has no native inbound email primitive. You need to build a custom tool that polls an inbox, or set up a webhook that triggers crew execution when new mail arrives. MCP servers with email capabilities can also provide receive functionality.
How do I avoid Gmail rate limits when running high-volume CrewAI email campaigns?
Gmail caps consumer accounts at roughly 500 sends/day and Workspace at 2,000. For higher volumes, switch to a dedicated email API with explicit rate limits. Implement backoff logic in your tool and spread sends across time windows rather than batching.
Can CrewAI agents handle email replies and thread management automatically?
Not out of the box. You need a custom tool that fetches replies (matched by In-Reply-To or References headers), parses them, and feeds the content back into a task. Threading requires tracking message IDs between crew runs.
What MCP server options exist for email integration in CrewAI beyond Gmail?
LobsterMail provides an MCP server purpose-built for agents, with send, receive, and inbox provisioning tools. You can also build your own MCP server wrapping any email API. The advantage is zero Python tool code in your project.
How do I ensure CAN-SPAM and GDPR compliance in a CrewAI-powered email workflow?
Include a physical address and unsubscribe mechanism in every commercial email. Track consent per recipient. Build an unsubscribe-handling tool that removes contacts from your agent's send list. Log all sends for audit purposes.
How do I test a custom email tool in CrewAI without sending real emails?
Run a local SMTP capture server like MailHog or Mailpit on localhost:1025. Point your tool's SMTP credentials there. All emails are captured in a web UI without actual delivery. Alternatively, add a dry_run parameter that logs instead of sending.
What happens if a CrewAI email tool fails mid-task?
By default, CrewAI reports the tool's error output back to the agent, which may retry or move on depending on your task configuration. For reliable sending, implement try/except blocks in your tool that return descriptive error strings so the agent can make informed decisions about retries.
How does deliverability differ between Gmail API, SMTP, and dedicated email APIs inside CrewAI agents?
Gmail inherits your personal sending reputation but has strict rate limits. Raw SMTP to your own server requires you to manage SPF, DKIM, and IP reputation yourself. Dedicated email APIs (SendGrid, Postmark, LobsterMail) handle authentication records and reputation monitoring, giving better deliverability at scale with less manual configuration.
Is it better to use a Zapier integration or a custom tool for email in CrewAI?
Zapier adds latency (webhook round-trips) and a per-execution cost that scales poorly. For prototyping or low-volume use, it's fast to set up. For anything your agent does repeatedly or at volume, a custom tool with a direct API call is faster, cheaper, and more reliable.
How do I pass the output of one CrewAI agent as the email body to another agent?
Use the context parameter on your sending task, pointing it to the drafting task. CrewAI automatically passes the drafting agent's output as context to the sending agent. No manual string passing required.


