cd /news/ai-agents/let-your-agent-negotiate-meeting-tim… · home topics ai-agents article
[ARTICLE · art-39491] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Let your agent negotiate meeting times over email

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.

read9 min views1 publishedJun 25, 2026

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.

I already wrote about giving your agent a calendar — provisioning a real mailbox, hosting events, RSVPing to invites. That post stops at the single create

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

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

One 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/*

. That's documented in 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.

Strip away the AI and a meeting negotiation is a state machine with four moves:

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

The 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

/v3/grants/{grant_id}/...

endpoint. Nothing new to learn — if you've used the Nylas email and calendar APIs, you already know the verbs.You need:

nylas agent account create support@yourcompany.com

or POST /v3/connect/custom

. The grant_id

, and a Nylas API key exported as NYLAS_API_KEY

.message.created

webhook so the agent reacts within seconds of a request arriving, or poll GET /messages

on a cadence for batch flows. Both are supported.New to Agent Accounts? Read What are Agent Accounts first — the rest of this assumes you've got a working account.

All API examples use the US base host https://api.us.nylas.com

and a bearer token. Swap in api.eu.nylas.com

if your app lives in the EU region.

When someone emails the agent — "Can we grab 30 minutes next week?" — that's a message.created

webhook with a thread_id

. The whole negotiation lives on that one thread, so the thread_id

is the key you'll carry through every later step. (See Email threading for agents for why the thread, not the subject line, is the stable identifier.)

Fetch the message body so your code can extract the requested window:

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages?thread_id=<THREAD_ID>&limit=5" \
  --header "Authorization: Bearer $NYLAS_API_KEY"

From the CLI:

nylas email threads show <THREAD_ID>

…and to read the specific message in full:

nylas email read <MESSAGE_ID>

What 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

, end

, plus any preferences). Don't pretend the API parses intent; it hands you text and you decide what it means. Keep the message_id

you're replying to and the thread_id

— you need both in a moment.

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

The endpoint is POST /v3/grants/{grant_id}/calendars/free-busy

. Give it a window and the email addresses to check — the agent's own address checks the agent's own calendar:

curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/calendars/free-busy" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "start_time": 1750766400,
    "end_time": 1751025600,
    "emails": ["support@yourcompany.com"]
  }'

The response is a list of busy blocks (epoch start_time

/end_time

pairs) 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?).

The CLI wraps the same call as nylas calendar availability check

:

nylas calendar availability check \
  --emails support@yourcompany.com \
  --start "next monday 9am" \
  --end "next friday 6pm"

There's also nylas calendar availability find

, 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

(or the API) so your own code controls how slots get picked.

A 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

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

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

Replying 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

, References

) for you when you pass reply_to_message_id

:

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 '{
    "reply_to_message_id": "<INBOUND_MESSAGE_ID>",
    "to": [{ "email": "alex@acme.com" }],
    "subject": "Re: 30 minutes next week",
    "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?"
  }'

The CLI version is shorter because reply

fetches the original message and fills in recipient, subject, and threading automatically:

nylas email reply <INBOUND_MESSAGE_ID> \
  --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?"

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

This 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

on the same thread. Your handler looks up the thread_id

in 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").

Two things make this loop survivable in production:

thread_id

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

Once 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

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

calendar_id

is a required query parameter on create; use primary

for the agent's default calendar:

curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/events?calendar_id=primary&notify_participants=true" \
  --header "Authorization: Bearer $NYLAS_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "title": "Acme <> Support sync",
    "when": { "start_time": 1750944600, "end_time": 1750946400 },
    "participants": [
      { "email": "alex@acme.com" }
    ]
  }'

The CLI mirrors it:

nylas calendar events create \
  --calendar primary \
  --title "Acme <> Support sync" \
  --start "2026-06-26 14:00" \
  --end "2026-06-26 14:30" \
  --participant alex@acme.com

After this fires, an event.created

webhook lands on the agent's calendar, and when the requester clicks Accept, their response flows back to the agent's mailbox and event.updated

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

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

A few things I'd build in before pointing this at real users:

thread_id

so the agent doesn't fire off two competing proposals.yes

/no

/maybe

via send-rsvp

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

nylas calendar

and nylas email

flag used above

── more in #ai-agents 4 stories · sorted by recency
── more on @nylas 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/let-your-agent-negot…] indexed:0 read:9min 2026-06-25 ·