{
  "name": "Appointment Reminder & No-Show Rescue Engine",
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": true
  },
  "nodes": [
    {
      "name": "Run Sweep Now",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -1180,
        280
      ],
      "parameters": {}
    },
    {
      "name": "Every 15 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -1180,
        460
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      }
    },
    {
      "name": "Init Reminder Run",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -960,
        370
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/01-init-reminder-run.js"
        }
      }
    },
    {
      "name": "Use Live Calendar?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -740,
        370
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "use-live",
              "leftValue": "={{ $json.useLive }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "CRM: Fetch Appointments",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -500,
        240
      ],
      "parameters": {
        "method": "GET",
        "url": "https://services.leadconnectorhq.com/calendars/events",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "specifyQuery": "keypair",
        "queryParameters": {
          "parameters": [
            {
              "name": "locationId",
              "value": "={{ $('Init Reminder Run').item.json.config.locationId }}"
            },
            {
              "name": "startTime",
              "value": "={{ new Date(Date.now() - 5 * 86400000).toISOString() }}"
            },
            {
              "name": "endTime",
              "value": "={{ new Date(Date.now() + 3 * 86400000).toISOString() }}"
            }
          ]
        },
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-04-15"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {
          "timeout": 20000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "name": "Mock Appointments",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -500,
        500
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/02-mock-appointments.js"
        }
      }
    },
    {
      "name": "Classify Fetch Error",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -260,
        40
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/05-classify-api-error.js"
        }
      }
    },
    {
      "name": "Retry Fetch?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -40,
        40
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "should-retry",
              "leftValue": "={{ $json.shouldRetry }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "Backoff Wait",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        -40,
        -160
      ],
      "parameters": {
        "amount": "={{ $json.retry.waitSeconds }}",
        "unit": "seconds"
      }
    },
    {
      "name": "Stop: Calendar Unavailable",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        200,
        -80
      ],
      "parameters": {}
    },
    {
      "name": "Plan Sends",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -40,
        370
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/03-plan-sends.js"
        }
      }
    },
    {
      "name": "Anything Due?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        180,
        370
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "has-sends",
              "leftValue": "={{ $json.hasSends }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "Nothing Due This Sweep",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        420,
        520
      ],
      "parameters": {}
    },
    {
      "name": "Render Messages",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        280
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/04-render-message.js"
        }
      }
    },
    {
      "name": "Send For Real?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        640,
        280
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "really-send",
              "leftValue": "={{ $json.config.simulateDelivery !== true && $json.blocked !== true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "Simulate Delivery",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        460
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/06-simulate-delivery.js"
        }
      }
    },
    {
      "name": "SMS or Email?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        160
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "is-sms",
              "leftValue": "={{ $json.channel === 'sms' }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "Send SMS",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1120,
        60
      ],
      "parameters": {
        "method": "POST",
        "url": "https://services.leadconnectorhq.com/conversations/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-04-15"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ type: 'SMS', contactId: $json.contactId, message: $json.body }) }}",
        "options": {
          "timeout": 20000,
          "batching": {
            "batch": {
              "batchSize": 10,
              "batchInterval": 1000
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "name": "Send Email",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1120,
        260
      ],
      "parameters": {
        "method": "POST",
        "url": "https://services.leadconnectorhq.com/conversations/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-04-15"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ type: 'Email', contactId: $json.contactId, subject: $json.subject, message: $json.body }) }}",
        "options": {
          "timeout": 20000,
          "batching": {
            "batch": {
              "batchSize": 10,
              "batchInterval": 1000
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "name": "Record Sends",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1360,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/07-record-sends.js"
        }
      }
    },
    {
      "name": "README",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1200,
        -60
      ],
      "parameters": {
        "width": 460,
        "height": 280,
        "color": 4,
        "content": "## Appointment reminders that respect the customer's clock\n\nRuns every 15 minutes. Works out which reminder touches are due, in the customer's own timezone, defers anything landing in quiet hours, and cancels a no-show sequence the moment the customer rebooks.\n\n**Sends nothing by default.** `simulateDelivery: true` and `mockMode: true` in **Init Reminder Run**. Hit **Test workflow** and read **Record Sends**.\n\nWhile `mockMode` is on the sweep is evaluated at `mockClockIso`, not at *now* \u2014 the fixture calendar only means anything relative to one instant, so pinning the clock is what makes this demo show the same sweep on any day. The live path always uses the real clock.\n\nThe node to open is **Plan Sends**."
      }
    },
    {
      "name": "Why this is code",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -280,
        -300
      ],
      "parameters": {
        "width": 480,
        "height": 240,
        "color": 5,
        "content": "## The three things a reminder builder gets wrong\n\n**Timezones.** \"Send at 9am\" means 9am where the customer is. Fixed offsets break twice a year; the server's clock texts Sydney at 3am. This uses Intl with real IANA zones, so DST is the tz database's problem, not ours.\n\n**Idempotency.** A 15-minute sweep with a 90-minute due window sees every touch six times. A deterministic key per (appointment, touch, channel) is what stops six texts.\n\n**Cancellation.** A rescue sequence that keeps running after the customer rebooks is worse than no sequence."
      }
    },
    {
      "name": "SMS billing",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        180,
        -80
      ],
      "parameters": {
        "width": 400,
        "height": 200,
        "color": 3,
        "content": "## Segment counting\n\nSMS is billed per segment. One emoji in a service name flips the message from GSM-7 to UCS-2 and drops the segment size from 160 characters to 70 \u2014 so a 150-character message silently bills as 3 segments instead of 1.\n\n**Render Messages** counts this before sending and blocks anything over the configured limit. It also refuses to send a message with an unresolved `{{placeholder}}`."
      }
    },
    {
      "name": "Calendar Fetched OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -260,
        240
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "calendar-fetched-ok",
              "leftValue": "={{ $json.statusCode >= 200 && $json.statusCode < 300 }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    }
  ],
  "connections": {
    "Run Sweep Now": {
      "main": [
        [
          {
            "node": "Init Reminder Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 15 Minutes": {
      "main": [
        [
          {
            "node": "Init Reminder Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Init Reminder Run": {
      "main": [
        [
          {
            "node": "Use Live Calendar?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use Live Calendar?": {
      "main": [
        [
          {
            "node": "CRM: Fetch Appointments",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mock Appointments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CRM: Fetch Appointments": {
      "main": [
        [
          {
            "node": "Calendar Fetched OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mock Appointments": {
      "main": [
        [
          {
            "node": "Plan Sends",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Fetch Error": {
      "main": [
        [
          {
            "node": "Retry Fetch?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retry Fetch?": {
      "main": [
        [
          {
            "node": "Backoff Wait",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop: Calendar Unavailable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Backoff Wait": {
      "main": [
        [
          {
            "node": "CRM: Fetch Appointments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Plan Sends": {
      "main": [
        [
          {
            "node": "Anything Due?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anything Due?": {
      "main": [
        [
          {
            "node": "Render Messages",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Nothing Due This Sweep",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Render Messages": {
      "main": [
        [
          {
            "node": "Send For Real?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send For Real?": {
      "main": [
        [
          {
            "node": "SMS or Email?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Simulate Delivery",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SMS or Email?": {
      "main": [
        [
          {
            "node": "Send SMS",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send SMS": {
      "main": [
        [
          {
            "node": "Record Sends",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email": {
      "main": [
        [
          {
            "node": "Record Sends",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simulate Delivery": {
      "main": [
        [
          {
            "node": "Record Sends",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calendar Fetched OK?": {
      "main": [
        [
          {
            "node": "Plan Sends",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Classify Fetch Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}