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 - Get Available Slots",
"nodes": [
{
"parameters": {
"httpMethod": "GET",
"path": "slots-webhook",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "webhook-slots",
"name": "Webhook - Get Slots",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
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 Appointments",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
220,
0
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Get date from query parameter\nconst queryDate = $('Webhook - Get Slots').first().json.query.date;\n\nif (!queryDate) {\n return [{ json: { error: 'Date parameter is required', slots: [] } }];\n}\n\n// Parse the requested date\nconst requestedDate = new Date(queryDate);\nconst dayOfWeek = requestedDate.getDay();\n\n// Check if it's a working day (Monday-Friday = 1-5)\nconst workingDays = [1, 2, 3, 4, 5];\nif (!workingDays.includes(dayOfWeek)) {\n return [{ json: { date: queryDate, slots: [], message: '\u041d\u0435\u0440\u0430\u0431\u043e\u0442\u0435\u043d \u0434\u0435\u043d' } }];\n}\n\n// Working hours configuration\nconst workingHours = {\n morning: { start: '09:00', end: '12:00' },\n afternoon: { start: '13:30', end: '17:00' }\n};\n\n// SLOT_INTERVAL = 30 minutes\nconst slotInterval = 30;\n// Default duration for PENDING appointments (before doctor confirms)\nconst pendingDefaultDuration = 30;\n// Pending expiration time in hours\nconst pendingExpirationHours = 2;\n\n// Helper functions\nfunction timeToMinutes(time) {\n const [hours, minutes] = time.split(':').map(Number);\n return hours * 60 + minutes;\n}\n\nfunction minutesToTime(mins) {\n const hours = Math.floor(mins / 60);\n const minutes = mins % 60;\n return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;\n}\n\n// Generate all possible 30-minute slots\nconst allSlots = [];\n\n// Morning slots (09:00 - 12:00)\nlet current = timeToMinutes(workingHours.morning.start);\nconst morningEnd = timeToMinutes(workingHours.morning.end);\nwhile (current < morningEnd) {\n allSlots.push(minutesToTime(current));\n current += slotInterval;\n}\n\n// Afternoon slots (13:30 - 17:00)\ncurrent = timeToMinutes(workingHours.afternoon.start);\nconst afternoonEnd = timeToMinutes(workingHours.afternoon.end);\nwhile (current < afternoonEnd) {\n allSlots.push(minutesToTime(current));\n current += slotInterval;\n}\n\n// Get all appointments\nconst appointments = $('Get Appointments').all();\nconst now = new Date();\n\n// Build list of blocked time ranges\nconst blockedRanges = [];\nconst pendingSlots = [];\nconst confirmedSlots = [];\n\nappointments\n .filter(apt => {\n const aptDate = apt.json.date;\n const status = apt.json.status;\n \n // Skip cancelled\n if (status === 'cancelled') return false;\n \n // Only this date\n if (aptDate !== queryDate) return false;\n \n // Check if pending has expired\n if (status === 'pending') {\n const createdAt = new Date(apt.json.createdAt);\n const expiresAt = new Date(createdAt.getTime() + pendingExpirationHours * 60 * 60 * 1000);\n if (now > expiresAt) {\n // Pending expired - don't block\n return false;\n }\n }\n \n return true;\n })\n .forEach(apt => {\n const startMinutes = timeToMinutes(apt.json.startTime);\n const status = apt.json.status;\n \n // Pending uses default 30 min, confirmed/completed uses actual duration\n let duration;\n if (status === 'pending') {\n duration = pendingDefaultDuration;\n pendingSlots.push({\n startTime: apt.json.startTime,\n patientName: apt.json.patientName,\n reason: apt.json.reason\n });\n } else {\n duration = parseInt(apt.json.duration) || 30;\n confirmedSlots.push({\n startTime: apt.json.startTime,\n duration: duration,\n patientName: apt.json.patientName,\n status: status\n });\n }\n \n const endMinutes = startMinutes + duration;\n blockedRanges.push({ \n start: startMinutes, \n end: endMinutes,\n status: status\n });\n });\n\n// Check if a slot conflicts with any blocked range\nfunction isSlotBlocked(slotTime) {\n const slotMinutes = timeToMinutes(slotTime);\n return blockedRanges.some(range => {\n return slotMinutes >= range.start && slotMinutes < range.end;\n });\n}\n\n// Filter out blocked slots\nconst availableSlots = allSlots.filter(slot => !isSlotBlocked(slot));\n\nreturn [{\n json: {\n date: queryDate,\n slots: availableSlots,\n totalSlots: allSlots.length,\n pendingCount: pendingSlots.length,\n confirmedCount: confirmedSlots.length,\n pendingSlots: pendingSlots,\n confirmedSlots: confirmedSlots\n }\n}];"
},
"id": "code-filter",
"name": "Calculate Available Slots",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
440,
0
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
}
]
}
}
},
"id": "response",
"name": "Respond with Slots",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
660,
0
]
}
],
"connections": {
"Webhook - Get Slots": {
"main": [
[
{
"node": "Get Appointments",
"type": "main",
"index": 0
}
]
]
},
"Get Appointments": {
"main": [
[
{
"node": "Calculate Available Slots",
"type": "main",
"index": 0
}
]
]
},
"Calculate Available Slots": {
"main": [
[
{
"node": "Respond with Slots",
"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.
googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Rodopi Dent - Get Available Slots. Uses googleSheets. Webhook trigger; 4 nodes.
Source: https://github.com/Georgi-Piskov/RODOPI-DENT/blob/07217e5d65fd897a41528db70a4869149ca9a5b1/n8n-workflows/01-slots-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.
[SANTOBET] FLUXO TODO - BACKUP. Uses googleSheets, httpRequest, googleSheetsTrigger. Webhook trigger; 57 nodes.
FLUXO DISPARO DATA E HORA. Uses itemLists, googleSheets, httpRequest. Webhook trigger; 48 nodes.
This workflow allows you to accept online payments via YooKassa and log both orders and transactions in Google Sheets — all without writing a single line of code. It supports full payment flow: produc
Transform your n8n instance management with this advanced automation system featuring artificial intelligence-driven workflow selection. This template provides comprehensive maintenance operations wit
Nexus_v6(ล่าสุดจริงๆ)ล่าสุดไกไก. Uses googleSheets, httpRequest. Webhook trigger; 41 nodes.