{
  "name": "SMS Appointment Reminders",
  "nodes": [
    {
      "parameters": {
        "content": "## SMS Appointment Reminders from Google Calendar\n\nNo-shows drop when clients get a text the night before - this reads tomorrow's appointments from your calendar and sends each one an SMS automatically.\n\n**Setup time:** 15 minutes  ·  **Level:** Intermediate\n\n**You'll need accounts/credentials for:** Google Calendar, Twilio\n\n**Replace these before running:**\n- `REPLACE_WITH_YOUR_CALENDAR`\n- `REPLACE_WITH_YOUR_TWILIO_NUMBER`\n\n**Setup steps:**\n1. Connect Google Calendar and select your bookings calendar.\n2. Put each client's mobile number in the event description as 'phone: +15551234567' (or adapt the Code node to your convention).\n3. Connect Twilio with your Account SID and Auth Token, and set your Twilio number as the sender.\n4. Activate the workflow - reminders go out this evening for tomorrow's appointments.\n\nFree templates and updates: navkhan103.github.io",
        "height": 618,
        "width": 400,
        "color": 4
      },
      "id": "sticky-setup",
      "name": "Setup — read me first",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        -40
      ]
    },
    {
      "parameters": {
        "content": "### How it runs\n\nStarts from **Daily 6pm**.\n\n1. A Schedule Trigger runs every day at 18:00.\n2. A Google Calendar node fetches tomorrow's events.\n3. A Code node extracts the client phone number (from the event description or location field) and builds the reminder text with the appointment time.\n4. A Twilio node sends the SMS to each client.",
        "height": 348,
        "width": 400,
        "color": 7
      },
      "id": "sticky-how",
      "name": "How it runs",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        618
      ]
    },
    {
      "parameters": {
        "content": "### If something breaks\n\nThe read and AI nodes here **retry 3× automatically** (5s apart), which absorbs rate limits and brief timeouts.\n\nNodes that **send or write deliberately do not retry** — a send that reached the far end but lost its response would otherwise fire twice, so you'd get duplicate messages or duplicate rows.\n\nTo be told when one of those fails, build a workflow starting with an **Error Trigger** node that posts to Slack or email, then set it under **three-dot menu > Settings > Error Workflow**. Do it once and point every workflow at it.",
        "height": 400,
        "width": 400,
        "color": 3
      },
      "id": "sticky-fail",
      "name": "If something breaks",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        1006
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 18 * * *"
            }
          ]
        }
      },
      "id": "d1e2f3a4-0001-4000-8000-000000000001",
      "name": "Daily 6pm",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "REPLACE_WITH_YOUR_CALENDAR"
        },
        "returnAll": true,
        "options": {
          "timeMin": "={{ $now.plus({ days: 1 }).startOf('day').toISO() }}",
          "timeMax": "={{ $now.plus({ days: 1 }).endOf('day').toISO() }}",
          "singleEvents": true
        }
      },
      "id": "d1e2f3a4-0002-4000-8000-000000000002",
      "name": "Tomorrow's Appointments",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        220,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "const out = [];\nfor (const i of $input.all()) {\n  const e = i.json;\n  const match = `${e.description || ''} ${e.location || ''}`.match(/phone:\\s*(\\+?[0-9][0-9 ()-]{7,})/i);\n  if (!match || !e.start?.dateTime) continue; // no phone found - skip\n  const time = new Date(e.start.dateTime).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });\n  out.push({ json: {\n    to: match[1].replace(/[ ()-]/g, ''),\n    body: `Reminder: your appointment \\\"${e.summary}\\\" is tomorrow at ${time}. Reply CANCEL if you can't make it.`\n  } });\n}\nreturn out;"
      },
      "id": "d1e2f3a4-0003-4000-8000-000000000003",
      "name": "Build Reminder Texts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "from": "REPLACE_WITH_YOUR_TWILIO_NUMBER",
        "to": "={{ $json.to }}",
        "message": "={{ $json.body }}",
        "options": {}
      },
      "id": "d1e2f3a4-0004-4000-8000-000000000004",
      "name": "Send SMS",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        660,
        0
      ]
    }
  ],
  "connections": {
    "Daily 6pm": {
      "main": [
        [
          {
            "node": "Tomorrow's Appointments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tomorrow's Appointments": {
      "main": [
        [
          {
            "node": "Build Reminder Texts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Reminder Texts": {
      "main": [
        [
          {
            "node": "Send SMS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}