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 →
{
"name": "Rodopi Dent - Create Booking",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "booking-webhook",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "webhook-booking",
"name": "Webhook - Create Booking",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
]
},
{
"parameters": {
"jsCode": "// Validate incoming booking data\nconst body = $input.first().json.body;\n\nconst required = ['patientName', 'patientPhone', 'date', 'startTime'];\nconst missing = required.filter(field => !body[field]);\n\nif (missing.length > 0) {\n return [{\n json: {\n success: false,\n error: `\u041b\u0438\u043f\u0441\u0432\u0430\u0449\u0438 \u043f\u043e\u043b\u0435\u0442\u0430: ${missing.join(', ')}`,\n valid: false\n }\n }];\n}\n\n// Validate phone number (Bulgarian format)\nconst phone = body.patientPhone.replace(/\\s/g, '');\nconst phoneRegex = /^(\\+359|0)[0-9]{9}$/;\nif (!phoneRegex.test(phone)) {\n return [{\n json: {\n success: false,\n error: '\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435\u043d \u043d\u043e\u043c\u0435\u0440',\n valid: false\n }\n }];\n}\n\n// Prepare data for conflict check\nreturn [{\n json: {\n patientName: body.patientName.trim(),\n patientPhone: phone,\n date: body.date,\n startTime: body.startTime,\n duration: parseInt(body.duration) || 30,\n reason: body.reason || '',\n valid: true\n }\n}];"
},
"id": "validate",
"name": "Validate Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
0
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.valid }}",
"value2": true
}
]
}
},
"id": "if-valid",
"name": "Is Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
440,
0
]
},
{
"parameters": {
"operation": "read",
"documentId": {
"__rl": true,
"mode": "id",
"value": "1hv4XAfHhScA40Bm1kQ3I-Ih4SJuCBpOJxTOYDNb167g"
},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Appointments"
},
"options": {
"returnAllMatches": true
}
},
"id": "sheets-read",
"name": "Get Existing Appointments",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
660,
-100
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Get the new booking request\nconst booking = $('Validate Input').first().json;\nconst newDate = booking.date;\nconst newStartTime = booking.startTime;\nconst newDuration = booking.duration;\n\n// Helper function to convert time to minutes\nfunction timeToMinutes(time) {\n const [hours, minutes] = time.split(':').map(Number);\n return hours * 60 + minutes;\n}\n\nconst newStart = timeToMinutes(newStartTime);\nconst newEnd = newStart + newDuration;\n\n// Get existing appointments for the same date\nconst appointments = $('Get Existing Appointments').all();\n\n// Check for conflicts\nlet hasConflict = false;\nlet conflictWith = null;\n\nfor (const apt of appointments) {\n // Skip cancelled appointments\n if (apt.json.status === 'cancelled') continue;\n \n // Only check same date\n if (apt.json.date !== newDate) continue;\n \n const existingStart = timeToMinutes(apt.json.startTime);\n const existingDuration = parseInt(apt.json.duration) || 30;\n const existingEnd = existingStart + existingDuration;\n \n // Check for overlap:\n // Conflict if: newStart < existingEnd AND newEnd > existingStart\n if (newStart < existingEnd && newEnd > existingStart) {\n hasConflict = true;\n conflictWith = apt.json.startTime;\n break;\n }\n}\n\nif (hasConflict) {\n return [{\n json: {\n success: false,\n error: `\u0422\u043e\u0437\u0438 \u0447\u0430\u0441 \u0435 \u0432\u0435\u0447\u0435 \u0437\u0430\u0435\u0442 (\u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442 \u0441 ${conflictWith})`,\n conflict: true\n }\n }];\n}\n\n// No conflict - generate ID and prepare for save\nconst id = 'apt_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 8);\nconst now = new Date().toISOString();\n\nreturn [{\n json: {\n id: id,\n patientName: booking.patientName,\n patientPhone: booking.patientPhone,\n date: booking.date,\n startTime: booking.startTime,\n duration: booking.duration,\n reason: booking.reason,\n status: 'pending',\n createdAt: now,\n updatedAt: now,\n conflict: false\n }\n}];"
},
"id": "check-conflicts",
"name": "Check Conflicts",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
880,
-100
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.conflict }}",
"value2": false
}
]
}
},
"id": "if-no-conflict",
"name": "No Conflict?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1100,
-100
]
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"mode": "id",
"value": "1hv4XAfHhScA40Bm1kQ3I-Ih4SJuCBpOJxTOYDNb167g"
},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Appointments"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"id": "={{ $json.id }}",
"patientName": "={{ $json.patientName }}",
"patientPhone": "={{ $json.patientPhone }}",
"date": "={{ $json.date }}",
"startTime": "={{ $json.startTime }}",
"duration": "={{ $json.duration }}",
"reason": "={{ $json.reason }}",
"status": "={{ $json.status }}",
"createdAt": "={{ $json.createdAt }}",
"updatedAt": "={{ $json.updatedAt }}"
}
},
"options": {}
},
"id": "sheets-append",
"name": "Save to Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1320,
-200
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={\n \"success\": true,\n \"message\": \"\u0427\u0430\u0441\u044a\u0442 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u043f\u0430\u0437\u0435\u043d!\",\n \"appointmentId\": \"{{ $json.id }}\",\n \"date\": \"{{ $json.date }}\",\n \"time\": \"{{ $json.startTime }}\"\n}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
}
]
}
}
},
"id": "response-success",
"name": "Respond Success",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1540,
-200
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={\n \"success\": false,\n \"error\": \"{{ $json.error }}\"\n}",
"options": {
"responseCode": "409",
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
}
]
}
}
},
"id": "response-conflict",
"name": "Respond Conflict",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1320,
0
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={\n \"success\": false,\n \"error\": \"{{ $json.error }}\"\n}",
"options": {
"responseCode": "400",
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
}
]
}
}
},
"id": "response-invalid",
"name": "Respond Invalid",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
660,
100
]
},
{
"parameters": {
"from": "",
"to": "={{ $('Check Conflicts').item.json.patientPhone }}",
"message": "=\u0417\u0434\u0440\u0430\u0432\u0435\u0439\u0442\u0435, {{ $('Check Conflicts').item.json.patientName }}!\n\n\u0412\u0430\u0448\u0438\u044f\u0442 \u0447\u0430\u0441 \u0432 \u0420\u043e\u0434\u043e\u043f\u0438 \u0414\u0435\u043d\u0442 \u0435 \u0437\u0430\u043f\u0430\u0437\u0435\u043d:\n\ud83d\udcc5 \u0414\u0430\u0442\u0430: {{ $('Check Conflicts').item.json.date }}\n\ud83d\udd50 \u0427\u0430\u0441: {{ $('Check Conflicts').item.json.startTime }}\n\u23f1\ufe0f \u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442: {{ $('Check Conflicts').item.json.duration }} \u043c\u0438\u043d.\n\n\u041e\u0447\u0430\u043a\u0432\u0430\u043c\u0435 \u0412\u0438!\n\u0420\u043e\u0434\u043e\u043f\u0438 \u0414\u0435\u043d\u0442"
},
"id": "twilio-sms",
"name": "Send SMS Confirmation",
"type": "n8n-nodes-base.twilio",
"typeVersion": 1,
"position": [
1540,
-100
],
"credentials": {
"twilioApi": {
"name": "<your credential>"
}
},
"continueOnFail": true
}
],
"connections": {
"Webhook - Create Booking": {
"main": [
[
{
"node": "Validate Input",
"type": "main",
"index": 0
}
]
]
},
"Validate Input": {
"main": [
[
{
"node": "Is Valid?",
"type": "main",
"index": 0
}
]
]
},
"Is Valid?": {
"main": [
[
{
"node": "Get Existing Appointments",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Invalid",
"type": "main",
"index": 0
}
]
]
},
"Get Existing Appointments": {
"main": [
[
{
"node": "Check Conflicts",
"type": "main",
"index": 0
}
]
]
},
"Check Conflicts": {
"main": [
[
{
"node": "No Conflict?",
"type": "main",
"index": 0
}
]
]
},
"No Conflict?": {
"main": [
[
{
"node": "Save to Sheets",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Conflict",
"type": "main",
"index": 0
}
]
]
},
"Save to Sheets": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
},
{
"node": "Send SMS Confirmation",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"tags": [
{
"name": "Rodopi Dent"
}
]
}
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.
googleSheetsOAuth2ApitwilioApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Rodopi Dent - Create Booking. Uses googleSheets, twilio. Webhook trigger; 11 nodes.
Source: https://github.com/Georgi-Piskov/RODOPI-DENT/blob/f7724f1a121275ef2b565a53bb3bce44465b0933/n8n-workflows/02-booking-webhook.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.
n8n instance (self-hosted or cloud) Google Sheets account Twilio account with SMS-enabled phone number
Are you tired of manually entering open house visitor information into your CRM? Losing hot leads because you didn't follow up fast enough? This powerful n8n workflow automatically syncs every SignSna
16 - Abandoned Cart WA Voice Note. Uses httpRequest, awsS3, twilio, googleSheets. Webhook trigger; 9 nodes.
20 - Clinic Missed Call WA Recovery. Uses googleSheets, httpRequest, twilio. Webhook trigger; 8 nodes.
This template is ideal for solo store owners, eCommerce marketers, automation beginners, or anyone using Shopify and Gmail who wants to recover lost revenue without coding.