AutomationFlowsData & Sheets › Rodopi Dent - Create Booking

Rodopi Dent - Create Booking

Rodopi Dent - Create Booking. Uses googleSheets, twilio. Webhook trigger; 11 nodes.

Webhook trigger★★★★☆ complexity11 nodesGoogle SheetsTwilio
Data & Sheets Trigger: Webhook Nodes: 11 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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.

Pro

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 →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

n8n instance (self-hosted or cloud) Google Sheets account Twilio account with SMS-enabled phone number

Google Sheets, Twilio
Data & Sheets

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

Email Send, Google Sheets, HubSpot +3
Data & Sheets

16 - Abandoned Cart WA Voice Note. Uses httpRequest, awsS3, twilio, googleSheets. Webhook trigger; 9 nodes.

HTTP Request, AWS S3, Twilio +1
Data & Sheets

20 - Clinic Missed Call WA Recovery. Uses googleSheets, httpRequest, twilio. Webhook trigger; 8 nodes.

Google Sheets, HTTP Request, Twilio
Data & Sheets

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.

HTTP Request, Gmail, Twilio +3