# Give a LangChain Agent a Real Inbox

> Source: <https://dev.to/qasim157/give-a-langchain-agent-a-real-inbox-2bbd>
> Published: 2026-06-15 20:04:21+00:00

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?
