# Build a calendar conflict-resolution agent with Nylas

> Source: <https://dev.to/mqasimca/build-a-calendar-conflict-resolution-agent-with-nylas-2plc>
> Published: 2026-07-14 14:41:40+00:00

Booking races create conflicts nobody notices until two people show up for the same slot. Two requests land within a few seconds of each other, both pass whatever availability check you had, and now there are two events sitting on the same calendar at 2pm Thursday. The calendar UI happily renders both. Nobody finds out until the meeting starts and a second invite link shows up in the chat.

Most "AI scheduling" demos sidestep this entirely. They assume an open slot, negotiate a time, and book it. That's the easy half. The hard half is what happens *after* the booking, when the calendar already has a clash and somebody has to decide which event survives and tell the loser to move. That's a detection-and-resolution problem, not a negotiation problem, and it's the one I want to walk through here.

The approach: give the agent its own calendar — an **Agent Account** — let it watch its own events for overlaps, pick a winner by a rule you control, move the losing event, and email that event's participants to renegotiate. No human inbox involved. The agent *is* the participant.

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 operation also gets the raw `curl`

, because in production your agent is talking to the API directly, not shelling out to a CLI.

An Agent Account is just a **grant** — the same `grant_id`

abstraction every Nylas integration already uses. There's nothing new to learn on the data plane. It hits the same `/v3/grants/{grant_id}/...`

endpoints as any connected Google or Microsoft account: Events, Messages, Threads, Webhooks. The difference is that the mailbox and calendar belong to the agent, not to a human you're impersonating.

For conflict resolution specifically, that buys you three things:

`event.created`

, `event.updated`

, `event.deleted`

— that fire whenever something changes on that calendar, including when an external participant accepts an invite the agent doesn't control.One honest scoping note up front: **Scheduler and availability-config endpoints aren't supported for Agent Accounts.** You can't lean on a hosted booking page to enforce no-double-booking for you. You work with the grant's own Events and free/busy data, and the conflict *logic* lives in your app. That's actually fine for this use case — you want full control over the which-to-keep rule anyway, and Scheduler's "find an open slot" model doesn't help once the slot is already double-booked.

You need a registered domain (a custom domain or a Nylas `*.nylas.email`

trial subdomain) and an API key. Provisioning is a single call — no OAuth, no refresh token, because there's no human account to delegate from.

```
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": "Scheduling Agent",
    "settings": { "email": "scheduler@yourcompany.com" }
  }'
```

The response carries a `grant_id`

. That's the agent. Everything else in this post is scoped to it.

From the CLI it's one line:

```
nylas agent account create scheduler@yourcompany.com --name "Scheduling Agent"
```

The API auto-creates a default workspace and policy for the account. If you later want a custom policy — to tighten attachment limits or spam handling on the inbox — attach it with `nylas workspace update <workspace-id> --policy-id <policy-id>`

. There's no `--workspace`

flag on `create`

; the workspace comes for free.

This is the heart of it, and it's the part that's *your* code, not an endpoint. Nylas doesn't have a "find me conflicts" call. What it gives you is the event data; you compare start/end times and decide.

There are two ways to know an event changed: react to a webhook, or poll. Both are legitimate, and I'll show both because the right answer depends on how fast you need to react.

Webhooks in Nylas are **application-scoped**, not grant-scoped. You subscribe once at the app level, and events for every grant arrive at one endpoint, each payload carrying the `grant_id`

you filter on. Subscribe to the event triggers:

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/webhooks" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "trigger_types": ["event.created", "event.updated"],
    "webhook_url": "https://your-app.example.com/nylas/webhooks",
    "description": "Calendar conflict detection"
  }'
```

The CLI does the same subscription in one line — and `nylas webhook triggers`

lists the valid trigger names so you don't guess:

```
nylas webhook create \
  --url https://your-app.example.com/nylas/webhooks \
  --triggers event.created,event.updated \
  --description "Calendar conflict detection"
```

`event.created`

and `event.updated`

are both [supported for Agent Accounts](https://developer.nylas.com/docs/v3/agent-accounts/supported-endpoints/) — they fire for every change to the agent's events, whether the agent made the change or an external participant did by accepting an invite. When one arrives, dedupe on the top-level notification `id`

(constant across the up-to-3 delivery attempts Nylas makes), verify the `X-Nylas-Signature`

HMAC, then pull the surrounding window to look for overlaps.

The webhook tells you *something* moved. It doesn't tell you what it now clashes with. For that you list the events around the changed event's time range and compare.

Whether you got here from a webhook or a polling loop, the detection step is the same: list the events in a window and check every pair for overlap.

```
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/$GRANT_ID/events?calendar_id=primary&start=1744387200&end=1744473600" \
  --header "Authorization: Bearer $NYLAS_API_KEY"
```

The `start`

and `end`

query params bound the window in epoch seconds. From the CLI:

```
nylas calendar events list scheduler@yourcompany.com \
  --calendar primary \
  --days 1 \
  --json
