# Run a shared team inbox owned by an AI agent

> Source: <https://dev.to/mqasimca/run-a-shared-team-inbox-owned-by-an-ai-agent-3md4>
> Published: 2026-07-10 11:56:20+00:00

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.

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:

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.

`message.created`

webhooks for inbound mail and reads/writes the real mailbox.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.

You need:

`*.nylas.email`

trial subdomain. New domains warm over roughly four weeks, so don't provision one the morning of a launch.`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](https://developer.nylas.com/docs/v3/getting-started/agent-accounts/) first. The walkthrough below assumes you've at least skimmed it.

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!"
    }
  }'
```

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!"
```

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.

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!"
    }
  }'
```

The CLI has a dedicated command for it:

```
nylas agent account update support@yourcompany.com \
  --app-password "Sh4redInb0xReview2024!"
```

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.

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:

`starred`

field flips to `true`

and a `message.updated`

webhook fires.`folders`

array updates.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.

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" }'
```

Or from the terminal:

```
nylas email folders create "Needs review" <NYLAS_GRANT_ID>
```

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>"] }'
```

From the CLI it's one command:

```
nylas email move <MESSAGE_ID> --folder <NEEDS_REVIEW_FOLDER_ID> <NYLAS_GRANT_ID>
```

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.

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."
  }'
```

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."
```

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>
```

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>
```

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>"
```

Or:

```
nylas email list <NYLAS_GRANT_ID> --limit 10
```

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.

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:

`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.`id`

.`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.`X-Nylas-Signature`

header: a hex HMAC-SHA256 of the `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.

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

`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.

`nylas`

command shown above, with its flags.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.

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

:
