You booked a meeting on a customer's calendar, and you want them to get a reminder email the day before so they actually show up. The obvious build is a cron job that wakes up every few minutes, scans for appointments coming up in 24 hours, and fires an email. That's a polling loop you have to run, scale, and debug. There's a cleaner way that uses three Nylas features together: you read the appointment from the calendar, schedule the reminder email for exactly 24 hours before it, and let a template carry the copy. No loop, no worker watching the clock.
This post is a worked use case rather than a feature tour. It pulls together the Calendar, scheduled send, and templates APIs from two angles: the HTTP API for your backend and the nylas CLI for the terminal. I work on the CLI, so the terminal commands below are the ones I reach for when I'm prototyping the flow.
The shape of the solution
The reminder flow is three steps, each handled by a different piece. The calendar tells you when the appointment is. Scheduled send fires the email at a time you compute from that, with nothing running in between. And a template holds the reminder copy so it's editable without a deploy. Wire them together and an appointment booked today produces a reminder that goes out automatically tomorrow.
This same need shows up everywhere a booking exists: a clinic confirming appointments, a sales team reminding prospects about a demo, a service business trying to cut no-shows. The domain changes the copy, not the mechanism, so the implementation below is the same whether you're reminding patients or prospects.
The key insight is that you don't poll. Instead of a worker that repeatedly asks "is anything due soon?", you compute the send time once, when the appointment is created or found, and hand it to scheduled send. The reminder then sits queued until its moment arrives. That inverts the usual reminder architecture: the work happens at scheduling time, not at delivery time, so there's no always-on loop to operate.
Step 1: find the appointments
You need the appointments and their start times, which come from the Events API. A GET /v3/grants/{grant_id}/events call with a calendar_id and a start/end window returns the events in that range, each with a when object carrying the start_time as a Unix timestamp. That timestamp is what you base the reminder on.
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/events?calendar_id=primary&start=1744700000&end=1744900000" \
--header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode
From the terminal, nylas calendar events list shows the upcoming events on a calendar so you can eyeball what you're working with. There are two ways to drive this in production: pull events on a schedule for a rolling window, or react to the event.created webhook so the reminder is scheduled the instant a booking happens. The webhook path is the one I prefer, since it means a reminder exists the moment the appointment does, with no scan at all.
Two query parameters matter for reminders specifically. Pass expand_recurring=true so a weekly standing meeting yields each instance with its own start_time, rather than a single recurrence rule you'd have to expand yourself. And set show_cancelled=false so you never schedule a reminder for a meeting that's already off the calendar.
Step 2: compute the reminder time
This step is plain arithmetic, and it's where the "24 hours before" lives. The event's start_time is a Unix timestamp in seconds, so a reminder one day ahead is start_time - 86400, and one hour ahead is start_time - 3600. You compute that target once and pass it to the send as send_at.
const reminderAt = event.when.start_time - 24 * 60 * 60; // 24h before
Enter fullscreen mode Exit fullscreen mode
There's one real constraint to respect here. A Nylas-stored scheduled send accepts a send_at up to 30 days in the future, so for an appointment booked more than a month out, the reminder time, the appointment minus a day, still falls outside that 30-day window. The clean pattern for far-future bookings is a thin daily job that schedules tomorrow's reminders, which keeps every send_at comfortably inside the 30-day ceiling, while last-minute bookings schedule their reminder right away.
Step 3: schedule the templated reminder
Now you send the reminder, dated into the future and filled from a template. You create a reminder template once with {{name}} and {{time}} placeholders, then each send references it by ID and supplies the variables, while send_at carries the time you computed. The message waits in the queue and goes out on its own at the reminder time.
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": "[email protected]" }],
"template": {
"id": "<REMINDER_TEMPLATE_ID>",
"variables": { "name": "Alex", "time": "tomorrow at 10am" }
},
"send_at": 1744714800
}'
Enter fullscreen mode Exit fullscreen mode
The whole flow collapses into a single CLI command, because nylas email send carries both the schedule and the template. You pass --schedule with the reminder time, --template-id with your reminder template, and --template-data with the variables, and the templated reminder is queued in one line:
nylas email send \
--to [email protected] \
--template-id <REMINDER_TEMPLATE_ID> \
--template-data '{ "name": "Alex", "time": "tomorrow at 10am" }' \
--schedule "2025-04-14 10:00"
Enter fullscreen mode Exit fullscreen mode
The response includes a schedule_id. Store it against the appointment, because that's your handle if the meeting changes.
Because the reminder sends from the connected mailbox, the customer sees a normal email from the business they booked with, not a generic no-reply address. That matters for a reminder specifically: it lands in the same thread as the original confirmation, looks like it came from a person, and is far more likely to be read than a transactional blast from a noreply domain.
Put it in a webhook handler
Here's the create path as a single handler. When event.created fires, you compute the reminder time, schedule the templated send through a small helper that wraps POST /messages/send, and store the returned schedule_id against the event so you can manage it later:
app.post("/webhooks/nylas", async (req, res) => {
res.sendStatus(200); // acknowledge fast
const evt = req.body.data.object;
// Only timed events: all-day events carry when.start_date, not start_time.
if (evt.object !== "event" || !evt.when.start_time) return;
const now = Math.floor(Date.now() / 1000);
const reminderAt = evt.when.start_time - 24 * 60 * 60;
// Too far out for the 30-day window: let a daily job pick it up later.
if (reminderAt - now > 30 * 24 * 60 * 60) return;
// Already due (e.g. a last-minute booking): send now by omitting send_at.
const sendAt = reminderAt <= now ? null : reminderAt;
const scheduleId = await scheduleReminder({
to: attendeeEmail(evt),
templateId: REMINDER_TEMPLATE_ID,
variables: { name: attendeeName(evt) },
sendAt, // null = send immediately
});
await saveReminder(evt.id, scheduleId); // for later cancel/reschedule
});
Enter fullscreen mode Exit fullscreen mode
That's the whole create path with its guards in place: skip all-day events, skip bookings too far out for the daily job to handle, send immediately when the reminder is already due, and otherwise schedule it and store the schedule_id. Everything else, the reschedule and cancel logic below, hangs off the mapping you save on that last line.
Handle reschedules and cancellations
Appointments move, and a reminder for a meeting that was cancelled or rescheduled is worse than none. This is where storing the schedule_id pays off. When the event.updated or event.deleted webhook fires for an appointment, you act on the reminder you queued for it.
For a cancellation, you cancel the scheduled send with a DELETE against its schedule_id, as long as you're more than 10 seconds before the send time. For a reschedule, you cancel the old reminder and schedule a new one for the new start_time - 86400. The webhook gives you the changed event, your stored mapping gives you the schedule_id to cancel, and the new time gives you the next reminder. That closed loop, schedule on create, re-schedule on change, cancel on delete, is what makes the reminders track reality instead of drifting from it.
One edge is worth handling explicitly: if a meeting is rescheduled to within the next 24 hours, the new reminder time lands in the past or only moments from now. There, you skip scheduling and send the reminder immediately. A quick check on the computed time, send now if it's already due or too soon to queue, covers the last-minute reschedule cleanly.
Don't send two reminders
A reminder system's worst failure is sending the same person two reminders, and there are two ways it happens. Webhooks can be redelivered, so the same event.created may fire twice, and a reschedule you handle by cancel-and-recreate can race with itself. Both are solved by keying your reminder record on the event ID: before scheduling, check whether a reminder already exists for that event, and if it does, update it instead of adding a second.
The mapping you store, event ID to schedule_id, is the dedup key. On event.created, schedule only if there's no reminder for that event yet. On event.updated, look up the existing schedule_id, cancel it, and schedule the replacement, all keyed on the same event ID. That single record per appointment is what guarantees one reminder per meeting no matter how many times the webhooks fire, which at any real volume they will.
The same pattern, other reminders
Once the three pieces click together, the same shape covers a family of timed messages, not just a 24-hour reminder. The only thing that changes is the offset you apply to the event time and the template you attach. A few that drop straight in:
-
A prep email a week out. Offset
start_time - 7 * 86400with a template that asks the customer to gather documents before the meeting. - A morning-of nudge. Offset to the morning of the appointment with a short "see you today" template.
-
A post-meeting follow-up. A positive offset,
start_time + 3600, with a template that sends a recap or a feedback link an hour after the meeting ends.
Each is the same compute-the-time, schedule-with-a-template move, so a single helper that takes an event, an offset, and a template ID generates any of them. That's the payoff of building the reminder out of composable pieces rather than a bespoke cron job: a new reminder type is a new offset and a new template, not new infrastructure.
Things to keep in mind
A short list of details makes this flow reliable in production.
-
Schedule on the webhook, not a poll. React to
event.createdso a reminder exists the moment the appointment does, with no scanning loop to run. -
Store the
schedule_idwith the appointment. It's the only handle for cancelling or rescheduling the reminder when the meeting changes. -
Mind the 30-day scheduled-send ceiling. When the computed reminder time is more than 30 days out, a thin daily job that queues tomorrow's reminders keeps every
send_atinside the window. - Respect the 10-second cancel cutoff. A reminder can't be reliably cancelled in the final seconds before it sends, so gate any last-moment changes.
- Keep the copy in a template. Editing the reminder wording is a template update, not a deploy, and one template serves every appointment.
- Dedup on the event ID. Key your reminder record on the event so a redelivered webhook updates the existing reminder instead of queuing a second.
Wrapping up
An appointment reminder doesn't need a cron job; it needs three Nylas calls wired together. Read the appointment's start_time from the Events API, subtract your offset to get the reminder time, and schedule a templated email for that moment with send_at. Store the schedule_id so you can cancel or re-schedule when the meeting moves, and the same pattern, with a different offset and template, gives you prep emails, morning-of nudges, and post-meeting follow-ups for free.
Where to go next:
- Using the Events API — reading and filtering calendar events
-
Schedule messages to send in the future — the
send_atfield and cancellation - Templates and workflows — reusable copy with variables
- Event webhooks — react to bookings and changes
AI-answer pages for agents
When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:
- Topic runbook: https://cli.nylas.com/ai-answers/appointment-scheduling-api-confirmations-reminders.md
- Industry playbooks hub: https://cli.nylas.com/ai-answers/agent-account-industry-playbooks.md
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.