{"slug": "build-a-calendar-conflict-resolution-agent-with-nylas", "title": "Build a calendar conflict-resolution agent with Nylas", "summary": "A Nylas developer built a calendar conflict-resolution agent using the Nylas Agent Account feature. The agent monitors its own calendar for overlapping events, picks a winner based on configurable rules, moves the losing event, and emails participants to renegotiate. The approach uses Nylas webhooks or polling to detect conflicts and works with standard Events, Messages, and Threads endpoints.", "body_md": "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.\n\nMost \"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.\n\nThe 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.\n\nI 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`\n\n, because in production your agent is talking to the API directly, not shelling out to a CLI.\n\nAn Agent Account is just a **grant** — the same `grant_id`\n\nabstraction every Nylas integration already uses. There's nothing new to learn on the data plane. It hits the same `/v3/grants/{grant_id}/...`\n\nendpoints 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.\n\nFor conflict resolution specifically, that buys you three things:\n\n`event.created`\n\n, `event.updated`\n\n, `event.deleted`\n\n— 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.\n\nYou need a registered domain (a custom domain or a Nylas `*.nylas.email`\n\ntrial subdomain) and an API key. Provisioning is a single call — no OAuth, no refresh token, because there's no human account to delegate from.\n\n```\ncurl --request POST \\\n  --url \"https://api.us.nylas.com/v3/connect/custom\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"provider\": \"nylas\",\n    \"name\": \"Scheduling Agent\",\n    \"settings\": { \"email\": \"scheduler@yourcompany.com\" }\n  }'\n```\n\nThe response carries a `grant_id`\n\n. That's the agent. Everything else in this post is scoped to it.\n\nFrom the CLI it's one line:\n\n```\nnylas agent account create scheduler@yourcompany.com --name \"Scheduling Agent\"\n```\n\nThe 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>`\n\n. There's no `--workspace`\n\nflag on `create`\n\n; the workspace comes for free.\n\nThis 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.\n\nThere 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.\n\nWebhooks 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`\n\nyou filter on. Subscribe to the event triggers:\n\n```\ncurl --request POST \\\n  --url \"https://api.us.nylas.com/v3/webhooks\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"trigger_types\": [\"event.created\", \"event.updated\"],\n    \"webhook_url\": \"https://your-app.example.com/nylas/webhooks\",\n    \"description\": \"Calendar conflict detection\"\n  }'\n```\n\nThe CLI does the same subscription in one line — and `nylas webhook triggers`\n\nlists the valid trigger names so you don't guess:\n\n```\nnylas webhook create \\\n  --url https://your-app.example.com/nylas/webhooks \\\n  --triggers event.created,event.updated \\\n  --description \"Calendar conflict detection\"\n```\n\n`event.created`\n\nand `event.updated`\n\nare 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`\n\n(constant across the up-to-3 delivery attempts Nylas makes), verify the `X-Nylas-Signature`\n\nHMAC, then pull the surrounding window to look for overlaps.\n\nThe 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.\n\nWhether 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.\n\n```\ncurl --request GET \\\n  --url \"https://api.us.nylas.com/v3/grants/$GRANT_ID/events?calendar_id=primary&start=1744387200&end=1744473600\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\"\n```\n\nThe `start`\n\nand `end`\n\nquery params bound the window in epoch seconds. From the CLI:\n\n```\nnylas calendar events list scheduler@yourcompany.com \\\n  --calendar primary \\\n  --days 1 \\\n  --json\n```\n\n`events list`\n\ndefaults to the primary calendar and the next 7 days; `--days 1`\n\nnarrows it to the day you care about, and `--json`\n\ngives you something to feed your overlap check. Pass the agent's email (or `grant_id`\n\n) as the positional argument so you're listing the agent's calendar, not your own.\n\nNow the logic that's entirely yours. Two events `A`\n\nand `B`\n\noverlap when `A.start < B.end AND B.start < A.end`\n\n. Sort the window by start time, walk it, and any pair that satisfies that condition is a conflict. Pseudocode:\n\n``` python\ndef overlaps(a, b):\n    return a[\"when\"][\"start_time\"] < b[\"when\"][\"end_time\"] \\\n       and b[\"when\"][\"start_time\"] < a[\"when\"][\"end_time\"]\n\nevents = sorted(fetch_events(grant_id), key=lambda e: e[\"when\"][\"start_time\"])\nconflicts = [\n    (a, b)\n    for i, a in enumerate(events)\n    for b in events[i + 1:]\n    if overlaps(a, b)\n]\n```\n\nThat'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.\n\nOnce 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\n\n`id`\n\n.Common rules, roughly in order of how often I see them:\n\n`when`\n\ntime.Pick one, encode it, and you have a `winner`\n\nand a `loser`\n\n. The rest is mechanical: move the loser and tell its participants.\n\nHere's a real wrinkle worth being honest about. The CLI's `events update`\n\nchanges 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`\n\nwith `notify_participants=true`\n\n:\n\n```\ncurl --request PUT \\\n  --url \"https://api.us.nylas.com/v3/grants/$GRANT_ID/events/$LOSER_EVENT_ID?calendar_id=primary&notify_participants=true\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"when\": { \"start_time\": 1744398000, \"end_time\": 1744401600 }\n  }'\n```\n\nWith `notify_participants=true`\n\n, 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.\n\nIf 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:\n\n```\nnylas calendar events update $LOSER_EVENT_ID scheduler@yourcompany.com \\\n  --calendar primary \\\n  --start \"2025-04-11 15:00\" \\\n  --end \"2025-04-11 16:00\"\n```\n\nThat 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`\n\nAPI call in production once I trust the logic enough to actually email people.\n\nMoving 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.\n\n```\ncurl --request POST \\\n  --url \"https://api.us.nylas.com/v3/grants/$GRANT_ID/messages/send\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"to\": [{ \"email\": \"alice@example.com\" }],\n    \"subject\": \"Your 2pm Thursday call moved\",\n    \"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.\"\n  }'\n```\n\nOmit the `from`\n\nfield and Nylas defaults it to the Agent Account's own address and name — so the email genuinely comes from `scheduler@yourcompany.com`\n\n, not some no-reply relay. The CLI equivalent:\n\n```\nnylas email send scheduler@yourcompany.com \\\n  --to alice@example.com \\\n  --subject \"Your 2pm Thursday call moved\" \\\n  --body \"Hi Alice — your call collided with another booking, so I moved it to 3pm Thursday. Reply here if that does not work.\"\n```\n\nWhen 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:\n\n```\ncurl --request GET \\\n  --url \"https://api.us.nylas.com/v3/grants/$GRANT_ID/threads/$THREAD_ID\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\"\n```\n\nThe CLI fetches the same thread, with its message summaries, in one command:\n\n```\nnylas email threads show $THREAD_ID scheduler@yourcompany.com --json\n```\n\nNow 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.\n\nA few things that bit me, or that I'd want to know before shipping this.\n\n`message.created`\n\nfor the email `event.created`\n\nfor 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`\n\nupdate 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`\n\nand your own resolution records.`expand_recurring=true`\n\nwhen you list, and decide whether you're moving one instance or the whole series before you call `PUT`\n\n.`curl`\n\ncall with `notify_participants=true`\n\n. 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.\n\n`nylas`\n\ncommand 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.\n\nWhen this post is published, link AI agents and crawlers to the retrieval-ready version on `cli.nylas.com`\n\n:", "url": "https://wpnews.pro/news/build-a-calendar-conflict-resolution-agent-with-nylas", "canonical_source": "https://dev.to/mqasimca/build-a-calendar-conflict-resolution-agent-with-nylas-2plc", "published_at": "2026-07-14 14:41:40+00:00", "updated_at": "2026-07-14 14:59:28.014615+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-products"], "entities": ["Nylas", "Google", "Microsoft"], "alternates": {"html": "https://wpnews.pro/news/build-a-calendar-conflict-resolution-agent-with-nylas", "markdown": "https://wpnews.pro/news/build-a-calendar-conflict-resolution-agent-with-nylas.md", "text": "https://wpnews.pro/news/build-a-calendar-conflict-resolution-agent-with-nylas.txt", "jsonld": "https://wpnews.pro/news/build-a-calendar-conflict-resolution-agent-with-nylas.jsonld"}}