{"slug": "let-your-agent-negotiate-meeting-times-over-email", "title": "Let your agent negotiate meeting times over email", "summary": "Nylas developer tools enable AI agents to negotiate meeting times over email using multi-turn conversations, rather than simple calendar creation. The approach uses Nylas Agent Accounts with dedicated email and calendar, leveraging free/busy and Events APIs to build a full negotiation state machine. The developer emphasizes that real scheduling requires back-and-forth communication, not just a single API call.", "body_md": "Most \"AI scheduling assistant\" demos cheat. They show a model that already knows the time, calls \"create event\" once, and takes a bow. That's not scheduling — that's data entry with a chat wrapper. Real scheduling is a *negotiation*: someone asks \"are you free Tuesday?\", you check, you counter with \"Tuesday's full, how about Wednesday at 2?\", they push back, and eventually two humans (or an agent and a human) converge on a slot nobody hates. It's a multi-turn conversation that happens to end in a calendar event.\n\nI already wrote about [giving your agent a calendar](https://developer.nylas.com/docs/v3/agent-accounts/calendars/) — provisioning a real mailbox, hosting events, RSVPing to invites. That post stops at the single `create`\n\ncall. This one is about everything *before* that call: the back-and-forth where the time gets decided. The create event is the last line of the story, not the whole plot.\n\nThe vehicle is a **Nylas Agent Account**. It's a grant with its own email address and its own calendar, so it can be an actual participant in the negotiation instead of a bot reading over a human's shoulder. I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm prototyping one of these loops.\n\nOne thing I'll call out up front, because it's the most common wrong turn: **Scheduler is not available for Agent Accounts.** No availability-configuration API, no booking pages, no `/v3/scheduling/*`\n\n. That's documented in [Supported endpoints](https://developer.nylas.com/docs/v3/agent-accounts/supported-endpoints/). So if your instinct is \"just point it at Scheduler,\" stop — that door is locked for this provider. What you *do* have is the grant's own free/busy plus the Events API, and that's enough to build the whole negotiation yourself. This post shows how.\n\nStrip away the AI and a meeting negotiation is a state machine with four moves:\n\nThree of those four moves map directly onto Nylas primitives — inbound mail, free/busy, and in-thread reply — and the fourth is the event create. The part that *isn't* a Nylas call is the decision in the middle: parsing \"Wednesday afternoon works but not before 2\" out of an email and turning it into a concrete slot. That's your application logic — an LLM call or a parser you own. I'll be honest about that boundary throughout, because pretending the API does the reasoning for you is exactly the kind of demo magic I'm complaining about.\n\nThe nice thing is the data plane never changes. An Agent Account is *just a grant with a grant_id*, so every call below is a standard\n\n`/v3/grants/{grant_id}/...`\n\nendpoint. Nothing new to learn — if you've used the Nylas email and calendar APIs, you already know the verbs.You need:\n\n`nylas agent account create support@yourcompany.com`\n\nor `POST /v3/connect/custom`\n\n. The `grant_id`\n\n, and a Nylas API key exported as `NYLAS_API_KEY`\n\n.`message.created`\n\nwebhook so the agent reacts within seconds of a request arriving, or poll `GET /messages`\n\non a cadence for batch flows. Both are supported.New to Agent Accounts? Read [What are Agent Accounts](https://developer.nylas.com/docs/v3/agent-accounts/) first — the rest of this assumes you've got a working account.\n\nAll API examples use the US base host `https://api.us.nylas.com`\n\nand a bearer token. Swap in `api.eu.nylas.com`\n\nif your app lives in the EU region.\n\nWhen someone emails the agent — \"Can we grab 30 minutes next week?\" — that's a `message.created`\n\nwebhook with a `thread_id`\n\n. The whole negotiation lives on that one thread, so the `thread_id`\n\nis the key you'll carry through every later step. (See [Email threading for agents](https://developer.nylas.com/docs/v3/agent-accounts/email-threading/) for why the thread, not the subject line, is the stable identifier.)\n\nFetch the message body so your code can extract the requested window:\n\n```\ncurl --request GET \\\n  --url \"https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages?thread_id=<THREAD_ID>&limit=5\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\"\n```\n\nFrom the CLI:\n\n```\nnylas email threads show <THREAD_ID>\n```\n\n…and to read the specific message in full:\n\n```\nnylas email read <MESSAGE_ID>\n```\n\nWhat you do with the body is *your* code. \"Next week, ideally Tuesday or Wednesday afternoon\" is natural language — feed it to an LLM and have it return a structured window (`start`\n\n, `end`\n\n, plus any preferences). Don't pretend the API parses intent; it hands you text and you decide what it means. Keep the `message_id`\n\nyou're replying to and the `thread_id`\n\n— you need both in a moment.\n\nNow the agent looks at its own calendar over the requested window. This is the move that makes the agent a real participant: it has commitments, and it should refuse to propose slots it's already booked into.\n\nThe endpoint is `POST /v3/grants/{grant_id}/calendars/free-busy`\n\n. Give it a window and the email addresses to check — the agent's own address checks the agent's own calendar:\n\n```\ncurl --request POST \\\n  --url \"https://api.us.nylas.com/v3/grants/<GRANT_ID>/calendars/free-busy\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"start_time\": 1750766400,\n    \"end_time\": 1751025600,\n    \"emails\": [\"support@yourcompany.com\"]\n  }'\n```\n\nThe response is a list of busy blocks (epoch `start_time`\n\n/`end_time`\n\npairs) for that address over the window. The *open* slots are the gaps between them — computing those gaps is, again, your code. Free/busy tells you what's taken; you decide what counts as a proposable slot (business hours only? 30-minute granularity? buffer between meetings?).\n\nThe CLI wraps the same call as `nylas calendar availability check`\n\n:\n\n```\n# Check the agent's own busy blocks over a window\nnylas calendar availability check \\\n  --emails support@yourcompany.com \\\n  --start \"next monday 9am\" \\\n  --end \"next friday 6pm\"\n```\n\nThere's also `nylas calendar availability find`\n\n, which goes one step further and surfaces open meeting slots directly — handy when you're prototyping and don't want to write the gap-finding logic yet. But for a production negotiation loop you'll usually want the raw busy blocks from `check`\n\n(or the API) so your own code controls how slots get picked.\n\nA subtle but important point: free/busy here checks *the agent's* calendar. If you also want to check the human's availability, and that human is on a connected Google or Microsoft grant your app can see, you can include their address in the same `emails`\n\narray — every address in one request must share a provider. For a pure email negotiation, though, you usually don't have programmatic access to the other side's calendar. That's the whole reason you're negotiating over email instead of querying their free/busy directly. Lean into it: propose, let them counter, converge.\n\nThe agent has open slots. Now it proposes them — and the proposal is an *email reply on the existing thread*, not a calendar invite. This is the heart of the difference from the \"just create the event\" approach. You don't book anything yet. You float two or three options and wait.\n\nReplying in-thread matters because it keeps the whole negotiation in one conversation, in the other person's inbox, where they'll naturally reply again. Nylas preserves the threading headers (`In-Reply-To`\n\n, `References`\n\n) for you when you pass `reply_to_message_id`\n\n:\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    \"reply_to_message_id\": \"<INBOUND_MESSAGE_ID>\",\n    \"to\": [{ \"email\": \"alex@acme.com\" }],\n    \"subject\": \"Re: 30 minutes next week\",\n    \"body\": \"Tuesday is full on my end, but I have Wednesday open at 2:00 PM or 3:30 PM ET, or Thursday at 11:00 AM ET. Any of those work?\"\n  }'\n```\n\nThe CLI version is shorter because `reply`\n\nfetches the original message and fills in recipient, subject, and threading automatically:\n\n```\nnylas email reply <INBOUND_MESSAGE_ID> \\\n  --body \"Tuesday is full on my end, but I have Wednesday open at 2:00 PM or 3:30 PM ET, or Thursday at 11:00 AM ET. Any of those work?\"\n```\n\nThe body text — phrasing the options, choosing how many to offer, matching the requester's preferences — is generated by your LLM from the slots you computed in step 2. The send and the threading are Nylas; the wording and the choice of which slots to surface are yours.\n\nThis is the turn most demos skip entirely, and it's the one that makes the whole thing a negotiation. The other side replies: \"Wednesday at 2 doesn't work, can you do Wednesday morning instead?\" That reply fires another `message.created`\n\non the **same thread**. Your handler looks up the `thread_id`\n\nin whatever state store you're keeping, sees that this conversation is mid-negotiation, and loops back to step 2 with the new constraint (\"Wednesday morning\").\n\nTwo things make this loop survivable in production:\n\n`thread_id`\n\nto your negotiation state — which window you last proposed, how many rounds you've done, whether you're waiting on a reply. Keep it in a durable store, not memory; these conversations span hours or days. The Each round is just steps 2 and 3 again — re-check free/busy against the narrowed window, reply in-thread with a tighter proposal. You converge when one side's reply is an acceptance, which your parser detects the same way it detected the original request: by reading the email.\n\nOnce both sides have said yes to a specific time, *now* you create the event — the single call the other demos lead with. Because the agent is a real calendar participant, creating with `notify_participants=true`\n\nsends a normal ICS invitation from the agent's address. The requester gets it in Gmail or Outlook and clicks Accept like they would for any colleague.\n\n`calendar_id`\n\nis a required query parameter on create; use `primary`\n\nfor the agent's default calendar:\n\n```\ncurl --request POST \\\n  --url \"https://api.us.nylas.com/v3/grants/<GRANT_ID>/events?calendar_id=primary&notify_participants=true\" \\\n  --header \"Authorization: Bearer $NYLAS_API_KEY\" \\\n  --header \"Content-Type: application/json\" \\\n  --data '{\n    \"title\": \"Acme <> Support sync\",\n    \"when\": { \"start_time\": 1750944600, \"end_time\": 1750946400 },\n    \"participants\": [\n      { \"email\": \"alex@acme.com\" }\n    ]\n  }'\n```\n\nThe CLI mirrors it:\n\n```\nnylas calendar events create \\\n  --calendar primary \\\n  --title \"Acme <> Support sync\" \\\n  --start \"2026-06-26 14:00\" \\\n  --end \"2026-06-26 14:30\" \\\n  --participant alex@acme.com\n```\n\nAfter this fires, an `event.created`\n\nwebhook lands on the agent's calendar, and when the requester clicks Accept, their response flows back to the agent's mailbox and `event.updated`\n\nfires with their RSVP status. At that point the negotiation is genuinely done: a real event exists on real calendars, agreed over email, picked from times the agent confirmed it was actually free for.\n\nA nice touch: send one final reply on the thread confirming the booking (\"Sent you an invite for Wednesday at 2 — see you then\"). That closes the conversation in the requester's inbox where it started, instead of leaving the last word as a silent calendar invite.\n\nA few things I'd build in before pointing this at real users:\n\n`thread_id`\n\nso the agent doesn't fire off two competing proposals.`yes`\n\n/`no`\n\n/`maybe`\n\nvia `send-rsvp`\n\nThe shape here is deliberately simple: inbound message → check own free/busy → reply with a proposal → read the counter → loop → create the event. Four Nylas primitives and one piece of reasoning you own. No Scheduler, no booking pages, no availability config — none of which exist for Agent Accounts anyway.\n\n`nylas calendar`\n\nand `nylas email`\n\nflag used above", "url": "https://wpnews.pro/news/let-your-agent-negotiate-meeting-times-over-email", "canonical_source": "https://dev.to/mqasimca/let-your-agent-negotiate-meeting-times-over-email-2pdc", "published_at": "2026-06-25 14:48:45+00:00", "updated_at": "2026-06-25 15:13:33.442357+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "natural-language-processing", "ai-products"], "entities": ["Nylas", "Nylas Agent Account", "Nylas CLI", "Events API", "Scheduler"], "alternates": {"html": "https://wpnews.pro/news/let-your-agent-negotiate-meeting-times-over-email", "markdown": "https://wpnews.pro/news/let-your-agent-negotiate-meeting-times-over-email.md", "text": "https://wpnews.pro/news/let-your-agent-negotiate-meeting-times-over-email.txt", "jsonld": "https://wpnews.pro/news/let-your-agent-negotiate-meeting-times-over-email.jsonld"}}