Sharing another one for service businesses. No-shows cost more than missed calls because you
already blocked the time and drove to the job.
The problem is usually just timing: customers forgot to cancel, not that they wanted to bail.
A reminder 24 hours out and again 2 hours before clears most of those.
**What it does:**
Runs on a 15-minute Schedule Trigger. Reads every appointment from a Google Sheet, calculates
hours until each one, and sends the right reminder at the right window: an email the day before
and an SMS two hours out. A flag written back to the sheet after each send means the flow can
run all day without double-texting anyone.
**Nodes (11 logic nodes):**
- Schedule Trigger (every 15 min, cron: `*/15 * * * *`)
- Google Sheets – read Appointments tab
- Set: Calculate hours until appointment (`Math.round((new Date($json.appointment_datetime).getTime() - Date.now()) / 3600000)`)
- IF: Is 24h window? (hours >= 23 AND hours <= 26 AND reminder_24h not set)
- IF: Has email address? (guard against empty rows)
- Gmail – 24h reminder email
- Google Sheets – write `reminder_24h = Yes` (dedup flag)
- IF: Is 2h window? (hours >= 1 AND hours <= 3 AND reminder_2h not set)
- IF: Has phone number? (guard against empty rows)
- Twilio SMS – 2h reminder text
- Google Sheets – write `reminder_2h = Yes` (dedup flag)
**The dedup pattern** (nodes 7 + 11): writing the flag back to the sheet before the window
closes prevents double-sends even if n8n restarts or runs overlap. This is the part that
bites people – if you skip the flag, a 15-min poll can fire 3-4 messages in the same window.
**Sheet columns needed:**
```
row_id | appointment_datetime (ISO 8601 UTC) | customer_name | customer_phone |
customer_email | service_type | reminder_24h | reminder_2h
```
**Setup time: about 20 minutes** (Google Sheets OAuth, Gmail OAuth, Twilio creds, your sheet ID)
```json
{
“name”: “Appointment Reminder System”,
“nodes”: [
{
“name”: “Every 15 Minutes”,
“type”: “n8n-nodes-base.scheduleTrigger”,
“parameters”: {
“rule”: { “interval”: [{ “field”: “cronExpression”, “expression”: “*/15 * * * *” }] }
}
},
{
“name”: “Read Appointments”,
“type”: “n8n-nodes-base.googleSheets”,
“parameters”: {
“operation”: “read”,
“documentId”: { “__rl”: true, “value”: “YOUR_GOOGLE_SHEET_ID”, “mode”: “id” },
“sheetName”: { “__rl”: true, “value”: “Appointments”, “mode”: “name” }
}
},
{
“name”: “Calculate Time Until Appointment”,
“type”: “n8n-nodes-base.set”,
“parameters”: {
“assignments”: { “assignments”: [
{ “name”: “hours_until_appointment”,
“value”: “={{ Math.round((new Date($json.appointment_datetime).getTime() - Date.now()) / 3600000) }}”,
“type”: “number” },
{ “name”: “reminder_24h”, “value”: “={{ $json.reminder_24h || ‘’ }}”, “type”: “string” },
{ “name”: “reminder_2h”, “value”: “={{ $json.reminder_2h || ‘’ }}”, “type”: “string” },
{ “name”: “customer_email”, “value”: “={{ $json.customer_email || ‘’ }}”, “type”: “string” },
{ “name”: “customer_phone”, “value”: “={{ $json.customer_phone || ‘’ }}”, “type”: “string” },
{ “name”: “customer_name”, “value”: “={{ $json.customer_name || ‘’ }}”, “type”: “string” },
{ “name”: “service_type”, “value”: “={{ $json.service_type || ‘appointment’ }}”, “type”: “string” },
{ “name”: “row_id”, “value”: “={{ $json.row_id || ‘’ }}”, “type”: “string” }
]}
}
},
{
“name”: “Send 24h Reminder?”,
“type”: “n8n-nodes-base.if”,
“parameters”: {
“conditions”: {
“combinator”: “and”,
“conditions”: [
{ “leftValue”: “={{ $json.hours_until_appointment }}”, “rightValue”: 23, “operator”: { “type”: “number”, “operation”: “gte” }},
{ “leftValue”: “={{ $json.hours_until_appointment }}”, “rightValue”: 26, “operator”: { “type”: “number”, “operation”: “lte” }},
{ “leftValue”: “={{ $json.reminder_24h }}”, “rightValue”: “”, “operator”: { “type”: “string”, “operation”: “equals” }}
]
}
}
},
{
“name”: “Has Email?”,
“type”: “n8n-nodes-base.if”,
“parameters”: {
“conditions”: {
“conditions”: [
{ “leftValue”: “={{ $json.customer_email }}”, “operator”: { “type”: “string”, “operation”: “notEmpty” }}
]
}
}
},
{
“name”: “Email - 24h Reminder”,
“type”: “n8n-nodes-base.gmail”,
“parameters”: {
“sendTo”: “={{ $json.customer_email }}”,
“subject”: “Reminder: your {{ $json.service_type }} appointment is tomorrow”,
“emailType”: “text”,
“message”: “Hi {{ $json.customer_name }},\n\nJust a reminder that your {{ $json.service_type }} appointment is scheduled for tomorrow. Reply to this email or call us if you need to reschedule.\n\nSee you then.\nYOUR_BUSINESS_NAME”
}
},
{
“name”: “Update - 24h Sent”,
“type”: “n8n-nodes-base.googleSheets”,
“parameters”: {
“operation”: “update”,
“documentId”: { “__rl”: true, “value”: “YOUR_GOOGLE_SHEET_ID”, “mode”: “id” },
“sheetName”: { “__rl”: true, “value”: “Appointments”, “mode”: “name” },
“columns”: {
“mappingMode”: “defineBelow”,
“value”: { “row_id”: “={{ $json.row_id }}”, “reminder_24h”: “Yes” },
“matchingColumns”: [“row_id”]
}
}
},
{
“name”: “Send 2h Reminder?”,
“type”: “n8n-nodes-base.if”,
“parameters”: {
“conditions”: {
“combinator”: “and”,
“conditions”: [
{ “leftValue”: “={{ $json.hours_until_appointment }}”, “rightValue”: 1, “operator”: { “type”: “number”, “operation”: “gte” }},
{ “leftValue”: “={{ $json.hours_until_appointment }}”, “rightValue”: 3, “operator”: { “type”: “number”, “operation”: “lte” }},
{ “leftValue”: “={{ $json.reminder_2h }}”, “rightValue”: “”, “operator”: { “type”: “string”, “operation”: “equals” }}
]
}
}
},
{
“name”: “Has Phone?”,
“type”: “n8n-nodes-base.if”,
“parameters”: {
“conditions”: {
“conditions”: [
{ “leftValue”: “={{ $json.customer_phone }}”, “operator”: { “type”: “string”, “operation”: “notEmpty” }}
]
}
}
},
{
“name”: “SMS - 2h Reminder”,
“type”: “n8n-nodes-base.twilio”,
“parameters”: {
“from”: “YOUR_TWILIO_NUMBER”,
“to”: “={{ $json.customer_phone }}”,
“message”: “Reminder: your {{ $json.service_type }} with YOUR_BUSINESS_NAME is in about 2 hours. Reply STOP to unsubscribe.”
}
},
{
“name”: “Update - 2h Sent”,
“type”: “n8n-nodes-base.googleSheets”,
“parameters”: {
“operation”: “update”,
“documentId”: { “__rl”: true, “value”: “YOUR_GOOGLE_SHEET_ID”, “mode”: “id” },
“sheetName”: { “__rl”: true, “value”: “Appointments”, “mode”: “name” },
“columns”: {
“mappingMode”: “defineBelow”,
“value”: { “row_id”: “={{ $json.row_id }}”, “reminder_2h”: “Yes” },
“matchingColumns”: [“row_id”]
}
}
}
]
}
```
Full package (with the exact sheet column layout, connection JSON, and setup notes for Google
OAuth and Twilio) is at: whop.com/cadence-a678/appointment-reminder-system/
Happy to answer questions about adapting the timing windows or swapping Gmail for another
email provider (SendGrid, Mailgun, Postmark all have n8n nodes).