AutomationFlowsSlack & Telegram › Public Booking Calendar with Google Calendar & Telegram

Public Booking Calendar with Google Calendar & Telegram

Original n8n title: Rodopi Dent - Public Booking (calendar)

Rodopi Dent - Public Booking (Calendar). Uses googleCalendar, telegram, httpRequest. Webhook trigger; 17 nodes.

Webhook trigger★★★★☆ complexity17 nodesGoogle CalendarTelegramHTTP Request
Slack & Telegram Trigger: Webhook Nodes: 17 Complexity: ★★★★☆ Added:

This workflow follows the Google Calendar → HTTP Request 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 →

Download .json
{
  "name": "Rodopi Dent - Public Booking (Calendar)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "public-booking",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "*"
        }
      },
      "id": "webhook",
      "name": "Webhook",
      "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      valid: false,\n      error: `\u041b\u0438\u043f\u0441\u0432\u0430\u0449\u0438 \u043f\u043e\u043b\u0435\u0442\u0430: ${missing.join(', ')}`\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      valid: 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. \u041c\u043e\u043b\u044f, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u0444\u043e\u0440\u043c\u0430\u0442: 0888123456 \u0438\u043b\u0438 +359888123456'\n    }\n  }];\n}\n\n// Validate date is in the future\nconst bookingDate = new Date(body.date + 'T' + body.startTime);\nconst now = new Date();\nif (bookingDate < now) {\n  return [{\n    json: {\n      valid: false,\n      error: '\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0437\u0430\u043f\u0430\u0437\u0438\u0442\u0435 \u0447\u0430\u0441 \u0432 \u043c\u0438\u043d\u0430\u043b\u043e\u0442\u043e'\n    }\n  }];\n}\n\n// Check if it's a working day\nconst dayOfWeek = new Date(body.date).getDay();\nif (dayOfWeek === 0 || dayOfWeek === 6) {\n  return [{\n    json: {\n      valid: false,\n      error: '\u0421\u044a\u0431\u043e\u0442\u0430 \u0438 \u043d\u0435\u0434\u0435\u043b\u044f \u0441\u0430 \u043f\u043e\u0447\u0438\u0432\u043d\u0438 \u0434\u043d\u0438'\n    }\n  }];\n}\n\n// Default duration is 30 minutes for new bookings (doctor will confirm actual duration)\nconst duration = 30;\n\nreturn [{\n  json: {\n    valid: true,\n    patientName: body.patientName.trim(),\n    patientPhone: phone,\n    date: body.date,\n    startTime: body.startTime,\n    duration: duration,\n    reason: (body.reason || '').trim(),\n    notes: (body.notes || '').trim()\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": "is-valid",
      "name": "Is Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: $json.error }) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "respond-invalid",
      "name": "Respond Invalid",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        660,
        -100
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "rodopi.dent@gmail.com"
        },
        "returnAll": true,
        "options": {
          "timeMax": "={{ $json.date }}T23:59:59+02:00",
          "timeMin": "={{ $json.date }}T00:00:00+02:00",
          "singleEvents": true
        }
      },
      "id": "get-calendar",
      "name": "Get Calendar Events",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.2,
      "position": [
        660,
        100
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Get the booking request\nconst booking = $('Validate Input').first().json;\nconst bookingDate = booking.date;\nconst bookingTime = booking.startTime;\n\n// Helper function to convert to Europe/Sofia timezone and get minutes\nfunction toSofiaMinutes(isoString) {\n  if (!isoString) return null;\n  const d = new Date(isoString);\n  const options = {\n    timeZone: 'Europe/Sofia',\n    hour: '2-digit',\n    minute: '2-digit',\n    hour12: false\n  };\n  const formatter = new Intl.DateTimeFormat('en-CA', options);\n  const parts = formatter.formatToParts(d);\n  const hour = parseInt(parts.find(p => p.type === 'hour')?.value || '0');\n  const minute = parseInt(parts.find(p => p.type === 'minute')?.value || '0');\n  return hour * 60 + minute;\n}\n\nfunction timeToMinutes(time) {\n  const [hours, minutes] = time.split(':').map(Number);\n  return hours * 60 + minutes;\n}\n\nconst bookingStart = timeToMinutes(bookingTime);\n\n// Get existing calendar events from Get Calendar Events node\nconst calendarEvents = $('Get Calendar Events').all();\n\n// Check for exact same start time AND calculate max available minutes\nlet hasExactConflict = false;\nlet conflictWith = null;\nlet nextEventStart = 18 * 60; // Default: end of working day (18:00)\n\nfor (const item of calendarEvents) {\n  const event = item.json;\n  \n  // Skip cancelled events\n  if (event.status === 'cancelled') continue;\n  \n  let existingStart = null;\n  \n  if (event.start?.dateTime) {\n    // CONVERT TO SOFIA TIMEZONE\n    existingStart = toSofiaMinutes(event.start.dateTime);\n  } else if (event.start?.date) {\n    // All-day event blocks entire day\n    hasExactConflict = true;\n    conflictWith = '\u0446\u0435\u043b\u043e\u0434\u043d\u0435\u0432\u043d\u043e \u0441\u044a\u0431\u0438\u0442\u0438\u0435';\n    break;\n  }\n  \n  if (existingStart === null) continue;\n  \n  // Check for exact same start time\n  if (bookingStart === existingStart) {\n    hasExactConflict = true;\n    const startHour = Math.floor(existingStart / 60);\n    const startMin = existingStart % 60;\n    conflictWith = `${startHour.toString().padStart(2, '0')}:${startMin.toString().padStart(2, '0')}`;\n    break;\n  }\n  \n  // Find the earliest event that starts AFTER our booking start time\n  if (existingStart > bookingStart && existingStart < nextEventStart) {\n    nextEventStart = existingStart;\n  }\n}\n\nif (hasExactConflict) {\n  return [{\n    json: {\n      conflict: true,\n      error: `\u0422\u043e\u0437\u0438 \u0447\u0430\u0441 (${bookingTime}) \u0432\u0435\u0447\u0435 \u0435 \u0437\u0430\u0435\u0442. \u041c\u043e\u043b\u044f \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0440\u0443\u0433 \u0447\u0430\u0441.`\n    }\n  }];\n}\n\n// Calculate max available minutes until next event\nconst maxAvailableMinutes = nextEventStart - bookingStart;\n\n// Determine which buttons to show\nconst can30 = maxAvailableMinutes >= 30;\nconst can60 = maxAvailableMinutes >= 60;\n\nconst id = 'apt_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 8);\n\nreturn [{\n  json: {\n    conflict: false,\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    notes: booking.notes,\n    maxAvailableMinutes: maxAvailableMinutes,\n    can30: can30,\n    can60: can60\n  }\n}];"
      },
      "id": "check-conflict",
      "name": "Check Conflict",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.conflict }}",
              "value2": true
            }
          ]
        }
      },
      "id": "has-conflict",
      "name": "Has Conflict?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1100,
        100
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, error: $json.error }) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "respond-conflict",
      "name": "Respond Conflict",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1320,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Prepare calendar event data\nconst booking = $json;\n\n// Calculate end time\nconst [hours, minutes] = booking.startTime.split(':').map(Number);\nconst startMinutes = hours * 60 + minutes;\nconst endMinutes = startMinutes + booking.duration;\nconst endHours = Math.floor(endMinutes / 60);\nconst endMins = endMinutes % 60;\nconst endTime = `${endHours.toString().padStart(2, '0')}:${endMins.toString().padStart(2, '0')}`;\n\n// Build description with structured data\nlet description = `\ud83d\udcde \u0422\u0435\u043b: ${booking.patientPhone}\\n`;\nif (booking.reason) {\n  description += `\ud83d\udccb \u041f\u0440\u0438\u0447\u0438\u043d\u0430: ${booking.reason}\\n`;\n}\nif (booking.notes) {\n  description += `\ud83d\udcdd \u0411\u0435\u043b\u0435\u0436\u043a\u0438: ${booking.notes}\\n`;\n}\ndescription += `\\n\u23f3 \u0421\u0442\u0430\u0442\u0443\u0441: \u0427\u0410\u041a\u0410\u0429 (pending)\\n`;\ndescription += `\ud83c\udd94 ID: ${booking.id}\\n`;\ndescription += `\ud83d\udcc5 \u0421\u044a\u0437\u0434\u0430\u0434\u0435\u043d: ${new Date().toLocaleString('bg-BG')}`;\n\nreturn [{\n  json: {\n    summary: `\u23f3 ${booking.patientName}`,\n    description: description,\n    startDateTime: `${booking.date}T${booking.startTime}:00`,\n    endDateTime: `${booking.date}T${endTime}:00`,\n    colorId: '5',  // Yellow for pending\n    id: booking.id,\n    patientName: booking.patientName,\n    patientPhone: booking.patientPhone\n  }\n}];"
      },
      "id": "prepare-event",
      "name": "Prepare Event Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        200
      ]
    },
    {
      "parameters": {
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "rodopi.dent@gmail.com"
        },
        "start": "={{ $json.startDateTime }}",
        "end": "={{ $json.endDateTime }}",
        "additionalFields": {
          "summary": "={{ $json.summary }}",
          "description": "={{ $json.description }}",
          "colorId": "={{ $json.colorId }}"
        }
      },
      "id": "create-calendar-event",
      "name": "Create Calendar Event",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.2,
      "position": [
        1540,
        200
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Prepare success response\nconst calendarEvent = $input.first().json;\nconst preparedData = $('Prepare Event Data').first().json;\n\nreturn [{\n  json: {\n    success: true,\n    message: '\u0427\u0430\u0441\u044a\u0442 \u0435 \u0437\u0430\u043f\u0430\u0437\u0435\u043d \u0443\u0441\u043f\u0435\u0448\u043d\u043e! \u041e\u0447\u0430\u043a\u0432\u0430\u0439\u0442\u0435 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435.',\n    booking: {\n      id: preparedData.id,\n      googleEventId: calendarEvent.id,\n      patientName: preparedData.patientName,\n      patientPhone: preparedData.patientPhone,\n      date: preparedData.startDateTime.split('T')[0],\n      startTime: preparedData.startDateTime.split('T')[1].slice(0, 5),\n      status: 'pending'\n    }\n  }\n}];"
      },
      "id": "prepare-response",
      "name": "Prepare Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        200
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "respond-success",
      "name": "Respond Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1980,
        200
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $('Check Conflict').first().json.can60 }}",
              "value2": true
            }
          ]
        }
      },
      "id": "can-60-check",
      "name": "Can 60 min?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1760,
        400
      ]
    },
    {
      "parameters": {
        "chatId": "2146283697",
        "text": "=\ud83e\uddb7 *\u041d\u043e\u0432\u0430 \u0437\u0430\u044f\u0432\u043a\u0430 \u0437\u0430 \u0447\u0430\u0441!*\n\n\ud83d\udc64 \u041f\u0430\u0446\u0438\u0435\u043d\u0442: {{ $('Prepare Event Data').first().json.patientName }}\n\ud83d\udcde \u0422\u0435\u043b\u0435\u0444\u043e\u043d: {{ $('Prepare Event Data').first().json.patientPhone }}\n\ud83d\udcc5 \u0414\u0430\u0442\u0430: {{ $('Prepare Event Data').first().json.startDateTime.split('T')[0] }}\n\ud83d\udd50 \u0427\u0430\u0441: {{ $('Prepare Event Data').first().json.startDateTime.split('T')[1].slice(0, 5) }}\n\ud83d\udccb \u041f\u0440\u0438\u0447\u0438\u043d\u0430: {{ $('Validate Input').first().json.reason || '\u041d\u0435 \u0435 \u043f\u043e\u0441\u043e\u0447\u0435\u043d\u0430' }}\n\u23f1\ufe0f \u0421\u0432\u043e\u0431\u043e\u0434\u043d\u043e \u0432\u0440\u0435\u043c\u0435: {{ $('Check Conflict').first().json.maxAvailableMinutes }} \u043c\u0438\u043d.\n\n\u23f3 *\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442:*",
        "additionalFields": {
          "parse_mode": "Markdown",
          "replyMarkup": "inlineKeyboard",
          "inlineKeyboard": {
            "rows": [
              {
                "row": [
                  {
                    "text": "30 \u043c\u0438\u043d",
                    "callbackData": "PLACEHOLDER_30"
                  },
                  {
                    "text": "60 \u043c\u0438\u043d",
                    "callbackData": "PLACEHOLDER_60"
                  }
                ]
              },
              {
                "row": [
                  {
                    "text": "\u274c \u041e\u0442\u043a\u0430\u0436\u0438",
                    "callbackData": "PLACEHOLDER_CANCEL"
                  }
                ]
              }
            ]
          }
        }
      },
      "id": "telegram-full",
      "name": "Telegram Full Options",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1980,
        350
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "notes": "CALLBACK DATA:\n30 \u043c\u0438\u043d: confirm_30_{{ $('Prepare Event Data').first().json.id }}\n60 \u043c\u0438\u043d: confirm_60_{{ $('Prepare Event Data').first().json.id }}\n\u041e\u0442\u043a\u0430\u0436\u0438: cancel_{{ $('Prepare Event Data').first().json.id }}"
    },
    {
      "parameters": {
        "chatId": "2146283697",
        "text": "=\ud83e\uddb7 *\u041d\u043e\u0432\u0430 \u0437\u0430\u044f\u0432\u043a\u0430 \u0437\u0430 \u0447\u0430\u0441!*\n\n\ud83d\udc64 \u041f\u0430\u0446\u0438\u0435\u043d\u0442: {{ $('Prepare Event Data').first().json.patientName }}\n\ud83d\udcde \u0422\u0435\u043b\u0435\u0444\u043e\u043d: {{ $('Prepare Event Data').first().json.patientPhone }}\n\ud83d\udcc5 \u0414\u0430\u0442\u0430: {{ $('Prepare Event Data').first().json.startDateTime.split('T')[0] }}\n\ud83d\udd50 \u0427\u0430\u0441: {{ $('Prepare Event Data').first().json.startDateTime.split('T')[1].slice(0, 5) }}\n\ud83d\udccb \u041f\u0440\u0438\u0447\u0438\u043d\u0430: {{ $('Validate Input').first().json.reason || '\u041d\u0435 \u0435 \u043f\u043e\u0441\u043e\u0447\u0435\u043d\u0430' }}\n\u26a0\ufe0f *\u0421\u0432\u043e\u0431\u043e\u0434\u043d\u043e \u0432\u0440\u0435\u043c\u0435: \u0441\u0430\u043c\u043e {{ $('Check Conflict').first().json.maxAvailableMinutes }} \u043c\u0438\u043d!*\n\n\u23f3 *\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435:*",
        "additionalFields": {
          "parse_mode": "Markdown",
          "replyMarkup": "inlineKeyboard",
          "inlineKeyboard": {
            "rows": [
              {
                "row": [
                  {
                    "text": "30 \u043c\u0438\u043d",
                    "callbackData": "PLACEHOLDER_30"
                  },
                  {
                    "text": "\ud83d\udcde \u0417\u0430 \u043e\u0431\u0430\u0436\u0434\u0430\u043d\u0435",
                    "callbackData": "PLACEHOLDER_CALLBACK"
                  }
                ]
              },
              {
                "row": [
                  {
                    "text": "\u274c \u041e\u0442\u043a\u0430\u0436\u0438",
                    "callbackData": "PLACEHOLDER_CANCEL"
                  }
                ]
              }
            ]
          }
        }
      },
      "id": "telegram-limited",
      "name": "Telegram Limited (30 only)",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1980,
        500
      ],
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "notes": "CALLBACK DATA:\n30 \u043c\u0438\u043d: confirm_30_{{ $('Prepare Event Data').first().json.id }}\n\u0417\u0430 \u043e\u0431\u0430\u0436\u0434\u0430\u043d\u0435: callback_{{ $('Prepare Event Data').first().json.id }}\n\u041e\u0442\u043a\u0430\u0436\u0438: cancel_{{ $('Prepare Event Data').first().json.id }}"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://n8n.simeontsvetanovn8nworkflows.site/webhook/send-sms",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"phone\": \"{{ $('Prepare Event Data').first().json.patientPhone }}\",\n  \"template\": \"booking_received\",\n  \"date\": \"{{ $('Prepare Event Data').first().json.startDateTime.split('T')[0] }}\",\n  \"time\": \"{{ $('Prepare Event Data').first().json.startDateTime.split('T')[1].slice(0, 5) }}\",\n  \"patientName\": \"{{ $('Prepare Event Data').first().json.patientName }}\"\n}",
        "options": {}
      },
      "id": "send-sms-received",
      "name": "Send SMS to Patient",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        500
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://n8n.simeontsvetanovn8nworkflows.site/webhook/patients-upsert",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"name\": \"{{ $('Prepare Event Data').first().json.patientName }}\",\n  \"phone\": \"{{ $('Prepare Event Data').first().json.patientPhone }}\"\n}",
        "options": {}
      },
      "id": "save-patient",
      "name": "Save Patient to Sheet",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        650
      ],
      "continueOnFail": true
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Validate Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Input": {
      "main": [
        [
          {
            "node": "Is Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Valid?": {
      "main": [
        [
          {
            "node": "Respond Invalid",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Calendar Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Calendar Events": {
      "main": [
        [
          {
            "node": "Check Conflict",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Conflict": {
      "main": [
        [
          {
            "node": "Has Conflict?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Conflict?": {
      "main": [
        [
          {
            "node": "Respond Conflict",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Event Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Event Data": {
      "main": [
        [
          {
            "node": "Create Calendar Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Calendar Event": {
      "main": [
        [
          {
            "node": "Prepare Response",
            "type": "main",
            "index": 0
          },
          {
            "node": "Can 60 min?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send SMS to Patient",
            "type": "main",
            "index": 0
          },
          {
            "node": "Save Patient to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Can 60 min?": {
      "main": [
        [
          {
            "node": "Telegram Full Options",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram Limited (30 only)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Response": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}

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 - Public Booking (Calendar). Uses googleCalendar, telegram, httpRequest. Webhook trigger; 17 nodes.

Source: https://github.com/Georgi-Piskov/RODOPI-DENT/blob/f071a84326ea5adf54e4eb20a2ba3e34aac5b728/n8n-workflows/16-public-booking.json — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

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

Slack & Telegram

qualiopi. Uses airtable, telegram, emailSend, httpRequest. Webhook trigger; 51 nodes.

Airtable, Telegram, Email Send +3
Slack & Telegram

PsyCardv2. Uses executeCommand, telegram, readBinaryFile, googleDrive. Webhook trigger; 41 nodes.

Execute Command, Telegram, Read Binary File +2
Slack & Telegram

[](https://www.linkedin.com/in/mosaab-yassir-lafrimi/)[](https://t.me/joevenner)

HTTP Request, Redis, S3 +1
Slack & Telegram

How it works • Webhook triggers from content creation system in Airtable • Downloads media (images/videos) from Airtable URLs • Uploads media to Postiz cloud storage • Schedules or publishes content a

Airtable, Telegram, HTTP Request
Slack & Telegram

clients kept booking meetings during my prayer times. i'd either miss a prayer or scramble to reschedule. the problem wasn't the clients — it was that my calendar had no blocked windows for salah. i n

Telegram Trigger, HTTP Request, Google Calendar +3