{
  "nodes": [
    {
      "id": "sticky-title",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1160,
        -300
      ],
      "parameters": {
        "content": "## Missed Call Text Back\n- Every missed call gets a text back within seconds, so the caller is not left hanging\n- Calls that land late at night wait politely and get their text at 8am",
        "height": 200,
        "width": 480
      }
    },
    {
      "id": "sticky-quiet-hours",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -260,
        -260
      ],
      "parameters": {
        "content": "## Quiet hours\n- A call between 8pm and 8am holds here and the text goes out at 8am",
        "height": 160,
        "width": 400,
        "color": 4
      }
    },
    {
      "id": "webhook-missed-call",
      "name": "Phone system reports a missed call",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -1100,
        0
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "demo/missed-call",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "code-read-call",
      "name": "Read the call details",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -880,
        0
      ],
      "parameters": {
        "jsCode": "// Missed-call events look different depending on the phone system.\n// Check the common shapes one after another: Quo/OpenPhone style,\n// Twilio style (a call that ended with CallStatus no-answer or busy),\n// CallRail style, and a plain flat shape. Whichever one arrives, we\n// pull out the same four facts: who called, their name if we have it,\n// which line they dialed, and when.\nconst raw = $json.body && typeof $json.body === 'object' ? $json.body : $json;\n\nconst pick = (...keys) => {\n  for (const k of keys) {\n    const v = raw[k];\n    if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();\n  }\n  return '';\n};\n\nconst digitsWithPlus = (p) => {\n  let out = String(p).replace(/[^\\d+]/g, '');\n  if (out && !out.startsWith('+')) {\n    out = out.length === 10 ? `+1${out}` : `+${out}`;\n  }\n  return out;\n};\n\nconst phone = digitsWithPlus(pick('caller', 'from', 'From', 'callerNumber', 'caller_number', 'customer_phone_number', 'phone'));\nconst callerName = pick('callerName', 'caller_name', 'CallerName', 'customer_name', 'name');\nconst lineCalled = digitsWithPlus(pick('called', 'to', 'To', 'calledNumber', 'called_number', 'tracking_phone_number', 'business_phone_number', 'line'));\nconst calledAt = pick('callTime', 'call_time', 'timestamp', 'Timestamp', 'createdAt', 'start_time', 'time') || new Date().toISOString();\n\nconst TZ = 'America/Denver';\nconst parsed = new Date(calledAt);\nconst calledAtLocal = (isNaN(parsed.getTime()) ? new Date() : parsed).toLocaleString('en-US', { timeZone: TZ });\n\n// Quiet hours: no texting between 8pm and 8am Denver time.\n// A late-night call gets its text held until 8am instead.\nconst nowLocal = new Date(new Date().toLocaleString('en-US', { timeZone: TZ }));\nconst hour = nowLocal.getHours();\nconst minute = nowLocal.getMinutes();\nconst canTextNow = hour >= 8 && hour < 20;\n\nlet waitMinutes = 0;\nif (!canTextNow) {\n  waitMinutes = hour < 8 ? (8 - hour) * 60 - minute : (24 - hour + 8) * 60 - minute;\n}\nconst sendAt = new Date(Date.now() + waitMinutes * 60000).toISOString();\n\nreturn { phone, callerName, lineCalled, calledAt, calledAtLocal, canTextNow, sendAt };"
      }
    },
    {
      "id": "respond-ok",
      "name": "Tell the phone system we got it",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        -660,
        0
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "{\n  \"status\": \"received\"\n}",
        "options": {}
      }
    },
    {
      "id": "ghl-save-caller",
      "name": "Save the caller in GoHighLevel",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -440,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "https://services.leadconnectorhq.com/contacts/upsert",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ locationId: 'bsHomeSvcDemo01', phone: $('Read the call details').item.json.phone, name: $('Read the call details').item.json.callerName || undefined, tags: ['missed-call', 'needs-text-back'] }) }}",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "if-quiet-hours",
      "name": "Okay to text right now?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -220,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "in-texting-window",
              "leftValue": "={{ $('Read the call details').item.json.canTextNow }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "wait-until-8am",
      "name": "Hold the text until 8am",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        0,
        180
      ],
      "parameters": {
        "resume": "specificTime",
        "dateTime": "={{ $('Read the call details').item.json.sendAt }}"
      }
    },
    {
      "id": "sms-text-back",
      "name": "Text the caller back",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "https://services.leadconnectorhq.com/conversations/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ type: 'SMS', contactId: $('Save the caller in GoHighLevel').item.json.contact?.id || $('Save the caller in GoHighLevel').item.json.id, message: 'Hey, this is Blue Summit Home Services. We just missed your call. What can we help with?' }) }}",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "id": "slack-missed-calls",
      "name": "Post a note in #missed-calls",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        440,
        0
      ],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "missed-calls"
        },
        "text": "=Missed call from {{ $('Read the call details').item.json.phone }}{{ $('Read the call details').item.json.callerName ? ' (' + $('Read the call details').item.json.callerName + ')' : '' }} at {{ $('Read the call details').item.json.calledAtLocal }}. {{ $('Read the call details').item.json.canTextNow ? 'Text back sent right away.' : 'Call came in overnight, so the text was held and sent at 8am.' }}",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Phone system reports a missed call": {
      "main": [
        [
          {
            "node": "Read the call details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the call details": {
      "main": [
        [
          {
            "node": "Tell the phone system we got it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tell the phone system we got it": {
      "main": [
        [
          {
            "node": "Save the caller in GoHighLevel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save the caller in GoHighLevel": {
      "main": [
        [
          {
            "node": "Okay to text right now?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Okay to text right now?": {
      "main": [
        [
          {
            "node": "Text the caller back",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Hold the text until 8am",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hold the text until 8am": {
      "main": [
        [
          {
            "node": "Text the caller back",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the caller back": {
      "main": [
        [
          {
            "node": "Post a note in #missed-calls",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}