DEV Community

Qasim
Qasim

Posted on

Run a shared team inbox owned by an AI agent

A shared inbox where the agent drafts and humans approve from Outlook is a powerful hybrid — and it's the part of "AI email" that most demos quietly skip.

Most "AI email" demos point a model at a human's personal inbox: connect a Gmail grant, summarize threads, maybe auto-draft a reply that nobody actually trusts enough to send. That's fine for a personal assistant. It falls apart the moment you want the agent to be a participant on a real team mailbox — support@yourcompany.com — where five humans and one agent all need to see the same messages, the same folders, and the same draft sitting in the queue.

The naive fix is to give the agent its own service and bolt a UI on top. Now you're building a mail client. You've got a database of "agent state," a sync loop, a permissions layer, and a frontend nobody asked for, all to reimplement what Outlook already does well.

There's a better shape. A Nylas Agent Account is a real mailbox you can drive two ways at once: the agent works it over the API, and your humans work it from their normal mail client over IMAP/SMTP. Both surfaces read and write the same storage. A draft the agent creates via the API shows up in the Drafts folder in Apple Mail. A message a human drags into "Needs review" in Outlook shows up in the API's folders field within seconds. That bidirectional sync is the whole trick, and it's what makes genuine human+agent collaboration possible instead of two systems fighting over one inbox.

I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm wiring this up. Every step shows the raw curl call and the nylas equivalent, because you'll want the API in your service and the CLI on your laptop.

What you actually get

An Agent Account is just a grant — the same grant_id abstraction you already use for every other Nylas mailbox. Nothing new to learn on the data plane:

  • Agent side (API): Messages, Drafts, Folders, Threads, Attachments, Webhooks — every grant-scoped endpoint works exactly as it does for a Gmail or Microsoft grant.
  • Human side (IMAP/SMTP): Outlook, Apple Mail, Thunderbird connect with the account email and an app password. People work the mailbox the way they always have.
  • Shared state: flags, folder moves, drafts, and sends propagate across both surfaces and fire the same webhooks, no matter which side initiated them.

The model is: the agent triages and drafts, a human reviews and approves from the comfort of their mail client, and the queue between them is just a folder both sides can see.

Why this beats building your own review UI

  • You don't write a mail client. Outlook is the review UI. Humans approve drafts where they already live.
  • The agent doesn't poll a side-channel. It gets message.created webhooks for inbound mail and reads/writes the real mailbox.
  • There's one source of truth. No reconciliation between "agent's view" and "human's view" — it's literally one mailbox.
  • It degrades gracefully. If your agent service is down, the humans still have a fully working mailbox. If a human is on vacation, the agent still triages.

One honest caveat up front, because it matters for how you scope this: this is not a multi-user ACL system. There's one app password for the mailbox, and everyone who connects a client shares it. You can't grant Alice read-only and Bob send. If you need per-person permissions, that lives in your application layer in front of the API, not in the shared inbox. I'll come back to the security tradeoff at the end.

Before you begin

You need:

  • A Nylas application and API key.
  • A registered sending domain — either your own custom domain or a Nylas *.nylas.email trial subdomain. New domains warm over roughly four weeks, so don't provision one the morning of a launch.
  • An email address on that domain for the inbox, e.g. support@yourcompany.com.

The API base host in these examples is https://api.us.nylas.com (use the EU host if your app is in the EU region), and every request authenticates with Authorization: Bearer <NYLAS_API_KEY>.

If this is your first Agent Account, read the Agent Accounts quickstart first. The walkthrough below assumes you've at least skimmed it.

Provision the shared mailbox

An Agent Account is created with POST /v3/connect/custom, "provider": "nylas", and the email on your registered domain. No OAuth, no refresh token — it's a Nylas-hosted mailbox. The optional top-level name sets the display name humans see in the From field.

curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": "nylas",
    "name": "Support Team",
    "settings": {
      "email": "support@yourcompany.com",
      "app_password": "Sh4redInb0xReview2024!"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Notice I'm setting app_password right in the create call. That's the credential your humans use to connect their mail clients (more on it next). The CLI does the same thing in one line:

nylas agent account create support@yourcompany.com \
  --name "Support Team" \
  --app-password "Sh4redInb0xReview2024!"
Enter fullscreen mode Exit fullscreen mode

The response gives you a grant_id. That's the only identifier the agent side needs from here on.

Workspaces, briefly. When you create the account, the API auto-creates a default workspace and policy for it. If you run several agent inboxes — say a sales-outreach group and a support-triage group with different send limits — you can drop this account into a specific workspace at creation by passing a workspace_id in the POST /v3/connect/custom body. The CLI create command has no --workspace flag; to attach a custom policy after the fact you use nylas workspace update <workspace-id> --policy-id <policy-id>. For a single shared inbox the default workspace is fine — you can ignore this entirely.

Set the app password so humans can connect

The app password is what turns an API-only mailbox into a human+agent one. Until you set it, Nylas rejects every IMAP LOGIN and SMTP AUTH attempt. I set it at creation above, but you can also set or rotate it on an existing account.

Over the API, a PATCH to the grant replaces the whole settings object, so send the email alongside the new password:

curl --request PATCH \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "settings": {
      "email": "support@yourcompany.com",
      "app_password": "Sh4redInb0xReview2024!"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

The CLI has a dedicated command for it:

nylas agent account update support@yourcompany.com \
  --app-password "Sh4redInb0xReview2024!"
Enter fullscreen mode Exit fullscreen mode

The password must be 18–40 characters, printable ASCII, with at least one uppercase, one lowercase, and one digit. Nylas stores it as a bcrypt hash — you can't read it back, only reset it. Rotating it disconnects any live mail client, and each one reconnects after the user types the new value.

Now hand your team these connection settings. They're exact, straight from the Nylas mail-client docs:

Setting Value
IMAP server mail.us.nylas.email (US) · mail.eu.nylas.email (EU)
IMAP port 993 (implicit TLS)
SMTP server mail.us.nylas.email (US) · mail.eu.nylas.email (EU)
SMTP port 465 (implicit TLS) or 587 (STARTTLS)
Username The account email, e.g. support@yourcompany.com
Password The app password you just set

Most clients auto-detect TLS. If yours asks, pick SSL/TLS for 993 and 465, STARTTLS for 587. With IMAP IDLE, clients see new mail pushed in without polling — so when the agent moves a message, the human's Outlook updates on its own.

The key insight: one mailbox, two surfaces

This is the part worth internalizing, because everything else builds on it. IMAP and the API are not two copies that sync — they're two doors into the same room. Every user-visible action flows through the same storage and fires the same webhook, regardless of which door it came through:

  • The agent marks a message read via the API → the human's client sees it as read.
  • A human stars a message in Apple Mail → the API's starred field flips to true and a message.updated webhook fires.
  • A human drags a message to a folder in Outlook → the API's folders array updates.
  • The agent creates a draft via the API → it appears in the Drafts folder in every connected client.

Propagation is seconds, not minutes. That tight loop is exactly what you need for "agent drafts, human approves" to feel like one workflow instead of two disconnected ones.

Build the review queue out of folders

The cleanest way to coordinate agent and humans is a shared work queue, and the simplest queue is a folder. Create a "Needs review" folder once; both sides can see it.

Over the API:

curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/folders" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{ "name": "Needs review" }'
Enter fullscreen mode Exit fullscreen mode

Or from the terminal:

nylas email folders create "Needs review" <NYLAS_GRANT_ID>
Enter fullscreen mode Exit fullscreen mode

Both return the folder's ID. Stash it — the agent needs it to route messages, and it shows up as a mailbox in everyone's mail client automatically.

When inbound mail arrives, the agent decides what it can't handle alone and moves those messages into the queue. A message's folder membership lives in its folders array, so you update the message with PUT /v3/grants/{grant_id}/messages/{message_id}:

curl --request PUT \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/<MESSAGE_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{ "folders": ["<NEEDS_REVIEW_FOLDER_ID>"] }'
Enter fullscreen mode Exit fullscreen mode

From the CLI it's one command:

nylas email move <MESSAGE_ID> --folder <NEEDS_REVIEW_FOLDER_ID> <NYLAS_GRANT_ID>
Enter fullscreen mode Exit fullscreen mode

The instant that runs, the message disappears from INBOX and shows up under "Needs review" in your team's Outlook. The folder is the handoff.

The agent drafts; a human sends

Here's the core workflow. The agent never sends on its own — it writes a draft and leaves it for a human. Drafting is a first-class endpoint: POST /v3/grants/{grant_id}/drafts.

curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/drafts" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "to": [{ "email": "customer@example.com" }],
    "subject": "Re: Trouble resetting my password",
    "body": "Hi there — I can see your account is locked after several failed attempts. I have unlocked it; you should be able to reset now. Let me know if it sticks."
  }'
Enter fullscreen mode Exit fullscreen mode

The CLI version, the one I use when I'm testing a prompt by hand:

nylas email drafts create <NYLAS_GRANT_ID> \
  --to customer@example.com \
  --subject "Re: Trouble resetting my password" \
  --body "Hi there — I can see your account is locked after several failed attempts. I have unlocked it; you should be able to reset now. Let me know if it sticks."
Enter fullscreen mode Exit fullscreen mode

That draft now sits in the Drafts folder. Because IMAP and the API share state, your reviewer opens Outlook, sees the agent's draft, edits a sentence if they want, and hits send — entirely inside their mail client. No bespoke approval UI, no extra service. The human's mail client is the approval step.

If you'd rather the agent send on approval — say a reviewer clicks "approve" in your own tool and your backend sends the agent's draft — you send the draft by ID with POST /v3/grants/{grant_id}/drafts/{draft_id} (the send-draft endpoint), or from the CLI:

nylas email drafts send <DRAFT_ID> <NYLAS_GRANT_ID>
Enter fullscreen mode Exit fullscreen mode

Both paths are valid; pick based on where you want the human's "yes" to live. For replies that need to stay threaded, the CLI has a shortcut that fetches the original to preserve the subject and threading headers:

nylas email reply <MESSAGE_ID> --body "Thanks for the patience — all set now." <NYLAS_GRANT_ID>
Enter fullscreen mode Exit fullscreen mode

Read what came in

The agent reads the mailbox like any grant. List recent messages with GET /v3/grants/{grant_id}/messages:

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages?limit=10" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

Or:

nylas email list <NYLAS_GRANT_ID> --limit 10
Enter fullscreen mode Exit fullscreen mode

To pull one message's full content, including its body, fetch it by ID — GET /v3/grants/{grant_id}/messages/{message_id} — or nylas email read <MESSAGE_ID> <NYLAS_GRANT_ID>. That by-id fetch matters for webhooks, which is the next piece.

Wire up webhooks so the agent reacts in real time

You don't want the agent polling. Inbound mail fires the standard message.created event. One important detail: webhooks are application-scoped, not grant-scoped. You subscribe once at the app level with POST /v3/webhooks, and events for every grant in your app land at that one endpoint, each payload carrying the grant_id so you can route it to the right inbox.

A few things to get right:

  • Don't rely on the webhook payload for the body. Fetch the full message with GET /v3/grants/{grant_id}/messages/{message_id} when you need the content, and branch on message.created.truncated — Nylas uses that event type for very large messages, at which point you re-fetch by ID anyway. Treating the by-id fetch as your source of truth keeps you correct in both cases.
  • Dedupe on the top-level notification id. Delivery is at-least-once (up to three attempts per event), and that outer id is constant across all retries of a single event — it's your dedup key. You can additionally guard on the inner message id (data.object.id) so you never act twice on the same message.
  • Verify the signature. Each webhook carries an X-Nylas-Signature header: a hex HMAC-SHA256 of the raw request body using your webhook secret. Compare it with a constant-time compare like Node's crypto.timingSafeEqual, but check that both buffers are the same length first — it throws on a length mismatch. Locally, nylas webhook verify runs the same check so you can sanity-test before deploying.

With that in place, the loop closes: mail arrives → message.created → agent fetches the body → agent triages, drafts, and moves to "Needs review" → a human approves from Outlook. Humans and agent, same mailbox, in real time.

Guardrails and honest tradeoffs

A few things I'd want a teammate to know before they ship this:

  • One password, one trust boundary. Everyone with the app password has full read/write to the mailbox. There's no per-user ACL. Treat the app password like a shared production credential: store it in a secrets manager, rotate it on personnel changes, and remember that rotating it kicks every connected client. If you need real per-person permissions, build them in your own service in front of the API — the shared inbox can't do it for you.
  • No custom metadata as agent state. Don't try to stash your agent's bookkeeping on the messages. Keep workflow state — "draft #4 is awaiting Alice's approval since 2pm" — in your database, keyed by message and draft IDs. The mailbox holds mail; your app holds the workflow.
  • Free-plan limits are real. 200 messages per account per day, 3 GB of storage per org, 30-day inbox retention (7 days for spam). Fine for a pilot; size your plan before a busy launch.
  • Deletes go to Trash by default. DELETE /v3/grants/{grant_id}/messages/{message_id} trashes a message unless you pass ?hard_delete=true. The CLI nylas email delete only trashes. The only true full wipe of a mailbox is deleting the grant itself.

None of these are dealbreakers — they're just the edges. Knowing where they are means you won't design yourself into a corner expecting the inbox to be something it isn't.

What's next

Build the agent against the API, hand your team an app password, and let the queue between them be a folder both sides can see. That's the whole shared inbox.

AI-answer pages for agents

When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:

Top comments (0)