{
  "id": "bMqCCMpHCMnY9VeR",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Predict No-show Risk and Route Reminders from Google Calendar Bookings",
  "tags": [],
  "nodes": [
    {
      "id": "b40b46db-8de0-488b-a1af-33583e4161c0",
      "name": "README",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1312,
        -192
      ],
      "parameters": {
        "width": 588,
        "height": 396,
        "content": "## Predict No-show Risk and Route Reminders from Google Calendar Bookings\n\nThis workflow predicts appointment no-show risk using historical customer data and sends differentiated reminders based on risk level.\n\n## How it works\n1. Runs daily at 9 AM to check tomorrow's bookings\n2. Fetches scheduled appointments from Google Calendar\n3. Looks up each customer's history from Google Sheets\n4. Calculates a risk score (0-100) from four weighted signals\n5. Routes to three paths based on risk level\n\nSuper High (>=70): Slack alert + AI re-confirmation email\nHigh (>=40): AI-generated friendly reminder email\nLow (<40): Silent log for records\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "f7ac8389-e75d-4799-9082-43d4938e0085",
      "name": "Trigger & Configuration",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1920,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 380,
        "height": 540,
        "content": "## Trigger & Configuration\n\nRuns daily at 9 AM.\nAll settings managed in Set Configuration."
      },
      "typeVersion": 1
    },
    {
      "id": "cca5d574-b120-45a7-9925-8f7d6f13d025",
      "name": "Data Collection",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2320,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 580,
        "height": 540,
        "content": "## Data Collection\n\nFetches tomorrow's bookings from Google Calendar.\nLooks up each customer's history from Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "ee60caa9-5f9e-4310-8139-f7c1225afb17",
      "name": "Risk Assessment",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2928,
        -192
      ],
      "parameters": {
        "color": 7,
        "height": 540,
        "content": "## Risk Assessment\n\nCalculates score (0-100)\nfrom 4 weighted signals:\n- No-show rate (40%)\n- Lead time (20%)\n- Day/time pattern (20%)\n- Recent cancellations (20%)"
      },
      "typeVersion": 1
    },
    {
      "id": "b8cd812d-765b-4bd2-ab03-501267182d38",
      "name": "Risk Routing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3184,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 200,
        "height": 540,
        "content": "## Risk Routing\n\nRoutes by score:\n- Super High (>=70)\n- High (>=40)\n- Low (<40)"
      },
      "typeVersion": 1
    },
    {
      "id": "5c4ffe28-858f-4aef-afbb-bfff57954b51",
      "name": "Super High Response",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3408,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 336,
        "content": "## Super High (>=70)\n\nSlack alert + re-confirmation email."
      },
      "typeVersion": 1
    },
    {
      "id": "5c3e5413-89a3-4e17-b172-c55fd753077a",
      "name": "High Response",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3408,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 272,
        "content": "## High (>=40)\n\nAI reminder email to customer."
      },
      "typeVersion": 1
    },
    {
      "id": "06c3a52b-2a77-4eea-9349-de20d07c65ed",
      "name": "Low Response",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3408,
        464
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 304,
        "content": "## Low (<40)\n\nSilent log to Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "7ac2141d-7f2d-46f1-9f94-edf37eec0f81",
      "name": "Daily at 9 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        1968,
        48
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "40791af4-a6f3-4943-8e4b-5fa957990c16",
      "name": "Set Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        2144,
        48
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-1",
              "name": "calendar_id",
              "type": "string",
              "value": "primary"
            },
            {
              "id": "cfg-2",
              "name": "history_sheet_id",
              "type": "string",
              "value": "REPLACE_WITH_YOUR_SHEET_ID"
            },
            {
              "id": "cfg-3",
              "name": "history_sheet_name",
              "type": "string",
              "value": "customer_history"
            },
            {
              "id": "cfg-4",
              "name": "manager_slack_channel",
              "type": "string",
              "value": "#booking-alerts"
            },
            {
              "id": "cfg-5",
              "name": "super_high_threshold",
              "type": "number",
              "value": 70
            },
            {
              "id": "cfg-6",
              "name": "high_threshold",
              "type": "number",
              "value": 40
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "fee3e1b0-b797-4870-b907-b39fc5ac49ee",
      "name": "Get Tomorrow's Bookings",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        2384,
        48
      ],
      "parameters": {
        "options": {},
        "timeMax": "={{ $now.plus(1, 'day').endOf('day').toISO() }}",
        "timeMin": "={{ $now.plus(1, 'day').startOf('day').toISO() }}",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.calendar_id }}"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "typeVersion": 1.3
    },
    {
      "id": "2606e518-fdf8-431e-8021-1711d3a4f165",
      "name": "Extract Customer Info",
      "type": "n8n-nodes-base.set",
      "position": [
        2576,
        48
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ex-1",
              "name": "customer_email",
              "type": "string",
              "value": "={{ $json.attendees ? $json.attendees[0].email : ($json.creator ? $json.creator.email : '') }}"
            },
            {
              "id": "ex-2",
              "name": "customer_name",
              "type": "string",
              "value": "={{ $json.attendees && $json.attendees[0].displayName ? $json.attendees[0].displayName : $json.summary }}"
            },
            {
              "id": "ex-3",
              "name": "event_id",
              "type": "string",
              "value": "={{ $json.id }}"
            },
            {
              "id": "ex-4",
              "name": "start_time",
              "type": "string",
              "value": "={{ $json.start.dateTime }}"
            },
            {
              "id": "ex-5",
              "name": "summary",
              "type": "string",
              "value": "={{ $json.summary }}"
            },
            {
              "id": "ex-6",
              "name": "config_sheet_id",
              "type": "string",
              "value": "={{ $('Set Configuration').item.json.history_sheet_id }}"
            },
            {
              "id": "ex-7",
              "name": "config_sheet_name",
              "type": "string",
              "value": "={{ $('Set Configuration').item.json.history_sheet_name }}"
            },
            {
              "id": "ex-8",
              "name": "config_slack_channel",
              "type": "string",
              "value": "={{ $('Set Configuration').item.json.manager_slack_channel }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "378138f1-3e6a-4562-8836-5d3546f7bb14",
      "name": "Lookup Customer History",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        2768,
        48
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.customer_email }}",
              "lookupColumn": "customer_email"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.config_sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.config_sheet_id }}"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.7
    },
    {
      "id": "f4204ea2-9524-411c-a542-748ccba61eea",
      "name": "Calculate Risk Score",
      "type": "n8n-nodes-base.code",
      "position": [
        2992,
        48
      ],
      "parameters": {
        "jsCode": "// Risk Score Calculator\n// Combines 4 weighted indicators into a 0-100 score\nconst booking = $input.first().json;\n\nconst customer_email = booking.customer_email || '';\nconst customer_name = booking.customer_name || 'Customer';\nconst event_id = booking.event_id || '';\nconst start_time = booking.start_time || '';\nconst summary = booking.summary || 'Appointment';\nconst config_slack_channel = booking.config_slack_channel || '#booking-alerts';\n\nconst total_bookings = Number(booking.total_bookings) || 0;\nconst no_show_count = Number(booking.no_show_count) || 0;\nconst last_booking_date = booking.last_booking_date || null;\nconst last_status = booking.last_status || null;\n\n// Signal 1: No-show rate (40%)\nlet noShowRateScore = 0;\nif (total_bookings > 0) {\n  const rate = no_show_count / total_bookings;\n  noShowRateScore = Math.min(rate * 100, 100) * 0.4;\n} else {\n  noShowRateScore = 15;\n}\n\n// Signal 2: Lead time (20%)\nlet leadTimeScore = 10;\ntry {\n  const appt = new Date(start_time);\n  const now = new Date();\n  const leadDays = (appt - now) / (1000 * 60 * 60 * 24);\n  if (leadDays > 14) leadTimeScore = 20;\n  else if (leadDays > 7) leadTimeScore = 15;\n  else if (leadDays > 3) leadTimeScore = 10;\n  else leadTimeScore = 5;\n} catch (e) {}\n\n// Signal 3: Day/time pattern (20%)\nlet timeSlotScore = 5;\ntry {\n  const appt = new Date(start_time);\n  const dow = appt.getDay();\n  const hour = appt.getHours();\n  if ((dow === 1 && hour >= 9 && hour <= 11) ||\n      (dow === 5 && hour >= 17 && hour <= 20)) {\n    timeSlotScore = 20;\n  } else if (dow === 0 || dow === 6) {\n    timeSlotScore = 10;\n  }\n} catch (e) {}\n\n// Signal 4: Recent cancellations (20%)\nlet recentCancelScore = 0;\nif (last_status === 'no_show' || last_status === 'cancelled') {\n  if (last_booking_date) {\n    const last = new Date(last_booking_date);\n    const daysSince = (new Date() - last) / (1000 * 60 * 60 * 24);\n    if (daysSince < 30) recentCancelScore = 20;\n    else if (daysSince < 90) recentCancelScore = 10;\n    else recentCancelScore = 5;\n  } else {\n    recentCancelScore = 10;\n  }\n}\n\nconst risk_score = Math.round(\n  noShowRateScore + leadTimeScore + timeSlotScore + recentCancelScore\n);\n\nlet risk_level;\nif (risk_score >= 70) risk_level = 'super_high';\nelse if (risk_score >= 40) risk_level = 'high';\nelse risk_level = 'low';\n\nreturn {\n  json: {\n    customer_email,\n    customer_name,\n    event_id,\n    start_time,\n    summary,\n    total_bookings,\n    no_show_count,\n    risk_score,\n    risk_level,\n    config_slack_channel,\n    score_breakdown: {\n      no_show_rate: Math.round(noShowRateScore),\n      lead_time: leadTimeScore,\n      time_slot: timeSlotScore,\n      recent_cancel: recentCancelScore\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "cdf9d307-b7f0-437d-8bbf-88b94ab1a290",
      "name": "Route by Risk Level",
      "type": "n8n-nodes-base.switch",
      "position": [
        3232,
        48
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "super_high",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "sw-1",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.risk_level }}",
                    "rightValue": "super_high"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "high",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "sw-2",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.risk_level }}",
                    "rightValue": "high"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "low",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "sw-3",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.risk_level }}",
                    "rightValue": "low"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.4
    },
    {
      "id": "9a978253-1afe-44c0-b1f1-5c93401872b6",
      "name": "Generate Urgent Message",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        3472,
        -96
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {},
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "You are a polite scheduling assistant. Write a brief re-confirmation email that asks the customer to confirm attendance. Warm, clear, under 5 sentences. Plain text only."
            },
            {
              "content": "=Customer: {{ $('Calculate Risk Score').item.json.customer_name }}\nAppointment: {{ $('Calculate Risk Score').item.json.summary }}\nTime: {{ $('Calculate Risk Score').item.json.start_time }}\n\nDraft the re-confirmation email body now."
            }
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "4a3132d2-b15c-4f6b-bcdf-e3d4a10229f1",
      "name": "Send Slack Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        3856,
        -176
      ],
      "parameters": {
        "text": "=High no-show risk detected\n\n- Customer: {{ $('Calculate Risk Score').item.json.customer_name }}\n- Appointment: {{ $('Calculate Risk Score').item.json.summary }}\n- Time: {{ $('Calculate Risk Score').item.json.start_time }}\n- Risk score: {{ $('Calculate Risk Score').item.json.risk_score }}/100\n\nA re-confirmation email has been sent automatically.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Calculate Risk Score').item.json.config_slack_channel }}"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.4
    },
    {
      "id": "a2a357d2-44b2-4480-8709-01c405d2c540",
      "name": "Send Re-confirmation Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3856,
        -16
      ],
      "parameters": {
        "sendTo": "={{ $('Calculate Risk Score').item.json.customer_email }}",
        "message": "={{ $json.content }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "=Please confirm your appointment on {{ $('Calculate Risk Score').item.json.start_time }}",
        "emailType": "text"
      },
      "typeVersion": 2.2
    },
    {
      "id": "ea721c09-b85e-4419-843b-e0e702f99b5a",
      "name": "Generate Reminder",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        3472,
        272
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {},
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "You are a friendly scheduling assistant. Write a short reminder email in warm, casual tone. Under 4 sentences. Plain text only."
            },
            {
              "content": "=Customer: {{ $('Calculate Risk Score').item.json.customer_name }}\nAppointment: {{ $('Calculate Risk Score').item.json.summary }}\nTime: {{ $('Calculate Risk Score').item.json.start_time }}\n\nDraft the reminder email body now."
            }
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "f6223e65-e6e9-42a5-9a97-8288f82f1167",
      "name": "Send Reminder Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3856,
        272
      ],
      "parameters": {
        "sendTo": "={{ $('Calculate Risk Score').item.json.customer_email }}",
        "message": "={{ $json.content }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "=Reminder: your appointment on {{ $('Calculate Risk Score').item.json.start_time }}",
        "emailType": "text"
      },
      "typeVersion": 2.2
    },
    {
      "id": "077732fb-72e0-4f0c-9593-525ae39aff43",
      "name": "Log Low-Risk Booking",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3472,
        592
      ],
      "parameters": {
        "columns": {
          "value": {
            "last_status": "confirmed",
            "customer_name": "={{ $json.customer_name }}",
            "no_show_count": "={{ $json.no_show_count || 0 }}",
            "customer_email": "={{ $json.customer_email }}",
            "total_bookings": "={{ ($json.total_bookings || 0) + 1 }}",
            "last_booking_date": "={{ $json.start_time.substring(0,10) }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Set Configuration').item.json.history_sheet_name }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.history_sheet_id }}"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "22f0842a-ae35-49cb-ae9b-f187fca0dfe2",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1312,
        224
      ],
      "parameters": {
        "width": 592,
        "height": 240,
        "content": "## Setup steps\n1. Create Google Sheets with customer_history tab\n   (customer_email, customer_name, total_bookings, no_show_count, last_booking_date, last_status)\n2. Open Set Configuration and fill in Sheet ID and Slack channel\n3. Connect Google Calendar, Sheets, Gmail, Slack, and OpenAI credentials\n4. Activate the workflow\n\nNote: Risk scoring is 100% rule-based.\nAI is used only for generating reminder text, never for the risk judgment itself."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "c5856c2c-4365-4e9f-bd9e-fb87da5fa52a",
  "connections": {
    "Daily at 9 AM": {
      "main": [
        [
          {
            "node": "Set Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Reminder": {
      "main": [
        [
          {
            "node": "Send Reminder Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Configuration": {
      "main": [
        [
          {
            "node": "Get Tomorrow's Bookings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Risk Level": {
      "main": [
        [
          {
            "node": "Generate Urgent Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Reminder",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Low-Risk Booking",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Risk Score": {
      "main": [
        [
          {
            "node": "Route by Risk Level",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Customer Info": {
      "main": [
        [
          {
            "node": "Lookup Customer History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Urgent Message": {
      "main": [
        [
          {
            "node": "Send Slack Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Re-confirmation Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Tomorrow's Bookings": {
      "main": [
        [
          {
            "node": "Extract Customer Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lookup Customer History": {
      "main": [
        [
          {
            "node": "Calculate Risk Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}