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.
Mailbox operations as LangChain tools
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.
Wiring them into an agent
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.
From chat loop to autonomous correspondent
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 CLI shortcut, if you'd rather not write tools
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.
Guardrails before you let it loose
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?
Top comments (0)