AutomationFlowsEmail & Gmail › Send Appointment Confirmations, Reminders and Review Requests with Google…

Send Appointment Confirmations, Reminders and Review Requests with Google…

Original n8n title: Send Appointment Confirmations, Reminders and Review Requests with Google Sheets and Gmail

ByManik Chandra Dhor @youthfireit on n8n.io

This workflow runs daily at 8 AM to read appointment records from Google Sheets, send the right Gmail email (confirmation, reminder, no-show win-back, or post-visit review request) based on each appointment’s status and date, and then update the appointment status back in the…

Cron / scheduled trigger★★★★☆ complexity15 nodesGoogle SheetsGmail
Email & Gmail Trigger: Cron / scheduled Nodes: 15 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17118 — we link there as the canonical source.

This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "Automate appointment confirmations, reminders and review requests with Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "trg",
      "name": "Every Day at 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        60
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "cfg",
      "name": "Configuration",
      "type": "n8n-nodes-base.code",
      "position": [
        300,
        150
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 Edit your business details, then click Test workflow \u2500\u2500\nreturn [{ json: {\n  businessName: 'Bright Smile Dental',            // shown in every message\n  bookingUrl:   'https://YOUR-BOOKING-LINK',      // where clients rebook\n  reviewUrl:    'https://YOUR-GOOGLE-REVIEW-LINK', // your Google review link\n  reminderDaysBefore: 1                            // send reminder N days before\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "get",
      "name": "Get Appointments",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        560,
        150
      ],
      "parameters": {
        "options": {},
        "resource": "sheet",
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Appointments"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "cls",
      "name": "Classify Appointments",
      "type": "n8n-nodes-base.code",
      "position": [
        820,
        150
      ],
      "parameters": {
        "jsCode": "// Decide what to do with each appointment row.\nconst cfg = $('Configuration').first().json;\nconst today = new Date(); today.setHours(0,0,0,0);\nconst todayStr = today.toISOString().slice(0,10);\nconst out = [];\nfor (const row of $input.all()) {\n  const j = row.json;\n  const status = String(j.Status || '').trim().toLowerCase();\n  const dateStr = String(j.AppointmentDate || '').slice(0, 10);\n  if (!j.Email || !dateStr) continue;\n  const appt = new Date(dateStr + 'T00:00:00');\n  const days = Math.round((appt - today) / 86400000);\n  let action = 'skip', newStatus = status;\n  if (status === 'new')                                     { action='confirm';  newStatus='confirmed'; }\n  else if (status === 'confirmed' && days === cfg.reminderDaysBefore) { action='remind';   newStatus='reminded'; }\n  else if (status === 'no-show')                            { action='winback';  newStatus='winback-sent'; }\n  else if (status === 'completed' && days === -1)           { action='review';   newStatus='review-requested'; }\n  if (action === 'skip') continue;\n  out.push({ json: {\n    AppointmentID: j.AppointmentID, Name: j.Name || 'there', Email: j.Email,\n    AppointmentDate: j.AppointmentDate, action, newStatus, todayStr\n  }});\n}\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "sw",
      "name": "Route by Lifecycle Stage",
      "type": "n8n-nodes-base.switch",
      "position": [
        1080,
        150
      ],
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "outputKey": "confirm",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "confirm"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "remind",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "remind"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "winback",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "winback"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "review",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "review"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "g1",
      "name": "Send Confirmation",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        1400,
        0
      ],
      "parameters": {
        "sendTo": "={{ $json.Email }}",
        "message": "=<p>Hi {{ $json.Name }},</p><p>Your appointment with <b>{{ $('Configuration').first().json.businessName }}</b> on <b>{{ $json.AppointmentDate }}</b> is confirmed. We look forward to seeing you.</p><p>Need to reschedule? <a href=\"{{ $('Configuration').first().json.bookingUrl }}\">Manage your booking here</a>.</p>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=Your appointment with {{ $('Configuration').first().json.businessName }} is confirmed",
        "resource": "message",
        "emailType": "html",
        "operation": "send"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "g2",
      "name": "Send Reminder",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        1400,
        160
      ],
      "parameters": {
        "sendTo": "={{ $json.Email }}",
        "message": "=<p>Hi {{ $json.Name }},</p><p>Just a friendly reminder of your appointment with <b>{{ $('Configuration').first().json.businessName }}</b> on <b>{{ $json.AppointmentDate }}</b>.</p><p>Can't make it? Please <a href=\"{{ $('Configuration').first().json.bookingUrl }}\">reschedule here</a> so we can offer the slot to someone else.</p>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=Reminder: your appointment with {{ $('Configuration').first().json.businessName }} is tomorrow",
        "resource": "message",
        "emailType": "html",
        "operation": "send"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "g3",
      "name": "Send No-Show Win-Back",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        1400,
        320
      ],
      "parameters": {
        "sendTo": "={{ $json.Email }}",
        "message": "=<p>Hi {{ $json.Name }},</p><p>We're sorry we missed you on <b>{{ $json.AppointmentDate }}</b>. These things happen!</p><p>We'd love to get you rebooked \u2014 <a href=\"{{ $('Configuration').first().json.bookingUrl }}\">pick a new time here</a>.</p><p>\u2014 The team at {{ $('Configuration').first().json.businessName }}</p>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=We missed you at {{ $('Configuration').first().json.businessName }}",
        "resource": "message",
        "emailType": "html",
        "operation": "send"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "g4",
      "name": "Send Review Request",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        1400,
        480
      ],
      "parameters": {
        "sendTo": "={{ $json.Email }}",
        "message": "=<p>Hi {{ $json.Name }},</p><p>Thanks for visiting <b>{{ $('Configuration').first().json.businessName }}</b>. We hope you had a great experience.</p><p>If you have a moment, a quick review really helps us: <a href=\"{{ $('Configuration').first().json.reviewUrl }}\">leave a review here</a>.</p><p>Thank you!</p>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=How was your visit to {{ $('Configuration').first().json.businessName }}?",
        "resource": "message",
        "emailType": "html",
        "operation": "send"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "waitBetweenTries": 2000
    },
    {
      "id": "upd",
      "name": "Update Appointment Status",
      "type": "n8n-nodes-base.googleSheets",
      "maxTries": 3,
      "position": [
        1740,
        240
      ],
      "parameters": {
        "columns": {
          "value": {
            "Status": "={{ $json.newStatus }}",
            "AppointmentID": "={{ $json.AppointmentID }}",
            "LastContacted": "={{ $json.todayStr }}"
          },
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "AppointmentID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "resource": "sheet",
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Appointments"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.7,
      "waitBetweenTries": 2000
    },
    {
      "id": "man",
      "name": "Test Manually",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        0,
        240
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "sec0",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        -430
      ],
      "parameters": {
        "color": "color4",
        "width": 1260,
        "height": 390,
        "content": "## 1. Read & classify appointments\n\nThe workflow starts on a daily schedule (or manually, for testing). It loads every row from your Google Sheet, then the Classify step tags each appointment with its lifecycle stage \u2014 new, reminder-due, no-show or completed \u2014 and the Switch routes each one to the right message on the right.\n\nThe **Configuration** node is where you set your business name, reminder timing and review link, so you rarely need to touch anything else."
      },
      "typeVersion": 1
    },
    {
      "id": "sec1",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        -690
      ],
      "parameters": {
        "color": "color5",
        "width": 280,
        "height": 650,
        "content": "## 2. Send the right message\n\nEach lifecycle stage gets its own Gmail message:\n\n- **Confirmation** \u2014 sent when a booking is first added\n- **Reminder** \u2014 sent before the appointment to cut no-shows\n- **No-show win-back** \u2014 a gentle nudge to rebook after a miss\n- **Review request** \u2014 sent after a completed visit to collect 5-star reviews\n\nEdit the subject and body in each Gmail node to match your brand voice."
      },
      "typeVersion": 1
    },
    {
      "id": "sec2",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1720,
        -380
      ],
      "parameters": {
        "color": "color3",
        "width": 280,
        "height": 340,
        "content": "## 3. Update the record\n\nAfter the right email is sent, the appointment's row in your Google Sheet is updated with its new status and a timestamp.\n\nThis is what stops the same customer from being emailed twice, and keeps your sheet as the single source of truth for every appointment."
      },
      "typeVersion": 1
    },
    {
      "id": "main",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -880,
        -40
      ],
      "parameters": {
        "width": 780,
        "height": 650,
        "content": "## Send appointment confirmations, reminders and review requests with Google Sheets and Gmail\n\nRun your entire appointment lifecycle on autopilot \u2014 confirmations, reminders, no-show win-backs and review requests \u2014 straight from a Google Sheet using Gmail. Built for clinics, salons, spas, agencies and any business that books appointments.\n\n### How it works\n1. Every morning the workflow reads your appointments from a Google Sheet\n2. Each appointment is classified by its lifecycle stage (new booking, upcoming, no-show, or completed)\n3. The Switch routes it to the right email \u2014 a confirmation, a reminder, a win-back, or a review request\n4. Gmail sends the matching message\n5. The sheet is updated so nobody is contacted twice\n\n### Setup steps\n1. Copy the Google Sheet and add your appointments (name, email, date, status)\n2. Connect your Google Sheets and Gmail credentials\n3. Open the Configuration node and set your business name, timings and message text\n4. Activate \u2014 it runs daily at 8 AM (change this in the Schedule Trigger)\n\n### Requirements\n- Google account (Sheets + Gmail)\n- A Google Sheet of appointments with a Status column\n\n### Customising this workflow\n- Edit the copy in each Gmail node to match your brand voice\n- Add SMS (Twilio) alongside email for reminders\n- Change the schedule, or add more stages in the Classify and Switch nodes"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Configuration": {
      "main": [
        [
          {
            "node": "Get Appointments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reminder": {
      "main": [
        [
          {
            "node": "Update Appointment Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test Manually": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Appointments": {
      "main": [
        [
          {
            "node": "Classify Appointments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Day at 8 AM": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Confirmation": {
      "main": [
        [
          {
            "node": "Update Appointment Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Review Request": {
      "main": [
        [
          {
            "node": "Update Appointment Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Appointments": {
      "main": [
        [
          {
            "node": "Route by Lifecycle Stage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send No-Show Win-Back": {
      "main": [
        [
          {
            "node": "Update Appointment Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Lifecycle Stage": {
      "main": [
        [
          {
            "node": "Send Confirmation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send No-Show Win-Back",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Review Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs daily at 8 AM to read appointment records from Google Sheets, send the right Gmail email (confirmation, reminder, no-show win-back, or post-visit review request) based on each appointment’s status and date, and then update the appointment status back in the…

Source: https://n8n.io/workflows/17118/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

This workflow runs every 3 hours to read appointments from Google Sheets and send confirmation, reminder, win-back, and review-request messages via Gmail and/or Twilio SMS, then updates the sheet and

Google Sheets, Gmail, Twilio +1
Email & Gmail

Tags: Supply Chain Management, Logistics, Transportation

Telegram Trigger, Telegram, Google Drive +2
Email & Gmail

This workflow implements a lightweight authentication system for n8n web portals using Google Sheets as a simple user and session store. It supports email-based registration, username and password log

Google Sheets, Form Trigger, Gmail +1
Email & Gmail

OTP Verification System. Uses googleSheets, gmail. Webhook trigger; 23 nodes.

Google Sheets, Gmail
Email & Gmail

Transform your visitor management process with this fully automated, enterprise-grade workflow. The Verified Visitor Pass Generator eliminates manual data entry, prevents fake registrations through em

N8N Nodes Htmlcsstoimage, Gmail, Slack +3