All posts
Guide·Jul 7, 2026·8 min read

How to give your LangChain agent its own email inbox

LangChain email guide: give a LangChain agent a real inbox with the AgenticEmail SDK, wrap send and read as tools, and handle inbound mail over webhooks.

Dvir Atias

Dvir Atias

Founder

To give a LangChain agent its own email inbox, create a real inbox with the AgenticEmail SDK, then wrap the send and read calls as LangChain tools with the @tool decorator or StructuredTool in Python (or tool() in JavaScript) and bind them to your agent. There is no LangChain-specific package to install and none to maintain: LangChain tools are ordinary typed functions, AgenticEmail is an ordinary SDK, and the two connect in about thirty lines. The result is an agent with a live address it can send from, receive at, and reply to over REST, with inbound mail pushed to you as a webhook.

Why a LangChain agent needs its own inbox, not just a send API

Most "email for agents" tutorials stop at sending. A LangChain agent that can only fire off outbound mail is a notification script with extra steps. The useful pattern is a two-way loop: the agent receives a message, reasons over it inside its normal tool-calling cycle, and replies in the same thread. Support triage, outbound follow-ups that expect an answer, vendor back-and-forth, and agent-to-agent handoffs are all email-shaped, and email is still the one channel every person and system already has. This is the same reason AI agents need email in general.

The usual workaround is to point the agent at a shared Gmail account through OAuth and IMAP. That falls over in production: tokens expire, one mailbox becomes a bottleneck, and you cannot cleanly give each agent, or each of your customers' agents, a separate address. An AI email agent wants an inbox that is a resource, created and destroyed by API, not a human mailbox it borrows.

Step 1: Create an inbox with the AgenticEmail SDK

Install the SDK and create an inbox. This is one call and returns a live address immediately.

import os
from agenticemail import AgenticEmail

client = AgenticEmail(api_key=os.environ["AGENTICEMAIL_API_KEY"])

inbox = client.inboxes.create(username="assistant")
print(inbox["id"], inbox["address"])
# ibx_... assistant@inbox.agenticemail.dev

On paid plans you attach your own domain and the address becomes assistant@yourdomain.com. Keep the inbox["id"] around; every send and read call is scoped to it. Inbound mail arrives as parsed JSON, so you never touch a MIME tree: read it with client.messages.list(inbox_id) or fetch one with client.messages.get(inbox_id, message_id). That send-and-read surface is exactly what a LangChain tool needs to wrap. The JavaScript SDK mirrors this call for call: new AgenticEmail({ apiKey }) and client.inboxes.create({ username }).

Step 2: Expose send and read as LangChain tools

LangChain gives you two ways to define a tool in Python: decorate a plain function with @tool, or build a StructuredTool with an explicit Pydantic args schema. Use StructuredTool when you want a strict input contract the model must fill; use @tool for the quick cases where the docstring is a good enough description. Here is a send tool with an explicit schema plus read and reply as decorated functions:

from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool, tool

INBOX_ID = os.environ["AGENTICEMAIL_INBOX_ID"]

class SendEmailInput(BaseModel):
    to: str = Field(..., description="Recipient email address")
    subject: str = Field(..., description="Subject line")
    body: str = Field(..., description="Plain-text message body")

def _send_email(to: str, subject: str, body: str) -> str:
    result = client.messages.send(INBOX_ID, to=[to], subject=subject, text=body)
    return f"Sent message {result['id']} to {to}"

send_email = StructuredTool.from_function(
    func=_send_email,
    name="send_email",
    description="Send an email from the agent's inbox to one recipient.",
    args_schema=SendEmailInput,
)

@tool
def read_inbox() -> str:
    """List the most recent messages in the agent's inbox as text."""
    messages = client.messages.list(INBOX_ID).get("data", [])
    return "\n".join(
        f"[{m['id']}] from {m['from']}: {m['subject']}" for m in messages[:10]
    )

@tool
def reply_to_email(message_id: str, body: str) -> str:
    """Reply to a message by id, staying in the same thread."""
    result = client.messages.reply(INBOX_ID, message_id, text=body)
    return f"Replied in thread as {result['id']}"

Threading headers on a reply are handled for you when you pass the message id, so the recipient sees a real conversation rather than a fresh email. Now bind the three tools to an agent and let the model drive them:

