No-shows are expensive.
A missed appointment is lost revenue, a wasted time slot, and sometimes a client you never get back. For service businesses â consultants, coaches, tutors, salons, clinics â no-shows can cost 10-20% of weekly revenue.
The fix is embarrassingly simple: send a reminder the day before and one hour before. Most no-shows are forgotten appointments, not intentional cancellations.
Here's a 5-node n8n workflow that reads your Google Calendar every hour and sends automatic email reminders â 24 hours out and 1 hour out â to every attendee. Set it up once, forget about it.
What the workflow does
Node 1 â Hourly schedule trigger
Runs every hour (you can change this to every 30 minutes for higher precision). This is the heartbeat of the whole workflow.
Node 2 â Google Calendar: fetch upcoming events
Pulls all calendar events starting from right now through the next 25 hours. The 25-hour window ensures we catch both the "24 hours out" and "1 hour out" reminders in a single pass.
Node 3 â Filter for reminder windows
A Code node checks each event's start time against the current time:
- If the event starts in 23.5 to 24.5 hours â flag as
24hourreminder - If the event starts in 0.9 to 1.1 hours â flag as
1hourreminder - All other events â return
type: 'none'
The 0.1-hour tolerance on each side accounts for the cron not firing at the exact second.
Node 4 â IF: has reminders?
Filters out the type: 'none' events so we only continue if there's actually a reminder to send.
Node 5 â Gmail: send reminder
Sends a personalized email to all attendees with the event name, time, location, and Google Meet link (if present). Subject line automatically says "in 1 hour" or "tomorrow" based on which window triggered.
Setup (5 minutes)
- Import the JSON into n8n (New Workflow, Import from clipboard)
- Connect your Google account in both the Calendar and Gmail nodes (one OAuth connection covers both)
- Test â manually trigger Node 1 to verify it reads your calendar
- Activate the workflow â it runs automatically every hour from now on
That's it. No extra tools, no third-party services, no monthly fees.
Full workflow JSON
{
"name": "Appointment Reminder",
"nodes": [
{"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 * * * *"}]}},"id":"ar1","name":"Check Every Hour","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[240,300]},
{"parameters":{"operation":"getAll","calendar":{"__rl":true,"value":"primary","mode":"name"},"returnAll":false,"limit":20,"options":{"timeMin":"={{ $now.toISO() }}","timeMax":"={{ $now.plus({hours: 25}).toISO() }}"}},"id":"ar2","name":"Get Upcoming Events","type":"n8n-nodes-base.googleCalendar","typeVersion":3,"position":[460,300]},
{"parameters":{"jsCode":"const events = $input.all();\nconst now = new Date();\nconst reminders = [];\nfor (const event of events) {\n const e = event.json;\n const start = new Date(e.start?.dateTime || e.start?.date);\n const hoursUntil = (start - now) / (1000 * 60 * 60);\n if (hoursUntil > 0.9 && hoursUntil < 1.1) {\n reminders.push({ json: { type: '1hour', summary: e.summary, start: start.toISOString(), attendees: (e.attendees || []).map(a => a.email).join(', '), location: e.location || 'No location', meetLink: e.hangoutLink || '' } });\n }\n if (hoursUntil > 23.5 && hoursUntil < 24.5) {\n reminders.push({ json: { type: '24hour', summary: e.summary, start: start.toISOString(), attendees: (e.attendees || []).map(a => a.email).join(', '), location: e.location || 'No location', meetLink: e.hangoutLink || '' } });\n }\n}\nreturn reminders.length > 0 ? reminders : [{ json: { type: 'none' } }];"},"id":"ar3","name":"Filter Reminder Times","type":"n8n-nodes-base.code","typeVersion":2,"position":[680,300]},
{"parameters":{"conditions":{"conditions":[{"leftValue":"={{ $json.type }}","rightValue":"none","operator":{"type":"string","operation":"notEquals"}}]}},"id":"ar4","name":"Has Reminders?","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[900,300]},
{"parameters":{"sendTo":"={{ $json.attendees }}","subject":"=Reminder: {{ $json.summary }} â {{ $json.type === '1hour' ? 'in 1 hour' : 'tomorrow' }}","message":"=Hi,\n\nThis is a friendly reminder about your upcoming appointment:\n\nEvent: {{ $json.summary }}\nWhen: {{ $json.start }}\nLocation: {{ $json.location }}\n{{ $json.meetLink ? 'Join link: ' + $json.meetLink : '' }}\n\nSee you there!"},"id":"ar5","name":"Send Reminder Email","type":"n8n-nodes-base.gmail","typeVersion":2.1,"position":[1140,300]}
],
"connections": {
"Check Every Hour":{"main":[[{"node":"Get Upcoming Events","type":"main","index":0}]]},
"Get Upcoming Events":{"main":[[{"node":"Filter Reminder Times","type":"main","index":0}]]},
"Filter Reminder Times":{"main":[[{"node":"Has Reminders?","type":"main","index":0}]]},
"Has Reminders?":{"main":[[{"node":"Send Reminder Email","type":"main","index":0}],[]]}
},
"settings":{"executionOrder":"v1"},
"tags":[{"name":"scheduling"}]
}
Customizations
Add SMS via Twilio
Replace or supplement the Gmail node with an HTTP Request to the Twilio API. SMS reminders have a higher open rate than email for last-minute reminders.
Add a confirmation link
Modify the email template to include a link like https://calendly.com/yourname/confirm?event=XYZ. Clients click to confirm â you know in advance who's coming.
Different reminder windows
Want 48h + 2h instead of 24h + 1h? Just change the hour ranges in the Code node (lines 9-15). No other changes needed.
Use a Sheets-based appointment list instead of Google Calendar
Replace Node 2 with a Google Sheets node that reads your bookings spreadsheet. Filter by date in Node 3. Useful if you book outside of Google Calendar.
Slack notification when a reminder fires
Add a Slack node in parallel with the Gmail node. You see in real-time who got reminded â useful for high-value clients.
Skip reminders for all-day events
In the Code node, add a check: if (!e.start?.dateTime) continue; before the start and hoursUntil calculation. All-day events use e.start.date (no time) and shouldn't trigger reminders.
Real-world impact
A freelance consultant running 8-10 client calls per week, with one no-show per week at $150/call, saves $600/month by eliminating that one no-show. The workflow takes 5 minutes to set up.
For a salon with 40 appointments per week at 5% no-show rate = 2 missed slots = $100-200 lost per week. Automated reminders typically cut no-shows by 50-80% for appointment-based businesses.
Get the full automation bundle
This workflow is part of our 15-template n8n automation bundle â each one covering a different business use case: lead capture, invoice generation, AI customer support, social media automation, price monitoring, and more.
Grab the full bundle at stripeai.gumroad.com â pre-tested, documented, ready to activate.
Built with n8n. Self-hostable, open source, no vendor lock-in.
Top comments (0)