This workflow follows the HTTP Request → Slack recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"nodes": [
{
"id": "sa-note",
"name": "Section 1 note",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-460,
-160
],
"parameters": {
"content": "## 1. Booking confirmed on the spot\n- A new booking lands, the customer is saved in GoHighLevel with their appointment time, and the confirmation text goes out right away.",
"height": 140,
"width": 620,
"color": 4
}
},
{
"id": "sa-hook",
"name": "A new appointment gets booked",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-420,
80
],
"parameters": {
"httpMethod": "POST",
"path": "demo/appointment-booked",
"responseMode": "responseNode",
"options": {}
}
},
{
"id": "sa-tidy",
"name": "Tidy up the booking details",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-200,
80
],
"parameters": {
"jsCode": "// Booking events arrive in a few different shapes, so check each one in turn.\nconst TZ = 'America/Denver';\n\nconst results = [];\n\nfor (const item of $input.all()) {\n const raw = item.json || {};\n const body = raw.body || {};\n const custom = raw.customData || body.customData || {};\n const contact = raw.contact || body.contact || {};\n const sources = [raw, body, custom, contact];\n\n const pick = (...keys) => {\n for (const key of keys) {\n for (const source of sources) {\n if (!source || typeof source !== 'object') continue;\n const value = source[key];\n if (value === undefined || value === null) continue;\n const text = String(value).trim();\n if (text !== '') return text;\n }\n }\n return '';\n };\n\n const fullName = pick('name', 'full_name', 'fullName', 'customerName', 'customer_name');\n const firstName = pick('firstName', 'first_name') || fullName.split(' ')[0] || 'there';\n\n let phone = pick('phone', 'phoneNumber', 'phone_number', 'mobile', 'customerPhone');\n phone = phone.replace(/[^0-9+]/g, '');\n if (phone && !phone.startsWith('+')) {\n phone = phone.length === 10 ? '+1' + phone : '+' + phone;\n }\n\n const appointmentTime = pick('appointmentTime', 'appointment_time', 'startTime', 'start_time', 'start', 'apptTime');\n const apptDate = new Date(appointmentTime);\n const validDate = appointmentTime && !isNaN(apptDate.getTime());\n\n const dayPretty = validDate\n ? apptDate.toLocaleDateString('en-US', { timeZone: TZ, weekday: 'long', month: 'long', day: 'numeric' })\n : 'your scheduled day';\n const timePretty = validDate\n ? apptDate.toLocaleTimeString('en-US', { timeZone: TZ, hour: 'numeric', minute: '2-digit' })\n : 'the scheduled time';\n\n results.push({\n json: {\n fullName: fullName || firstName,\n firstName,\n phone,\n contactId: pick('contactId', 'contact_id', 'id'),\n appointmentTime,\n jobType: pick('jobType', 'job_type', 'service', 'serviceType') || 'service visit',\n dayPretty,\n timePretty\n }\n });\n}\n\nreturn results;\n"
}
},
{
"id": "sa-save",
"name": "Save the customer in GoHighLevel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
20,
80
],
"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', firstName: $json.firstName, name: $json.fullName, phone: $json.phone, tags: ['appt-confirmed'], customFields: [{ key: 'appointment_time', field_value: $json.appointmentTime }] }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"id": "sa-sms",
"name": "Text the booking confirmation",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
240,
80
],
"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: $json.contact?.id || $('Tidy up the booking details').item.json.contactId, message: 'Hey ' + $('Tidy up the booking details').item.json.firstName + ', this is Blue Summit. You are on the books for ' + $('Tidy up the booking details').item.json.dayPretty + ' at ' + $('Tidy up the booking details').item.json.timePretty + '. We will text you a heads up before we head over.' }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"id": "sa-respond",
"name": "Tell the calendar it is booked",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
460,
80
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ ok: true, message: 'Booked and the confirmation text has gone out.', phone: $('Tidy up the booking details').item.json.phone }) }}",
"options": {}
}
},
{
"id": "sb-note",
"name": "Section 2 note",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-460,
480
],
"parameters": {
"content": "## 2. The reminder sweep\n- Every fifteen minutes the sweep finds who is about a day out or about two hours out and sends the right text.\n- Both timing windows sit at the top of one code node, so moving a reminder is a one line change.",
"height": 160,
"width": 620,
"color": 4
}
},
{
"id": "sb-schedule",
"name": "Check every fifteen minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-420,
760
],
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 15
}
]
}
}
},
{
"id": "sb-list",
"name": "Get everyone with an appointment on the books",
"type": "n8n-nodes-base.highLevel",
"typeVersion": 2,
"position": [
-200,
760
],
"parameters": {
"resource": "contact",
"operation": "getAll",
"returnAll": true,
"filters": {}
},
"credentials": {
"highLevelOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"id": "sb-code",
"name": "Work out who is due a text now",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
20,
760
],
"parameters": {
"jsCode": "// Change the reminder timing here. Minutes count down to the appointment.\nconst RULES = [\n { due: 'day_before', stampTag: 'reminder-24h-sent', fromMinutes: 22 * 60, toMinutes: 26 * 60 },\n { due: 'morning_of', stampTag: 'reminder-2h-sent', fromMinutes: 90, toMinutes: 150 }\n];\n\nconst TZ = 'America/Denver';\n\nfunction tagsOf(contact) {\n const raw = contact.tags ?? contact.tag ?? [];\n const list = Array.isArray(raw) ? raw : String(raw).split(',');\n return list.map(t => String(t).trim().toLowerCase()).filter(t => t.length > 0);\n}\n\nfunction toUsNumber(raw) {\n let digits = String(raw ?? '').replace(/[^0-9+]/g, '');\n if (!digits) return '';\n if (digits.startsWith('+')) return digits;\n if (digits.length === 10) return '+1' + digits;\n return '+' + digits;\n}\n\nfunction apptTimeOf(contact) {\n const direct = contact.appointment_time || contact.appointmentTime || contact.apptTime;\n if (direct) return String(direct);\n const lists = [contact.customFields, contact.customField, contact.custom_fields];\n for (const list of lists) {\n if (!Array.isArray(list)) continue;\n for (const field of list) {\n if (!field || typeof field !== 'object') continue;\n const key = String(field.key || field.fieldKey || field.name || '').toLowerCase();\n if (!key.includes('appointment')) continue;\n const value = field.value ?? field.field_value ?? field.fieldValue;\n if (value) return String(value);\n }\n }\n return '';\n}\n\nconst now = Date.now();\nconst due = [];\n\nfor (const item of $input.all()) {\n const contact = item.json ?? {};\n const tags = tagsOf(contact);\n\n // The tags are the memory. Whoever already has the stamp tag is skipped,\n // so a restart never sends anyone the same reminder twice.\n if (!tags.includes('appt-confirmed')) continue;\n if (tags.includes('no-show')) continue;\n\n const apptRaw = apptTimeOf(contact);\n if (!apptRaw) continue;\n const apptMs = new Date(apptRaw).getTime();\n if (isNaN(apptMs)) continue;\n\n const minutesToGo = (apptMs - now) / 60000;\n const rule = RULES.find(r => minutesToGo >= r.fromMinutes && minutesToGo <= r.toMinutes);\n if (!rule) continue;\n if (tags.includes(rule.stampTag)) continue;\n\n const phone = toUsNumber(contact.phone ?? contact.phoneNumber ?? contact.mobile);\n if (!phone) continue;\n\n const contactId = contact.id ?? contact.contactId ?? contact._id;\n if (!contactId) continue;\n\n const apptDate = new Date(apptMs);\n due.push({\n json: {\n contactId,\n firstName: contact.firstName ?? contact.first_name ?? contact.contactName ?? 'there',\n phone,\n due: rule.due,\n stampTag: rule.stampTag,\n dayPretty: apptDate.toLocaleDateString('en-US', { timeZone: TZ, weekday: 'long', month: 'long', day: 'numeric' }),\n timePretty: apptDate.toLocaleTimeString('en-US', { timeZone: TZ, hour: 'numeric', minute: '2-digit' })\n }\n });\n}\n\n// Nobody in a window right now means the sweep just ends quietly.\nreturn due;\n"
}
},
{
"id": "sb-switch",
"name": "Which reminder is due?",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
240,
760
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.due }}",
"rightValue": "day_before",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "day_before"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.due }}",
"rightValue": "morning_of",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"outputKey": "morning_of"
}
]
},
"options": {
"fallbackOutput": "none"
}
}
},
{
"id": "sb-sms-daybefore",
"name": "Text the day before reminder",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
460,
660
],
"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: $json.contactId, message: 'Hey ' + $json.firstName + ', Blue Summit here. We have you down for tomorrow at ' + $json.timePretty + '. Reply here if anything changes.' }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"id": "sb-sms-morning",
"name": "Text the morning of heads up",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
460,
860
],
"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: $json.contactId, message: 'Morning ' + $json.firstName + ', Blue Summit here. Our tech is headed your way around ' + $json.timePretty + ' today. See you soon.' }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"id": "sb-stamp",
"name": "Mark the reminder as sent",
"type": "n8n-nodes-base.highLevel",
"typeVersion": 2,
"position": [
700,
760
],
"parameters": {
"resource": "contact",
"operation": "update",
"contactId": "={{ $('Work out who is due a text now').item.json.contactId }}",
"updateFields": {
"tags": "={{ $('Work out who is due a text now').item.json.stampTag }}"
}
},
"credentials": {
"highLevelOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"id": "sc-note",
"name": "Section 3 note",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-460,
1160
],
"parameters": {
"content": "## 3. No-show rescue\n- A no-show gets a low pressure rebook text with the booking link the same day.\n- The office gets a call task for tomorrow and a note in #sales-team, so nobody falls through the cracks.",
"height": 160,
"width": 620,
"color": 4
}
},
{
"id": "sc-hook",
"name": "The calendar marks a no-show",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-420,
1440
],
"parameters": {
"httpMethod": "POST",
"path": "demo/no-show",
"responseMode": "responseNode",
"options": {}
}
},
{
"id": "sc-tidy",
"name": "Tidy up the no-show details",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-200,
1440
],
"parameters": {
"jsCode": "// No-show events arrive in a few different shapes, so check each one in turn.\nconst results = [];\n\nfor (const item of $input.all()) {\n const raw = item.json || {};\n const body = raw.body || {};\n const custom = raw.customData || body.customData || {};\n const contact = raw.contact || body.contact || {};\n const sources = [raw, body, custom, contact];\n\n const pick = (...keys) => {\n for (const key of keys) {\n for (const source of sources) {\n if (!source || typeof source !== 'object') continue;\n const value = source[key];\n if (value === undefined || value === null) continue;\n const text = String(value).trim();\n if (text !== '') return text;\n }\n }\n return '';\n };\n\n const fullName = pick('name', 'full_name', 'fullName', 'customerName', 'customer_name');\n const firstName = pick('firstName', 'first_name') || fullName.split(' ')[0] || 'there';\n\n let phone = pick('phone', 'phoneNumber', 'phone_number', 'mobile', 'customerPhone');\n phone = phone.replace(/[^0-9+]/g, '');\n if (phone && !phone.startsWith('+')) {\n phone = phone.length === 10 ? '+1' + phone : '+' + phone;\n }\n\n results.push({\n json: {\n contactId: pick('contactId', 'contact_id', 'id'),\n fullName: fullName || firstName,\n firstName,\n phone\n }\n });\n}\n\nreturn results;\n"
}
},
{
"id": "sc-tag",
"name": "Tag them as a no-show",
"type": "n8n-nodes-base.highLevel",
"typeVersion": 2,
"position": [
20,
1440
],
"parameters": {
"resource": "contact",
"operation": "update",
"contactId": "={{ $json.contactId }}",
"updateFields": {
"tags": "no-show"
}
},
"credentials": {
"highLevelOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"id": "sc-sms",
"name": "Text them a way to grab a new time",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
240,
1440
],
"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: $('Tidy up the no-show details').item.json.contactId, message: 'Hey ' + $('Tidy up the no-show details').item.json.firstName + ', this is Blue Summit. Sorry we missed each other today. Want to grab a new time? Book here: https://blue-summit.example.com/book' }) }}",
"options": {}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"id": "sc-task",
"name": "Give the office a call task for tomorrow",
"type": "n8n-nodes-base.highLevel",
"typeVersion": 2,
"position": [
460,
1440
],
"parameters": {
"resource": "task",
"operation": "create",
"contactId": "={{ $('Tidy up the no-show details').item.json.contactId }}",
"title": "=Call {{ $('Tidy up the no-show details').item.json.fullName }} if they have not rebooked",
"dueDate": "={{ $now.plus({ days: 1 }).toISO() }}",
"additionalFields": {
"description": "=They missed today's visit and got the rebook text. If there is no new appointment on the calendar, give them a call at {{ $('Tidy up the no-show details').item.json.phone }}."
}
},
"credentials": {
"highLevelOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"id": "sc-slack",
"name": "Let the sales team know",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [
680,
1440
],
"parameters": {
"resource": "message",
"operation": "post",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "name",
"value": "#sales-team"
},
"text": "=No-show today: {{ $('Tidy up the no-show details').item.json.fullName }} ({{ $('Tidy up the no-show details').item.json.phone }}). The rebook text went out and the office has a call task for tomorrow in case they do not grab a new time.",
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
}
},
{
"id": "sc-respond",
"name": "Tell the calendar it is handled",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
900,
1440
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ ok: true, message: 'No-show handled. The rebook text went out and the office has a follow up task.', phone: $('Tidy up the no-show details').item.json.phone }) }}",
"options": {}
}
}
],
"connections": {
"A new appointment gets booked": {
"main": [
[
{
"node": "Tidy up the booking details",
"type": "main",
"index": 0
}
]
]
},
"Tidy up the booking details": {
"main": [
[
{
"node": "Save the customer in GoHighLevel",
"type": "main",
"index": 0
}
]
]
},
"Save the customer in GoHighLevel": {
"main": [
[
{
"node": "Text the booking confirmation",
"type": "main",
"index": 0
}
]
]
},
"Text the booking confirmation": {
"main": [
[
{
"node": "Tell the calendar it is booked",
"type": "main",
"index": 0
}
]
]
},
"Check every fifteen minutes": {
"main": [
[
{
"node": "Get everyone with an appointment on the books",
"type": "main",
"index": 0
}
]
]
},
"Get everyone with an appointment on the books": {
"main": [
[
{
"node": "Work out who is due a text now",
"type": "main",
"index": 0
}
]
]
},
"Work out who is due a text now": {
"main": [
[
{
"node": "Which reminder is due?",
"type": "main",
"index": 0
}
]
]
},
"Which reminder is due?": {
"main": [
[
{
"node": "Text the day before reminder",
"type": "main",
"index": 0
}
],
[
{
"node": "Text the morning of heads up",
"type": "main",
"index": 0
}
]
]
},
"Text the day before reminder": {
"main": [
[
{
"node": "Mark the reminder as sent",
"type": "main",
"index": 0
}
]
]
},
"Text the morning of heads up": {
"main": [
[
{
"node": "Mark the reminder as sent",
"type": "main",
"index": 0
}
]
]
},
"The calendar marks a no-show": {
"main": [
[
{
"node": "Tidy up the no-show details",
"type": "main",
"index": 0
}
]
]
},
"Tidy up the no-show details": {
"main": [
[
{
"node": "Tag them as a no-show",
"type": "main",
"index": 0
}
]
]
},
"Tag them as a no-show": {
"main": [
[
{
"node": "Text them a way to grab a new time",
"type": "main",
"index": 0
}
]
]
},
"Text them a way to grab a new time": {
"main": [
[
{
"node": "Give the office a call task for tomorrow",
"type": "main",
"index": 0
}
]
]
},
"Give the office a call task for tomorrow": {
"main": [
[
{
"node": "Let the sales team know",
"type": "main",
"index": 0
}
]
]
},
"Let the sales team know": {
"main": [
[
{
"node": "Tell the calendar it is handled",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
highLevelOAuth2ApihttpHeaderAuthslackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Appointment-Reminders-Noshow. Uses httpRequest, highLevel, slack. Webhook trigger; 22 nodes.
Source: https://github.com/mcruz1799/automation-examples/blob/main/claude-code/02-automation-file-factory/skills/n8n/section-templates/appointment-reminders-noshow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
HR teams, IT Operations, and System Administrators managing employee onboarding at scale. It’s perfect if you use Odoo 18 to trigger account requests and need Redmine + GitLab accounts created instant
This workflow is a complete, production-ready solution for recovering abandoned carts in Shopify stores using a multi-channel, multi-touch approach. It automates personalized follow-ups via Email, SMS
Backbrief: transcripts (Zoom webhook -> Slack + vault). Uses httpRequest, slack. Webhook trigger; 52 nodes.
This workflow automates end-to-end research analysis by coordinating multiple AI models—including NVIDIA NIM (Llama), OpenAI GPT-4, and Claude to analyze uploaded documents, extract insights, and gene
Are you tired of the repetitive dance between git push, creating a pull request in GitHub, updating the corresponding task in JIRA, and then manually notifying your team in Slack, or Notion?