
how to add email to your llamaindex functionagent workflow
Build a LlamaIndex FunctionAgent workflow with real email send and receive. Full Python code for multi-agent orchestration with LobsterMail.
LlamaIndex's FunctionAgent gives you a clean way to build tool-using agents, and AgentWorkflow lets you chain several of them together for multi-step tasks. But most LlamaIndex tutorials stop at web search and document retrieval. The moment your agent needs to send a follow-up email, confirm a signup, or monitor an inbox for replies, you're left stitching together SMTP libraries and mail server configs on your own.
Email is one of those capabilities that sounds simple until you try to wire it into an agent system. Your agent needs a real address. It needs to receive messages and parse their contents. It needs to send replies that land in the recipient's primary inbox, not their spam folder. Setting up SMTP credentials, DNS records, and domain warming is a solid weekend of work, and that's before you even consider prompt injection risks hiding in incoming mail content.
If you'd rather skip the infrastructure, and your agent can send and receive email in a couple of minutes. Here's how to connect it to a LlamaIndex workflow.
What FunctionAgent and AgentWorkflow do#
If you haven't used these yet, here's the short version. FunctionAgent is a LlamaIndex agent class that calls Python functions through an LLM's native function-calling API. You define functions, wrap them as FunctionTool objects, and pass them to the agent. The LLM decides when and how to invoke each tool based on the user's request and the conversation context.
AgentWorkflow sits one level above. It orchestrates multiple FunctionAgent instances, routing tasks between specialized agents using a handoff mechanism. One agent researches a topic, another writes a summary, a third sends the email. Each agent has its own tool set and system prompt, and the workflow manages state transitions between them.
The pattern works like this: your Researcher agent finishes gathering information and tells the workflow "hand off to Writer." AgentWorkflow routes the conversation context to the Writer agent. The Writer composes the email body and hands off to Mailer. Email becomes just another stage in the pipeline.
from llama_index.core.agent.workflow import FunctionAgent, AgentWorkflow
research_agent = FunctionAgent(
name="ResearchAgent",
description="Finds information on a topic",
system_prompt="You are a research assistant...",
tools=[search_tool],
can_handoff_to=["EmailAgent"]
)
email_agent = FunctionAgent(
name="EmailAgent",
description="Sends email with research results",
system_prompt="You compose and send emails...",
tools=[send_email_tool, check_inbox_tool],
can_handoff_to=[]
)
workflow = AgentWorkflow(agents=[research_agent, email_agent])
The question is: what goes inside send_email_tool and check_inbox_tool? You can use a single FunctionAgent if email is the only task, but the multi-agent pattern pays off when you need to separate concerns. One agent focuses on research, another on writing, another on delivery. Each agent can be tested independently and swapped out without touching the others. That modularity is worth the small overhead of defining separate agents.
Building email tools for LlamaIndex#
Since FunctionAgent works with plain Python functions, adding email means writing functions that call an email API and wrapping them with FunctionTool. LobsterMail exposes a REST API that any language can call, so httpx or requests work fine from Python.
Start by setting up your credentials:
import httpx
import os
LOBSTERMAIL_TOKEN = os.environ["LOBSTERMAIL_TOKEN"]
BASE_URL = "https://api.lobstermail.ai/v1"
headers = {"Authorization": f"Bearer {LOBSTERMAIL_TOKEN}"}
Then define tools for the three core operations.
Creating an inbox:
from llama_index.core.tools import FunctionTool
def create_inbox(name: str) -> dict:
"""Create a new email inbox with a human-readable address."""
resp = httpx.post(
f"{BASE_URL}/inboxes",
headers=headers,
json={"name": name}
)
return resp.json()
create_inbox_tool = FunctionTool.from_defaults(fn=create_inbox)
Checking for new messages:
def check_inbox(inbox_id: str) -> list:
"""Check for new emails in the specified inbox."""
resp = httpx.get(
f"{BASE_URL}/inboxes/{inbox_id}/emails",
headers=headers
)
return resp.json().get("emails", [])
check_inbox_tool = FunctionTool.from_defaults(fn=check_inbox)
Sending an email:
def send_email(inbox_id: str, to: str, subject: str, body: str) -> dict:
"""Send an email from the agent's inbox to the given recipient."""
resp = httpx.post(
f"{BASE_URL}/inboxes/{inbox_id}/emails",
headers=headers,
json={"to": to, "subject": subject, "body": body}
)
return resp.json()
send_email_tool = FunctionTool.from_defaults(fn=send_email)
Each function has a docstring. This matters because FunctionAgent passes those docstrings to the LLM so it understands what each tool does and when to call it. A vague docstring leads to unreliable tool selection, so write them as clear, one-line descriptions of the action.
Tip
If you're using LlamaIndex.TS (TypeScript), you can install the @lobsterkit/lobstermail SDK directly instead of making REST calls. The workflow patterns are the same.
Full example: research-and-report workflow#
Here's a complete multi-agent workflow. Three agents collaborate: one researches a topic using Brave Search, one writes a summary, one sends it by email.
from llama_index.core.agent.workflow import FunctionAgent, AgentWorkflow
from llama_index.core.tools import FunctionTool
from llama_index.tools.brave_search import BraveSearchToolSpec
import httpx
import os
LOBSTERMAIL_TOKEN = os.environ["LOBSTERMAIL_TOKEN"]
BASE_URL = "https://api.lobstermail.ai/v1"
headers = {"Authorization": f"Bearer {LOBSTERMAIL_TOKEN}"}
INBOX_ID = os.environ["AGENT_INBOX_ID"]
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email with the given subject and body."""
resp = httpx.post(
f"{BASE_URL}/inboxes/{INBOX_ID}/emails",
headers=headers,
json={"to": to, "subject": subject, "body": body}
)
return "Sent" if resp.status_code == 200 else f"Error: {resp.text}"
def check_inbox() -> list:
"""Check for new emails in the agent inbox."""
resp = httpx.get(
f"{BASE_URL}/inboxes/{INBOX_ID}/emails",
headers=headers
)
return resp.json().get("emails", [])
brave_tools = BraveSearchToolSpec(
api_key=os.environ["BRAVE_API_KEY"]
).to_tool_list()
researcher = FunctionAgent(
name="Researcher",
description="Searches the web for current information",
system_prompt="Search for information, then hand off to Writer.",
tools=brave_tools,
can_handoff_to=["Writer"]
)
writer = FunctionAgent(
name="Writer",
description="Writes clear email summaries from research",
system_prompt="Write a concise summary, then hand off to Mailer.",
tools=[],
can_handoff_to=["Mailer"]
)
mailer = FunctionAgent(
name="Mailer",
description="Sends emails and checks for replies",
system_prompt="Handle all email operations.",
tools=[
FunctionTool.from_defaults(fn=send_email),
FunctionTool.from_defaults(fn=check_inbox),
],
can_handoff_to=["Researcher"]
)
workflow = AgentWorkflow(agents=[researcher, writer, mailer])
response = await workflow.run(
user_msg="Research browser-use agents and email a summary to frank@example.com"
)
The Researcher gathers information, hands off to the Writer who composes the email body, who hands off to the Mailer who sends it. If a reply arrives that needs follow-up research, the Mailer can hand back to the Researcher, creating a loop that runs until the task is done.
This pattern scales by adding agents. You could insert a FactChecker between Researcher and Writer, or a Scheduler that queues emails for later delivery. AgentWorkflow handles all the routing without you managing state manually.
Why this beats raw SMTP#
You could skip LobsterMail and use Python's smtplib directly. It's in the standard library, it works, and there are a hundred tutorials for it. But the approach has specific problems when agents are running autonomously.
Your agent can't provision its own mail server. SMTP requires credentials that a human creates and pastes into a config file. Gmail needs OAuth and a human to click "Allow." Self-hosted mail means running Postfix, managing TLS certificates, and building IP reputation over weeks of gradual volume increases. With LobsterMail, the agent creates its own inbox through the API, no human intervention required.
Incoming email is a prompt injection vector. When your agent reads messages from strangers, those messages can contain instructions disguised as ordinary text, telling the LLM to ignore its system prompt or perform unauthorized actions. LobsterMail scores every incoming email for injection risk, so your agent can filter or flag suspicious content before processing it. Raw SMTP hands you a bytestring and wishes you luck. See the security docs for details on how the scoring system works.
Deliverability also isn't free. Sending from a misconfigured domain or a fresh IP with no sending history means your agent's messages go straight to spam. We covered the most common failures in 5 agent email setup mistakes that tank your deliverability. LobsterMail handles SPF, DKIM, and domain reputation from the start, so messages actually reach the recipient's inbox.
Handling incoming email in a workflow#
Sending is half the picture. Real agent workflows need to receive email too. Consider an agent that signs up for a service, waits for a verification email, extracts the confirmation code, and completes registration. Or a support agent that monitors a shared inbox and triages customer messages throughout the day.
Here's a polling tool you can add to your Mailer agent:
import time
def wait_for_email(timeout: int = 60) -> dict:
"""Poll the inbox until a new email arrives or timeout is reached."""
start = time.time()
while time.time() - start < timeout:
resp = httpx.get(
f"{BASE_URL}/inboxes/{INBOX_ID}/emails",
headers=headers,
params={"unread": True}
)
emails = resp.json().get("emails", [])
if emails:
return emails[0]
time.sleep(5)
return {"error": "Timed out waiting for email"}
wait_tool = FunctionTool.from_defaults(fn=wait_for_email)
Add wait_tool to your Mailer agent's tool list, and it can now pause mid-workflow to wait for incoming messages. The AgentWorkflow orchestration keeps everything in sequence: the Researcher triggers a signup on some website, the Mailer waits for the confirmation email, extracts the verification code, and hands it back so the next agent can complete registration.
For higher-volume use cases where polling every five seconds isn't practical, LobsterMail supports webhooks that push to your server the instant an email arrives. That's a better fit for production systems handling dozens of inboxes.
Next steps#
The free tier gives you 1,000 emails per month with no credit card required, which is plenty for building and testing a LlamaIndex workflow. If your production agent needs multiple inboxes or higher send volume, the Builder plan at $9/month covers up to 10 inboxes and 5,000 emails per month.
, drop the API token into your environment variables, and start building tools. The full multi-agent workflow above runs in under 60 lines of actual logic.
For a hands-on walkthrough of what your agent can do once it has email (signup verification, outbound notifications, inbox monitoring), check out the agent quickstart guide.
Frequently asked questions
Does LlamaIndex have built-in email support?
No. LlamaIndex provides the agent framework (FunctionAgent, AgentWorkflow, tool wrappers) but no email infrastructure. You need a service like LobsterMail to handle inbox creation, sending, and receiving.
Can I use LobsterMail with LlamaIndex.TS instead of Python?
Yes. LlamaIndex.TS users can install the @lobsterkit/lobstermail npm package and use the SDK directly instead of making REST calls. The multi-agent workflow patterns are the same.
How does FunctionAgent decide when to call a tool?
It uses the LLM's native function-calling API. The agent passes tool names and docstrings to the model, which selects the right tool based on the user's request. Clear, specific docstrings improve selection accuracy.
Is the LobsterMail API free?
The free tier includes 1,000 emails per month with no credit card required. The Builder tier at $9/month adds up to 10 inboxes and 5,000 emails per month.
How do I handle email attachments in a LlamaIndex workflow?
The LobsterMail API supports attachments on both send and receive. Include them as base64-encoded data in your send function, and parse the attachments array from incoming messages.
Can my agent use a custom domain instead of @lobstermail.ai?
Yes. LobsterMail supports custom domains on paid plans. Your agent's emails would come from something like agent@yourdomain.com.
What happens if two agents read the same inbox concurrently?
Both agents receive the same emails. LobsterMail doesn't lock messages to a single reader. If you need exclusive processing, track which messages each agent has handled in your application state or use separate inboxes.
How does LobsterMail protect against prompt injection in emails?
Every incoming email is scored for injection risk. The score is returned alongside the message content, so your agent can skip or flag high-risk messages before the LLM processes them. Details are in the security docs.
Can I use AgentWorkflow with a different email provider?
Yes. AgentWorkflow is provider-agnostic since it works with any Python function wrapped as a FunctionTool. You could use SendGrid, Mailgun, or raw SMTP. LobsterMail's advantage is that the agent can self-provision inboxes without human setup.
What's the difference between FunctionAgent and ReActAgent for email tasks?
FunctionAgent uses the LLM's native function-calling API, which tends to be more reliable for structured tool use. ReActAgent uses a text-based reasoning-action loop. For email workflows where you need predictable, well-formatted tool calls, FunctionAgent is the better fit.
How many inboxes can my agent create on the free plan?
The free plan includes one inbox. If your workflow needs multiple inboxes (one per agent, or per task type), the Builder plan supports up to 10.
Can my LlamaIndex agent send HTML-formatted emails?
Yes. Pass HTML content in the body field with a content_type of text/html. The API handles MIME formatting so your agent doesn't need to construct multipart messages manually.