from langchain.chat_models import init_chat_model
from langgraph.prebuilt import create_react_agent

llm = init_chat_model("anthropic:claude-sonnet-4-5")
agent = create_react_agent(llm, [send_email, read_inbox, reply_to_email])

agent.invoke({"messages": [
    ("user", "Check the inbox and reply to any unanswered questions."),
]})

If your stack is TypeScript, the shape is identical with LangChain's tool() helper and a Zod schema:

import { z } from "zod";
import { tool } from "@langchain/core/tools";
import { AgenticEmail } from "agenticemail";

const client = new AgenticEmail({ apiKey: process.env.AGENTICEMAIL_API_KEY });
const inboxId = process.env.AGENTICEMAIL_INBOX_ID;

const sendEmail = tool(
  async ({ to, subject, body }) => {
    const result = await client.messages.send(inboxId, { to: [to], subject, text: body });
    return `Sent message ${result.id} to ${to}`;
  },
  {
    name: "send_email",
    description: "Send an email from the agent's inbox to one recipient.",
    schema: z.object({
      to: z.string().describe("Recipient email address"),
      subject: z.string().describe("Subject line"),
      body: z.string().describe("Plain-text message body"),
    }),
  },
);

Scope each agent's API key to its own inbox so a compromised agent can at worst read and send from its own address, not touch your whole account.

Step 3: Feed inbound mail to the agent with a webhook

Polling with read_inbox is fine for a one-off run, but an agent that reacts to mail in real time should be driven by a webhook. Register one endpoint and every inbound message is delivered as signed JSON:

client.webhooks.create(
    url="https://your-app.com/hooks/email",
    event_types=["message.received"],
)

Then verify the signature and invoke the agent with the new message. verify_webhook raises if the signature does not match, so a forged request never reaches the model:

from fastapi import FastAPI, Request
from agenticemail import verify_webhook

app = FastAPI()
SECRET = os.environ["AGENTICEMAIL_WEBHOOK_SECRET"]

@app.post("/hooks/email")
async def inbound(request: Request):
    raw = await request.body()
    event = verify_webhook(raw.decode(), dict(request.headers), SECRET)
    message = event["data"]
    agent.invoke({"messages": [
        ("user", f"New email {message['id']} from {message['from']}. Read the thread and reply."),
    ]})
    return {"ok": True}

Now inbound mail triggers a run, the agent reads the thread with read_inbox, and it answers through reply_to_email, with no polling loop. If part of your stack speaks the Model Context Protocol instead, the same inbox is reachable through the hosted MCP email server, so a LangChain agent and an MCP client can share one address.

A note on end-to-end encryption for agent-to-agent mail

When two of your agents email each other, the body often carries exactly what you would not want sitting in plaintext on a server: customer records, scoped tokens, raw tool output. AgenticEmail offers opt-in, agent-to-agent encrypted email that is zero-knowledge by design. The private key is generated in your runtime and never reaches the server, so the platform only ever stores ciphertext. It is opt-in, not on by default, and applies only between two AgenticEmail inboxes that have each published a key; it does not cover mail arriving from the outside world.

Turn it on by generating an identity and passing it to the client. Your existing tools need no changes, since the SDK encrypts under the hood whenever the recipient has a key:

from agenticemail.e2e import generate_identity

identity = generate_identity("assistant@inbox.agenticemail.dev")

client = AgenticEmail(
    api_key=os.environ["AGENTICEMAIL_API_KEY"],
    e2e={
        "identity": identity,
        "auto_publish": True,
        "verify_sender": True,
        "allow_plaintext_fallback": False,
    },
)
client.publish_public_key(inbox["id"])

With allow_plaintext_fallback off, a send to a peer with no published key fails loudly instead of leaking a body, and verify_sender makes the receiving agent check the signature before trusting a byte.

Where to go next

That is a full LangChain agent email setup: a real inbox from the SDK, send and read wrapped as LangChain tools, a create_react_agent that owns those tools, webhooks for inbound, and optional encryption for agent-to-agent traffic. The same inbox is reachable over REST, the CLI, or the hosted MCP server if part of your stack is not Python or TypeScript. For the complete method reference see the docs, and if you are weighing this against stitching together Gmail or a transactional API, the alternatives page lays out the tradeoffs.

Talk to a real person