{"slug": "give-a-langchain-agent-a-real-inbox", "title": "Give a LangChain Agent a Real Inbox", "summary": "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.", "body_md": "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@`\n\naccount is on someone's personal OAuth grant, and legal has opinions about both.\n\nThe 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:\n\n``` python\nimport os, requests\n\nBASE = \"https://api.us.nylas.com\"\nHEADERS = {\n    \"Authorization\": f\"Bearer {os.environ['NYLAS_API_KEY']}\",\n    \"Content-Type\": \"application/json\",\n}\n\nresp = requests.post(\n    f\"{BASE}/v3/connect/custom\",\n    headers=HEADERS,\n    json={\"provider\": \"nylas\",\n          \"settings\": {\"email\": \"assistant@your-application.nylas.email\"}},\n)\nGRANT_ID = resp.json()[\"data\"][\"id\"]\n```\n\nThat `GRANT_ID`\n\nis the agent's identity. Everything below builds on it.\n\nThe mailbox surface maps naturally onto tools. Three cover most agent behavior — read the inbox, read one message in full, send:\n\n``` python\nfrom langchain_core.tools import tool\n\n@tool\ndef list_messages(limit: int = 10) -> str:\n    \"\"\"List the newest messages in the agent's own inbox. Returns JSON\n    with subject, from, snippet, and message IDs.\"\"\"\n    r = requests.get(\n        f\"{BASE}/v3/grants/{GRANT_ID}/messages\",\n        headers=HEADERS, params={\"limit\": limit}, timeout=30,\n    )\n    return r.text if r.ok else f\"Error: {r.text}\"\n\n@tool\ndef read_message(message_id: str) -> str:\n    \"\"\"Fetch a single message, including its full body.\"\"\"\n    r = requests.get(\n        f\"{BASE}/v3/grants/{GRANT_ID}/messages/{message_id}\",\n        headers=HEADERS, timeout=30,\n    )\n    return r.text if r.ok else f\"Error: {r.text}\"\n\n@tool\ndef send_email(to: str, subject: str, body: str) -> str:\n    \"\"\"Send an email from the agent's own address. Confirm recipient,\n    subject, and body with the user before calling.\"\"\"\n    r = requests.post(\n        f\"{BASE}/v3/grants/{GRANT_ID}/messages/send\",\n        headers=HEADERS, timeout=30,\n        json={\"subject\": subject, \"body\": body, \"to\": [{\"email\": to}]},\n    )\n    return \"Email sent.\" if r.ok else f\"Error: {r.text}\"\n```\n\nTwo 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`\n\nof 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: ...\"`\n\noutput if you let it through (\"Looks like that message ID doesn't exist — let me list the inbox again\").\n\nThe account has a calendar too — every Agent Account ships with a primary one — so a fourth tool makes the agent schedule-aware:\n\n``` php\n@tool\ndef list_events() -> str:\n    \"\"\"List events on the agent's own calendar. Returns JSON with\n    titles, times, and participants.\"\"\"\n    r = requests.get(\n        f\"{BASE}/v3/grants/{GRANT_ID}/events\",\n        headers=HEADERS, params={\"calendar_id\": \"primary\"}, timeout=30,\n    )\n    return r.text if r.ok else f\"Error: {r.text}\"\n```\n\nThe same pattern extends to `POST /v3/grants/{grant_id}/events`\n\nfor creating meetings and the `send-rsvp`\n\nendpoint for answering invitations — all on the identity the agent already owns.\n\nFrom here it's standard LangChain. Bind the tools to a model and let the loop run:\n\n``` python\nfrom langgraph.prebuilt import create_react_agent\nfrom langchain_anthropic import ChatAnthropic\n\nagent = create_react_agent(\n    ChatAnthropic(model=\"claude-sonnet-4-5\"),\n    tools=[list_messages, read_message, send_email, list_events],\n)\n\nresult = agent.invoke({\n    \"messages\": [(\"user\", \"Did anyone reply about the contract? \"\n                          \"If so, summarize and draft a thank-you.\")]\n})\n```\n\nThe 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`\n\nfires 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.\n\nThe version above still waits for a human prompt. The upgrade path is event-driven: register a `message.created`\n\nwebhook and invoke the agent whenever mail arrives. Registration is one call, straight from the quickstart:\n\n```\nrequests.post(\n    f\"{BASE}/v3/webhooks\",\n    headers=HEADERS,\n    json={\n        \"trigger_types\": [\"message.created\"],\n        \"callback_url\": \"https://yourapp.example.com/webhooks/nylas\",\n    },\n)\n```\n\nThen the webhook handler becomes the agent's trigger. The payload carries the message's `grant_id`\n\n, `subject`\n\n, sender, and snippet under `data.object`\n\n, which is exactly enough to compose a prompt:\n\n``` python\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n@app.post(\"/webhooks/nylas\")\ndef on_mail():\n    payload = request.get_json()\n    if payload.get(\"type\") == \"message.created\":\n        msg = payload[\"data\"][\"object\"]\n        agent.invoke({\"messages\": [(\n            \"user\",\n            f\"New email arrived (id: {msg['id']}) from \"\n            f\"{msg['from'][0]['email']}: {msg['subject']}. \"\n            \"Read it and respond appropriately.\",\n        )]})\n    return \"\", 200\n```\n\nInbound 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.\n\nThe 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`\n\nand `nylas email send --yes`\n\ngive 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`\n\n, because without it send commands wait on a confirmation prompt an agent loop will never answer, and `--json`\n\n, 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`\n\n, so run a per-tenant CLI process or pass `--api-key`\n\nexplicitly. Same tool-calling pattern, different transport.\n\nA 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.\n\nStart 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?", "url": "https://wpnews.pro/news/give-a-langchain-agent-a-real-inbox", "canonical_source": "https://dev.to/qasim157/give-a-langchain-agent-a-real-inbox-2bbd", "published_at": "2026-06-15 20:04:21+00:00", "updated_at": "2026-06-15 20:33:05.085797+00:00", "lang": "en", "topics": ["large-language-models", "ai-agents", "developer-tools"], "entities": ["LangChain", "Nylas", "ChatAnthropic", "LangGraph"], "alternates": {"html": "https://wpnews.pro/news/give-a-langchain-agent-a-real-inbox", "markdown": "https://wpnews.pro/news/give-a-langchain-agent-a-real-inbox.md", "text": "https://wpnews.pro/news/give-a-langchain-agent-a-real-inbox.txt", "jsonld": "https://wpnews.pro/news/give-a-langchain-agent-a-real-inbox.jsonld"}}