
how to build an email tool for llamaindex's functioncallingagent
Give your LlamaIndex FunctionAgent real email capabilities. Step-by-step guide to wrapping LobsterMail as a FunctionTool for sending, receiving, and managing inboxes.
Your LlamaIndex agent can call functions, query databases, and search the web. But ask it to check an inbox or send a message, and you're stuck writing SMTP boilerplate that has nothing to do with your actual agent logic.
The FunctionCallingAgent pattern (now called FunctionAgent in current LlamaIndex) makes adding new capabilities simple: define a Python function, wrap it as a FunctionTool, hand it to the agent. Email has never been hard to wrap. The hard part is everything underneath. Provisioning addresses, authenticating with mail servers, managing DNS records, keeping messages out of spam folders.
LobsterMail removes that entire layer. Your agent provisions its own inbox through a single API call, and you wrap the send/receive operations as LlamaIndex tools. Here's how to wire it up from scratch. If you'd rather skip the infrastructure setup, and jump straight to the LlamaIndex integration below.
What FunctionAgent expects from a tool#
LlamaIndex's FunctionAgent delegates work to tools through the LLM's native function-calling interface. When you register a tool, the LLM sees three things: the function name, its docstring, and its parameter types. Based on the user's message, the LLM decides which tool to call and with what arguments.
A FunctionTool wraps any Python callable:
from llama_index.core.tools import FunctionTool
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
tool = FunctionTool.from_defaults(fn=add)
For email, your agent needs at minimum three tools: one to create an inbox, one to check for incoming mail, and one to send a message. Each function needs clear type hints and a descriptive docstring. The LLM uses the docstring to decide when to invoke the tool, so precision here matters more than you'd expect.
Setting up LobsterMail#
Install your dependencies:
pip install llama-index-core llama-index-llms-openai requests
LobsterMail's REST API handles inbox provisioning, sending, and receiving. No separate Python email library, no SMTP config. The agent authenticates with a single API token.
Store your token as an environment variable:
export LOBSTERMAIL_API_KEY="lm_sk_live_your_token_here"
If you don't have a token yet, the LobsterMail SDK can auto-provision one on first use. For this tutorial, we'll work with the REST API directly since LlamaIndex is Python-first and the REST endpoints map cleanly onto tool functions.
Building the email tools#
Here's where LlamaIndex and LobsterMail connect. You'll write three Python functions and wrap each as a FunctionTool.
import os
import requests
from llama_index.core.tools import FunctionTool
API_BASE = "https://api.lobstermail.ai/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['LOBSTERMAIL_API_KEY']}",
"Content-Type": "application/json",
}
def create_inbox(name: str) -> dict:
"""Create a new email inbox for the agent.
Args:
name: A human-readable name like 'research-bot'.
LobsterMail generates a clean address from this name,
handling collisions automatically.
Returns:
A dict with 'address' (the new email) and 'inbox_id'.
"""
resp = requests.post(
f"{API_BASE}/inboxes",
headers=HEADERS,
json={"name": name},
)
resp.raise_for_status()
data = resp.json()
return {"address": data["address"], "inbox_id": data["id"]}
def check_inbox(inbox_id: str) -> list[dict]:
"""Check for new emails in a specific inbox.
Args:
inbox_id: The inbox ID returned when the inbox was created.
Returns:
A list of email summaries with 'id', 'from', 'subject', and 'preview'.
"""
resp = requests.get(
f"{API_BASE}/inboxes/{inbox_id}/emails",
headers=HEADERS,
)
resp.raise_for_status()
emails = resp.json().get("emails", [])
return [
{
"id": e["id"],
"from": e["from"],
"subject": e["subject"],
"preview": e.get("preview", ""),
}
for e in emails
]
def send_email(inbox_id: str, to: str, subject: str, body: str) -> dict:
"""Send an email from an agent's inbox.
Args:
inbox_id: The inbox to send from.
to: Recipient email address.
subject: Email subject line.
body: Plain text email body.
Returns:
A dict with 'message_id' confirming delivery.
"""
resp = requests.post(
f"{API_BASE}/inboxes/{inbox_id}/send",
headers=HEADERS,
json={"to": to, "subject": subject, "body": body},
)
resp.raise_for_status()
return {"message_id": resp.json()["id"]}
A few things to notice. The docstrings are detailed on purpose. FunctionAgent passes them directly to the LLM, and vague descriptions lead to wrong tool choices at runtime. The create_inbox docstring explains what the name parameter does and that collisions are handled automatically. The check_inbox docstring specifies the return shape so the LLM knows what fields to reference in its response. These details help the model construct correct calls on the first try instead of hallucinating parameter names.
Now wrap them:
inbox_tool = FunctionTool.from_defaults(fn=create_inbox)
check_tool = FunctionTool.from_defaults(fn=check_inbox)
send_tool = FunctionTool.from_defaults(fn=send_email)
Wiring up the FunctionAgent#
With the tools ready, creating the agent takes a few lines:
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.openai import OpenAI
llm = OpenAI(model="gpt-4o")
agent = FunctionAgent(
tools=[inbox_tool, check_tool, send_tool],
llm=llm,
system_prompt=(
"You are a helpful assistant with email capabilities. "
"You can create inboxes, check for new messages, and send emails. "
"Always create an inbox before trying to send or receive."
),
)
Run it:
response = await agent.run(
"Create an inbox called 'research-bot', "
"then check if there are any emails waiting."
)
print(response)
The agent calls create_inbox first, captures the returned inbox_id, then passes it to check_inbox. If there's mail, it summarizes what it found. If not, it tells you the inbox is empty. No SMTP config, no DNS records, no OAuth flow.
You can swap in Anthropic's Claude or any other LLM that supports tool calling. Just change the llm parameter:
from llama_index.llms.anthropic import Anthropic
llm = Anthropic(model="claude-sonnet-4-20250514")
The tools stay identical. FunctionAgent handles the translation between whatever function-calling format the LLM expects and your Python functions.
What your agent can do with email#
Once the wiring is done, the interesting part starts. Here are patterns that work well in production.
The most common is service signup and verification. Your agent creates an inbox, uses the address to register for an API or service, polls for the verification email, extracts the confirmation code, and completes registration. The agent handles the entire flow without anyone clicking a password reset link. This single pattern covers a huge range of agent workflows, from signing up for SaaS tools to activating accounts on platforms that require email confirmation.
Research agents work well as automated reporters. They gather data, format it into a summary, and email the report to a distribution list on a schedule. The inbox doubles as a reply channel: recipients respond with follow-up questions, and the agent picks them up on the next polling cycle. You can build surprisingly useful internal tools this way with very little code.
Multi-agent coordination is another strong use case. Two agents with separate LobsterMail inboxes email each other. One gathers leads and sends them to a second agent that handles outreach. Each agent owns its address and manages its own inbox state independently. Email becomes a natural asynchronous communication layer between agents that don't share memory or runtime.
On the inbound side, an agent can monitor an inbox for support requests, classify them by urgency, draft responses for straightforward questions, and flag complex ones for a human. LobsterMail's built-in injection risk scoring helps here: emails with suspicious content get flagged before the agent processes the message body. That matters when you're feeding raw email text into an LLM. See the security docs for how the scoring works.
Handling two common gotchas#
Two things will trip you up if you don't plan for them.
First, inbox state between sessions. FunctionAgent doesn't persist state between runs by default. If your agent creates an inbox in one session, it won't remember the inbox_id in the next. Store it somewhere persistent (a JSON file, a database, an environment variable) or add a list_inboxes tool that the agent calls at the start of each session to discover existing inboxes.
Second, polling frequency and limits. The free LobsterMail tier gives you 1,000 emails per month. If your agent polls in a tight loop, those requests add up fast. Use reasonable intervals (30-60 seconds between checks) and add basic retry logic for rate limits:
import time
def check_inbox_with_backoff(inbox_id: str, max_retries: int = 3) -> list[dict]:
"""Check inbox with retry logic for rate limits."""
for attempt in range(max_retries):
resp = requests.get(
f"{API_BASE}/inboxes/{inbox_id}/emails",
headers=HEADERS,
)
if resp.status_code == 429:
time.sleep(2 ** attempt)
continue
resp.raise_for_status()
return [
{"id": e["id"], "from": e["from"], "subject": e["subject"]}
for e in resp.json().get("emails", [])
]
return []
For higher volumes, the Builder plan ($9/mo) gives you 5,000 emails per month and up to 10 inboxes. You can also switch from polling to webhooks for real-time delivery notifications, which eliminates the polling overhead entirely.
Where to go from here#
The FunctionAgent + LobsterMail combination gives your agent a real email address it controls. Not a mock or a forwarding hack. The agent provisions the address, reads from it, sends from it.
If you need more than the three basic tools, the LobsterMail API also supports webhooks for push-based delivery, custom domains so your agent sends from bot@yourdomain.com instead of @lobstermail.ai, and attachment handling. Each one maps cleanly to another FunctionTool with the same wrapping pattern shown above.
For the full API reference, check the getting started guide. If you're using Claude Code or Cursor instead of LlamaIndex, the MCP server guide covers zero-code setups where the agent gets email tools without writing any wrapper functions at all.
Frequently asked questions
Does LobsterMail have a Python SDK?
LobsterMail's primary SDK is for Node.js/TypeScript (@lobsterkit/lobstermail). For Python projects like LlamaIndex agents, use the REST API directly with requests or httpx. The API endpoints are the same.
Can I use FunctionAgent with Claude instead of GPT-4?
Yes. Swap OpenAI for Anthropic in the LLM parameter. FunctionAgent works with any LLM that supports tool calling, including Claude, Gemini, and Mistral. The email tools stay identical.
What is the difference between FunctionCallingAgent and FunctionAgent in LlamaIndex?
FunctionCallingAgent was the older name. The current API uses FunctionAgent imported from llama_index.core.agent.workflow. They work the same way, but FunctionAgent is the maintained version going forward.
How many inboxes can I create on the free LobsterMail plan?
The free tier includes one inbox with 1,000 emails per month. The Builder plan ($9/mo) supports up to 10 inboxes and 5,000 emails per month.
Does the agent need to create a new inbox every time it runs?
No. Create the inbox once and store the inbox_id. On later runs, your agent can reuse the same inbox. Add a list_inboxes tool so the agent can discover its existing inboxes at startup.
Can two LlamaIndex agents share the same LobsterMail inbox?
Technically yes, but it's better to give each agent its own inbox. Shared inboxes create race conditions where both agents try to process the same email. Separate inboxes keep things clean.
How does LobsterMail protect agents from prompt injection in emails?
Every incoming email gets an injection risk score. Your agent can check this score before processing the email content, filtering out messages with suspicious patterns. See the security docs for details.
Can my FunctionAgent send emails with file attachments?
Yes. The LobsterMail API supports attachments on send requests. Add a file parameter to your send_email tool function and include it in the API request body.
Is there a way to get real-time email delivery instead of polling?
Yes. LobsterMail supports webhooks that push a notification to your application when new email arrives. This eliminates polling entirely and reduces your API call volume.
Can I use a custom domain instead of @lobstermail.ai?
Yes, on the Builder plan and above. See the custom domains guide for DNS setup instructions. Your agent sends from addresses like bot@yourdomain.com.
What happens if my agent hits the LobsterMail rate limit?
The API returns a 429 status code. Implement exponential backoff in your tool functions, as shown in the check_inbox_with_backoff example above, and your agent will retry automatically.
Do I need to configure DNS records to use LobsterMail?
Not on the default @lobstermail.ai domain. SPF, DKIM, and DMARC are all preconfigured. You only touch DNS if you add a custom domain.