```

`events list`

defaults to the primary calendar and the next 7 days; `--days 1`

narrows it to the day you care about, and `--json`

gives you something to feed your overlap check. Pass the agent's email (or `grant_id`

) as the positional argument so you're listing the agent's calendar, not your own.

Now the logic that's entirely yours. Two events `A`

and `B`

overlap when `A.start < B.end AND B.start < A.end`

. Sort the window by start time, walk it, and any pair that satisfies that condition is a conflict. Pseudocode:

``` python
def overlaps(a, b):
    return a["when"]["start_time"] < b["when"]["end_time"] \
       and b["when"]["start_time"] < a["when"]["end_time"]

events = sorted(fetch_events(grant_id), key=lambda e: e["when"]["start_time"])
conflicts = [
    (a, b)
    for i, a in enumerate(events)
    for b in events[i + 1:]
    if overlaps(a, b)
]
```

That's the detection nobody-noticed-the-clash problem solved: you're now finding the double-booking the moment it lands, instead of at 2pm Thursday.

Once you have a conflicting pair, you need a rule for which one survives. This is a business decision, so it lives in your code — and a quick warning: **Agent Accounts don't support custom metadata on events**, so you can't stash "priority: high" on the event itself and filter by it. Keep your conflict state and your tie-break inputs in your own database, keyed by event

`id`

.Common rules, roughly in order of how often I see them:

`when`

time.Pick one, encode it, and you have a `winner`

and a `loser`

. The rest is mechanical: move the loser and tell its participants.

Here's a real wrinkle worth being honest about. The CLI's `events update`

changes the event, but it has no flag to notify participants — that's an API-only query parameter. So when you need the move to push an updated invite to everyone's calendar, use `curl`

with `notify_participants=true`

:

```
curl --request PUT \
  --url "https://api.us.nylas.com/v3/grants/$GRANT_ID/events/$LOSER_EVENT_ID?calendar_id=primary&notify_participants=true" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "when": { "start_time": 1744398000, "end_time": 1744401600 }
  }'
```

With `notify_participants=true`

, Nylas sends an ICS update and the new time lands on every participant's Google, Outlook, or Apple calendar automatically. The agent is the organizer here, so it's allowed to move its own event.

If you only want to reschedule on the agent's side — say you're staging a tentative new time before you've confirmed it with anyone — the CLI is the cleaner tool, since its silent-by-default behavior is exactly what you want:

```
nylas calendar events update $LOSER_EVENT_ID scheduler@yourcompany.com \
  --calendar primary \
  --start "2025-04-11 15:00" \
  --end "2025-04-11 16:00"
```

That updates the stored event without firing invites. In practice I reach for the CLI version while developing and testing the rule, then switch to the `notify_participants=true`

API call in production once I trust the logic enough to actually email people.

Moving the event tells calendars the new time. It doesn't *explain* anything, and for a contested slot you usually want a human-readable note: "your meeting clashed with a higher-priority booking, here's the new time, reply if it doesn't work." That's a plain email from the agent's own address.

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/$GRANT_ID/messages/send" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "to": [{ "email": "alice@example.com" }],
    "subject": "Your 2pm Thursday call moved",
    "body": "Hi Alice — your call collided with another booking on my calendar, so I moved it to 3pm Thursday. The updated invite is already on your calendar. If 3pm does not work, reply here and I will find another slot."
  }'
```

Omit the `from`

field and Nylas defaults it to the Agent Account's own address and name — so the email genuinely comes from `scheduler@yourcompany.com`

, not some no-reply relay. The CLI equivalent:

```
nylas email send scheduler@yourcompany.com \
  --to alice@example.com \
  --subject "Your 2pm Thursday call moved" \
  --body "Hi Alice — your call collided with another booking, so I moved it to 3pm Thursday. Reply here if that does not work."
```

When Alice replies, that reply lands in the agent's mailbox and threads against the original message. Read the thread to keep the negotiation context before the agent decides on a counter-slot:

```
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/$GRANT_ID/threads/$THREAD_ID" \
  --header "Authorization: Bearer $NYLAS_API_KEY"
```

The CLI fetches the same thread, with its message summaries, in one command:

```
nylas email threads show $THREAD_ID scheduler@yourcompany.com --json
```

Now you have a closed loop: detect the clash, move the loser, notify its people, and read their responses to renegotiate if the new time doesn't stick — all on one grant the agent owns end to end.

A few things that bit me, or that I'd want to know before shipping this.

`message.created`

for the email `event.created`

for the calendar entry. Decide which one drives your logic and ignore the other; driving off the event is cleaner because the object already has structured times and participants.`notify_participants=true`

update and every renegotiation email counts toward the Agent Account's daily send limit (200 messages/account/day on the free plan). If a storm of conflicts triggers a storm of emails, you can blow through it. Batch where you can.`id`

and your own resolution records.`expand_recurring=true`

when you list, and decide whether you're moving one instance or the whole series before you call `PUT`

.`curl`

call with `notify_participants=true`

. If it's an internal/staging change, the CLI is fine and quieter.The skeleton above is deliberately small: list, compare, move, email. The interesting work is the rule — first-booked, priority, duration — and the renegotiation flow once someone pushes back. Both of those are your domain logic sitting on top of a grant that behaves exactly like every other Nylas grant you've used.

`nylas`

command used aboveIf you want the negotiation-from-an-open-slot side of this — proposing times, collecting picks, booking the winner — that's a different post. This one is strictly about the clash that already happened, and getting the agent to clean it up before anyone notices.

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

:
