# Round-robin email routing to a team

Source: https://developer.nylas.com/docs/cookbook/use-cases/automate/round-robin-email-routing/

Shared inboxes like sales@ or support@ collapse the moment two people grab the same lead and a third gets ignored. Round-robin routing fixes that by handing each new message to the next person in a fixed rotation, so a 5-person team splits 100 inbound leads into roughly 20 each instead of a free-for-all. This recipe shows how to receive inbound mail through a webhook, pick the next assignee in code, and send an automatic acknowledgment, all on top of the Nylas Email API.

Nylas has no assignment endpoint, and that's the key design point: the rotation lives entirely in your application. The API tells you when mail arrives and lets you reply or notify; your code owns who gets what. For the one-time webhook subscription and challenge handshake, follow [the new email webhook recipe](/docs/cookbook/use-cases/build/new-email-webhook/) first, then come back here for the routing logic.

## How does round-robin email assignment work?

Round-robin assignment keeps a counter that advances by 1 with every new message. You hold a list of team members and compute `index = counter % team.length`, so a 4-person list cycles 0, 1, 2, 3, then wraps back to 0. The message goes to that person, and the counter increments for the next one.

The counter is application state, not something the Email API stores. A `message.created` webhook fires when mail lands in the shared mailbox, your handler reads the current counter, picks the assignee, and persists the new value. Keeping the rotation server-side means you control fairness completely: weight it, skip absent members, or reset it nightly. A typical handler runs this whole decision in under 50 ms before it acknowledges the webhook. The math is trivial; the discipline is making the state durable and concurrency-safe, which the later sections cover.

## How do I receive inbound email for routing?

Inbound mail reaches your app through the `message.created` trigger. Nylas `POST`s a JSON notification when a message arrives on the connected mailbox, with the message nested under `data.object` carrying its `id` and `grant_id`. Your handler has 10 seconds to return a `200 OK`, so acknowledge first and do the routing afterward to avoid a timeout.

The payload only includes IDs and a partial body, so fetch the full message with `GET /v3/grants/{grant_id}/messages/{message_id}` when you need the sender or subject for assignment rules. Verify the `X-Nylas-Signature` header before you trust a payload, as covered in [verify webhook signatures](/docs/cookbook/use-cases/build/verify-webhook-signatures/). The handler below acknowledges within the window, then reads the IDs it needs to route.

```js [receiveInbound-Node.js]


const app = express();

app.post("/webhooks/nylas", express.raw({ type: "*/*" }), (req, res) => {
  // Reject forged events before acting (see Verify webhook signatures).
  if (!verifyNylasSignature(req.body, req.get("X-Nylas-Signature"))) {
    return res.status(401).end();
  }
  res.status(200).end(); // acknowledge within 10 seconds

  const { object } = JSON.parse(req.body).data;
  routeMessage(object.grant_id, object.id); // your routing logic
});

app.listen(3000);
```

```python
from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks/nylas", methods=["POST"])
def notification():
    # Reject forged events before acting (see Verify webhook signatures).
    if not verify_nylas_signature(request.get_data(), request.headers.get("X-Nylas-Signature")):
        return "", 401
    obj = request.get_json()["data"]["object"]
    # acknowledge first, then route off the request thread
    threading.Thread(
        target=route_message, args=(obj["grant_id"], obj["id"]), daemon=True
    ).start()
    return "", 200
```

## How do I pick the next assignee in rotation?

Picking the assignee is a single modulo operation against a counter you own. Read the counter, compute `counter % team.length`, select that member, then store `counter + 1`. With a 6-person team and 600 leads per month, each person lands close to 100, give or take the wrap point. The exact spread depends on when your billing window starts and stops.

Concurrency is the one trap worth naming. Webhooks deliver in parallel, so two notifications arriving 5 milliseconds apart can both read counter `7` and both assign the same person. Use an atomic increment instead: Redis `INCR`, a SQL `UPDATE ... RETURNING`, or a row lock returns a unique value to each caller. The snippet below uses an atomic increment so every message gets a distinct slot, then derives the assignee from the returned value.

```js [pickAssignee-Node.js]
const team = [
  "ana@yourco.com",
  "bo@yourco.com",
  "cy@yourco.com",
];

async function nextAssignee(redis) {
  // Atomic: each caller gets a unique, monotonically increasing count.
  // INCR returns 1 on the first call, so subtract 1 to start at index 0.
  const count = await redis.incr("roundrobin:sales");
  return team[(count - 1) % team.length];
}
```

```python
team = ["ana@yourco.com", "bo@yourco.com", "cy@yourco.com"]

def next_assignee(redis):
    # Atomic increment returns a unique value per caller.
    # INCR returns 1 on the first call, so subtract 1 to start at index 0.
    count = redis.incr("roundrobin:sales")
    return team[(count - 1) % len(team)]
```

## How do I notify the assignee or auto-reply?

Once you have an assignee, you can act in two ways: send an internal heads-up to the team member or send an acknowledgment back to the sender. Both use `POST /v3/grants/{grant_id}/messages/send`, the same endpoint covered in [send email without SMTP](/docs/cookbook/use-cases/build/send-email-without-smtp/). A short auto-reply that confirms receipt within 1 minute measurably lifts reply rates on cold inbound leads.

To reply on the original thread, fetch the inbound message first and pass its `reply_to_message_id` so the acknowledgment threads cleanly. The snippet below sends an internal notification to the chosen assignee with the lead's address in the body. Swap the recipient and content to auto-reply to the sender instead.

```js [notifyAssignee-Node.js]
async function notify(nylas, grantId, assignee, lead) {
  await nylas.messages.send({
    identifier: grantId,
    requestBody: {
      to: [{ email: assignee }],
      subject: `New lead assigned: ${lead}`,
      body: `You're up. Reply to ${lead} from the shared inbox.`,
    },
  });
}
```

```python
def notify(nylas, grant_id, assignee, lead):
    nylas.messages.send(
        identifier=grant_id,
        request_body={
            "to": [{"email": assignee}],
            "subject": f"New lead assigned: {lead}",
            "body": f"You're up. Reply to {lead} from the shared inbox.",
        },
    )
```

## How do I keep routing fair and avoid duplicates?

Two delivery properties shape a correct router. Nylas guarantees at-least-once delivery, so the same `message.created` can arrive twice and would otherwise advance the counter twice and double-assign 1 message. Dedupe on the message `id` before you route: store processed IDs in a set with a 24-hour expiry and skip any repeat. This single check prevents the most common fairness bug in production routers.

Fairness also breaks when the rotation ignores reality. If someone is on leave, a blind modulo still feeds them about 25% of a 4-person queue while they're out. Maintain an availability flag per member and filter the list before the modulo, or weight a senior rep at 0.5 so they receive half the volume. Reset or rebalance the counter on a schedule that matches your shifts. For retry timing, ordering, and signature verification, read [Using webhooks with Nylas](/docs/v3/notifications/), since out-of-order notifications can otherwise skew your counts.

## What's next

- [New email webhook](/docs/cookbook/use-cases/build/new-email-webhook/) for the subscription, challenge handshake, and notification basics
- [Send email without SMTP](/docs/cookbook/use-cases/build/send-email-without-smtp/) for the full send and reply payload reference
- [Using webhooks with Nylas](/docs/v3/notifications/) for delivery guarantees, retries, and signature verification