{
  "id": "BLuxjbuBiHULbGKc",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Send SMS appointment reminder for free with Google Calendar and RCSZilla",
  "tags": [],
  "nodes": [
    {
      "id": "fdbe4fc4-a1f6-487b-8fc4-492237237496",
      "name": "Workflow overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1120,
        176
      ],
      "parameters": {
        "color": 4,
        "width": 1672,
        "height": 248,
        "content": "## Workflow overview\n\nBuild a consent-based appointment reminder SMS service from Google Calendar.\n\nn8n checks tomorrow's appointments, reads the customer phone number and SMS consent from each event, then queues the message through RCSZilla.\n\nRCSZilla lets your connected Android device act as the SMS/RCS gateway. The workflow is free to run, but your carrier plan may still apply SMS costs."
      },
      "typeVersion": 1
    },
    {
      "id": "87609a60-479e-47c8-b1c5-e12b436849bd",
      "name": "Setup guide",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1664,
        176
      ],
      "parameters": {
        "color": 5,
        "width": 472,
        "height": 324,
        "content": "## Setup guide\n\n1. Install the community node: n8n-nodes-rcszilla.\n2. Add Google Calendar and RCSZilla credentials.\n3. Connect the Android phone that will send SMS messages. RCSZilla setup docs: https://docs.rcszilla.com/?page=get_started\n4. Choose the Google Calendar that stores appointments.\n5. Add appointment metadata to each event description:\n\nName: Maria\nPhone: +40700000000\nSMS consent: yes\n\n6. Test with one internal event before activating.\n\nOnly send reminders to customers who gave SMS consent."
      },
      "typeVersion": 1
    },
    {
      "id": "29229a23-6d34-46d9-a70a-dab9a703d411",
      "name": "Calendar event format",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1664,
        528
      ],
      "parameters": {
        "color": 6,
        "width": 476,
        "height": 320,
        "content": "## Calendar event format\n\nThe parser reads the event title, description, date/time, and location.\n\nRequired in the description:\n\nPhone: +40700000000\nSMS consent: yes\n\nOptional:\n\nName: Maria\n\nThe SMS includes the appointment title, date/time, location when available, and an opt-out line."
      },
      "typeVersion": 1
    },
    {
      "id": "06e93633-4a6b-44d5-906b-4daff062365c",
      "name": "Note - Daily trigger",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1120,
        496
      ],
      "parameters": {
        "color": 7,
        "width": 280,
        "height": 204,
        "content": "## 1. Daily trigger\n\nStarts the workflow every day at 09:00.\n\nChange this time if your customers should receive reminders earlier or later in the day."
      },
      "typeVersion": 1
    },
    {
      "id": "57dcad09-2943-4e9e-abdf-d0607bbcca7a",
      "name": "Note - Read appointments",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -624,
        496
      ],
      "parameters": {
        "color": 7,
        "width": 272,
        "height": 208,
        "content": "## 2. Read appointments\n\nGets tomorrow's Google Calendar events from the selected calendar.\n\nThe node only returns the fields this workflow needs: id, title, description, start/end time, and location."
      },
      "typeVersion": 1
    },
    {
      "id": "46249348-1f6b-4dac-8699-d5160df388fe",
      "name": "Note - Prepare reminder",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        496
      ],
      "parameters": {
        "color": 7,
        "width": 292,
        "height": 212,
        "content": "## 3. Prepare reminder\n\nParses the event text for Phone, Name, and SMS consent.\n\nIt normalizes the phone number, builds the reminder text, adds the opt-out line, and marks the item as eligible only when consent is present."
      },
      "typeVersion": 1
    },
    {
      "id": "a8cabb45-bfdf-4853-bea5-ee1cef1f7c85",
      "name": "Note - Consent gate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 304,
        "height": 224,
        "content": "## 4. Consent gate\n\nChecks whether the event has a usable phone number and positive SMS consent.\n\nEligible reminders go to RCSZilla. Missing or opted-out records go to the skipped summary."
      },
      "typeVersion": 1
    },
    {
      "id": "126adbab-b3e5-481c-a235-ab02fe0fc04e",
      "name": "Note - Queue SMS",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 248,
        "height": 208,
        "content": "## 5. Queue SMS\n\nQueues the reminder with RCSZilla using the SMS channel.\n\nRCSZilla sends from your connected Android device, which acts as the phone-based SMS gateway for this free reminder workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "79d02d22-2406-41e3-aeba-e66a0489d836",
      "name": "Note - Skipped reminders",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        864
      ],
      "parameters": {
        "color": 7,
        "width": 264,
        "height": 192,
        "content": "## Skipped reminders\n\nUse this output while testing.\n\nIf a reminder is skipped, update the Google Calendar event description with a valid phone number and SMS consent, or leave it skipped when the customer has not opted in."
      },
      "typeVersion": 1
    },
    {
      "id": "ac0abac2-1a58-4cf8-b826-cd093d25ef88",
      "name": "Every day at 09:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1040,
        720
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "8a7d99d4-515d-4c4d-81da-056ee1dfc339",
      "name": "Get tomorrow's appointments",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        -528,
        720
      ],
      "parameters": {
        "options": {
          "fields": "items(id,summary,description,start,end,location)",
          "orderBy": "startTime"
        },
        "timeMax": "={{ $now.plus({ days: 1 }).endOf('day').toISO() }}",
        "timeMin": "={{ $now.plus({ days: 1 }).startOf('day').toISO() }}",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "primary",
          "cachedResultName": "Primary Calendar"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 1.3
    },
    {
      "id": "4bffb98f-5c70-4f42-a3b4-c59d321918fa",
      "name": "Prepare reminder SMS",
      "type": "n8n-nodes-base.code",
      "position": [
        -112,
        720
      ],
      "parameters": {
        "jsCode": "function present(value) {\n  return value !== undefined && value !== null && String(value).trim() !== '';\n}\n\nfunction cleanPhone(value) {\n  if (!present(value)) return '';\n  const text = String(value).trim();\n  if (text.startsWith('+')) return '+' + text.slice(1).replace(/\\D/g, '');\n  return text.replace(/[^\\d+]/g, '');\n}\n\nfunction lineValue(text, labelPattern) {\n  const pattern = new RegExp(`^\\\\s*(?:${labelPattern})\\\\s*[:=]\\\\s*(.+)$`, 'im');\n  const match = String(text || '').match(pattern);\n  return match ? match[1].trim() : '';\n}\n\nfunction formatDateTime(value) {\n  if (!present(value)) return 'your scheduled time';\n  const date = new Date(value);\n  if (Number.isNaN(date.getTime())) return String(value);\n  return date.toLocaleString('en-US', {\n    weekday: 'short',\n    month: 'short',\n    day: 'numeric',\n    hour: 'numeric',\n    minute: '2-digit',\n  });\n}\n\nconst event = $json;\nconst text = [event.summary, event.description, event.location].filter(present).join('\\n');\nconst explicitPhone = lineValue(text, 'Phone|Tel|Mobile|SMS');\nconst fallbackPhone = (text.match(/\\+?[0-9][0-9\\s().-]{7,}/) || [])[0] || '';\nconst phone = cleanPhone(explicitPhone || fallbackPhone);\nconst consentText = lineValue(text, 'SMS consent|smsConsent|SMS opt in|sms_opt_in');\nconst hasConsent = ['yes', 'true', '1', 'subscribed', 'ok'].includes(String(consentText).trim().toLowerCase());\nconst optedOut = /\\b(no sms|sms opt out|unsubscribe|do not text|stop)\\b/i.test(text);\nconst customerName = lineValue(text, 'Name|Customer|Client') || 'there';\nconst start = event.start?.dateTime || event.start?.date || '';\nconst appointmentTime = formatDateTime(start);\nconst service = event.summary || 'your appointment';\nconst location = present(event.location) ? ` at ${event.location}` : '';\nconst message = `Hi ${customerName}, reminder: ${service} is scheduled for ${appointmentTime}${location}. Reply STOP to opt out.`;\nconst eligible = Boolean(phone && hasConsent && !optedOut);\n\nreturn [{\n  json: {\n    eligible,\n    skipReason: eligible ? '' : 'Missing phone, SMS consent, or customer has opted out',\n    phone,\n    message: message.length > 320 ? message.slice(0, 317) + '...' : message,\n    customerName,\n    appointmentTime,\n    service,\n    location: event.location || '',\n    eventId: event.id || '',\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "55c4f848-cd8a-42c2-8172-5d4394cb4116",
      "name": "Has phone and SMS consent?",
      "type": "n8n-nodes-base.if",
      "position": [
        336,
        720
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{$json.eligible}}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "bfdec4b3-d972-4449-a525-557ab385ac61",
      "name": "Queue appointment SMS with RCSZilla",
      "type": "n8n-nodes-rcszilla.rcsZilla",
      "position": [
        912,
        528
      ],
      "parameters": {
        "to": "={{$json.phone}}",
        "message": "={{$json.message}}"
      },
      "typeVersion": 1
    },
    {
      "id": "ab772d73-00b3-4693-b58d-d2cc968b0295",
      "name": "Skipped reminder summary",
      "type": "n8n-nodes-base.code",
      "position": [
        928,
        1088
      ],
      "parameters": {
        "jsCode": "return [{ json: { status: 'skipped', reason: $json.skipReason, eventId: $json.eventId, service: $json.service } }];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "bffa44f1-17c6-406f-98c6-caf4d4ea7c61",
  "connections": {
    "Every day at 09:00": {
      "main": [
        [
          {
            "node": "Get tomorrow's appointments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare reminder SMS": {
      "main": [
        [
          {
            "node": "Has phone and SMS consent?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has phone and SMS consent?": {
      "main": [
        [
          {
            "node": "Queue appointment SMS with RCSZilla",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skipped reminder summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get tomorrow's appointments": {
      "main": [
        [
          {
            "node": "Prepare reminder SMS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}