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. 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 https://developer.nylas.com/docs/v3/getting-started/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: python 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: python 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 https://developer.nylas.com/docs/cookbook/cli/llm-agent-with-tools/ 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: php @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: python 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: python 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 https://developer.nylas.com/docs/v3/agent-accounts/policies-rules-lists/ : 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?