cd /news/large-language-models/give-a-langchain-agent-a-real-inbox · home topics large-language-models article
[ARTICLE · art-28534] src=dev.to ↗ pub= topic=large-language-models verified=true sentiment=↑ positive

Give a LangChain Agent a Real Inbox

An engineer built a LangChain agent that reads an inbox, summarizes threads, and drafts replies, but faced production issues with OAuth and privacy. The solution uses Nylas Agent Accounts to create a dedicated mailbox via API, avoiding human inboxes. The agent uses tools to list messages, read full messages, send emails, and access a calendar, all bound to a LangChain agent.

read5 min views5 publishedJun 15, 2026

An engineer on a small team wires up a LangChain agent over a weekend: it reads the inbox, summarizes threads, drafts replies. The demo kills. Then Monday's planning meeting asks the question that kills the demo back: whose inbox does this run on in production? Nobody wants the bot reading their mail, the shared support@

account is on someone's personal OAuth grant, and legal has opinions about both.

The clean answer is to give the agent its own mailbox. Nylas Agent Accounts — currently in beta — are hosted email-and-calendar identities you create with a single API call. No OAuth flow, no refresh token, no human's inbox anywhere in the loop. The quickstart gets you a live address in under 5 minutes:

import os, requests

BASE = "https://api.us.nylas.com"
HEADERS = {
    "Authorization": f"Bearer {os.environ['NYLAS_API_KEY']}",
    "Content-Type": "application/json",
}

resp = requests.post(
    f"{BASE}/v3/connect/custom",
    headers=HEADERS,
    json={"provider": "nylas",
          "settings": {"email": "assistant@your-application.nylas.email"}},
)
GRANT_ID = resp.json()["data"]["id"]

That GRANT_ID

is the agent's identity. Everything below builds on it.

The mailbox surface maps naturally onto tools. Three cover most agent behavior — read the inbox, read one message in full, send:

from langchain_core.tools import tool

@tool
def list_messages(limit: int = 10) -> str:
    """List the newest messages in the agent's own inbox. Returns JSON
    with subject, from, snippet, and message IDs."""
    r = requests.get(
        f"{BASE}/v3/grants/{GRANT_ID}/messages",
        headers=HEADERS, params={"limit": limit}, timeout=30,
    )
    return r.text if r.ok else f"Error: {r.text}"

@tool
def read_message(message_id: str) -> str:
    """Fetch a single message, including its full body."""
    r = requests.get(
        f"{BASE}/v3/grants/{GRANT_ID}/messages/{message_id}",
        headers=HEADERS, timeout=30,
    )
    return r.text if r.ok else f"Error: {r.text}"

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email from the agent's own address. Confirm recipient,
    subject, and body with the user before calling."""
    r = requests.post(
        f"{BASE}/v3/grants/{GRANT_ID}/messages/send",
        headers=HEADERS, timeout=30,
        json={"subject": subject, "body": body, "to": [{"email": to}]},
    )
    return "Email sent." if r.ok else f"Error: {r.text}"

Two design choices here come straight from the LLM-agent tooling recipe in the cookbook. First, the default limit

of 10: a hundred messages of JSON will flood your context window, so cap aggressively in the schema and let the agent ask for more. Second, error strings go back to the model rather than raising — LLMs are surprisingly good at deciding what to do with "Error: ..."

output if you let it through ("Looks like that message ID doesn't exist — let me list the inbox again").

The account has a calendar too — every Agent Account ships with a primary one — so a fourth tool makes the agent schedule-aware:

@tool
def list_events() -> str:
    """List events on the agent's own calendar. Returns JSON with
    titles, times, and participants."""
    r = requests.get(
        f"{BASE}/v3/grants/{GRANT_ID}/events",
        headers=HEADERS, params={"calendar_id": "primary"}, timeout=30,
    )
    return r.text if r.ok else f"Error: {r.text}"

The same pattern extends to POST /v3/grants/{grant_id}/events

for creating meetings and the send-rsvp

endpoint for answering invitations — all on the identity the agent already owns.

From here it's standard LangChain. Bind the tools to a model and let the loop run:

from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic

agent = create_react_agent(
    ChatAnthropic(model="claude-sonnet-4-5"),
    tools=[list_messages, read_message, send_email, list_events],
)

result = agent.invoke({
    "messages": [("user", "Did anyone reply about the contract? "
                          "If so, summarize and draft a thank-you.")]
})

The agent may chain several tool calls — list, then read the relevant message, then send — before producing its final answer. That's the same multi-step pattern as any tool loop; what's different is that send_email

fires from an address the agent owns, and replies come back to that same address, so the conversation accumulates in the agent's inbox rather than evaporating.

The version above still waits for a human prompt. The upgrade path is event-driven: register a message.created

webhook and invoke the agent whenever mail arrives. Registration is one call, straight from the quickstart:

requests.post(
    f"{BASE}/v3/webhooks",
    headers=HEADERS,
    json={
        "trigger_types": ["message.created"],
        "callback_url": "https://yourapp.example.com/webhooks/nylas",
    },
)

Then the webhook handler becomes the agent's trigger. The payload carries the message's grant_id

, subject

, sender, and snippet under data.object

, which is exactly enough to compose a prompt:

from flask import Flask, request

app = Flask(__name__)

@app.post("/webhooks/nylas")
def on_mail():
    payload = request.get_json()
    if payload.get("type") == "message.created":
        msg = payload["data"]["object"]
        agent.invoke({"messages": [(
            "user",
            f"New email arrived (id: {msg['id']}) from "
            f"{msg['from'][0]['email']}: {msg['subject']}. "
            "Read it and respond appropriately.",
        )]})
    return "", 200

Inbound message in, tool calls in the middle, reasoned reply out — a genuinely autonomous correspondent. And because the reply goes out from the agent's own address, the human's answer comes back to the same mailbox and fires the same webhook: the loop sustains itself.

The cookbook recipe takes an even lazier route worth knowing about: shell out to the Nylas CLI instead of calling REST. Subprocess wrappers around nylas email list --json

and nylas email send --yes

give you 16 tools across six providers in under 50 lines of Python — versus roughly 300 lines of boilerplate for a hand-rolled Gmail OAuth integration alone. Two flags carry the load: --yes

, because without it send commands wait on a confirmation prompt an agent loop will never answer, and --json

, because structured output is what the model can actually parse. One caveat for multi-tenant setups: the CLI operates on whichever grant is active in nylas auth list

, so run a per-tenant CLI process or pass --api-key

explicitly. Same tool-calling pattern, different transport.

A tool description saying "confirm with the user before sending" is a suggestion, not a control. The platform-level controls live in policies and rules: daily send quotas, outbound rules that block sends to wrong domains before they ever reach a provider, inbound rules that reject junk at SMTP so prompt-injection-bearing spam never enters the model's context. Belt, meet suspenders.

Start small: provision a trial-domain account, paste the three tools above into your existing LangChain project, and email your agent something. What's the first message you'd want software answering on its own?

── more in #large-language-models 4 stories · sorted by recency
── more on @langchain 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/give-a-langchain-age…] indexed:0 read:5min 2026-06-15 ·