AutomationFlowsData & Sheets › Rodopi Dent - Patients Backfill From Calendar

Rodopi Dent - Patients Backfill From Calendar

Rodopi Dent - Patients Backfill from Calendar. Uses googleCalendar, googleSheets. Event-driven trigger; 10 nodes.

Event trigger★★★★☆ complexity10 nodesGoogle CalendarGoogle Sheets
Data & Sheets Trigger: Event Nodes: 10 Complexity: ★★★★☆ Added:

This workflow follows the Google Calendar → Google Sheets 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 - Patients Backfill from Calendar",
  "nodes": [
    {
      "parameters": {},
      "id": "manual-trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Date range: 1 year back to 3 months forward (to catch future appointments)\nconst now = new Date();\nconst oneYearAgo = new Date();\noneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);\nconst threeMonthsAhead = new Date();\nthreeMonthsAhead.setMonth(threeMonthsAhead.getMonth() + 3);\n\nreturn [{\n  json: {\n    startDate: oneYearAgo.toISOString().split('T')[0] + 'T00:00:00+02:00',\n    endDate: threeMonthsAhead.toISOString().split('T')[0] + 'T23:59:59+02:00'\n  }\n}];"
      },
      "id": "set-date-range",
      "name": "Set Date Range",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "rodopi.dent@gmail.com"
        },
        "returnAll": true,
        "options": {
          "timeMax": "={{ $json.endDate }}",
          "timeMin": "={{ $json.startDate }}",
          "singleEvents": true,
          "orderBy": "startTime"
        }
      },
      "id": "get-calendar-events",
      "name": "Get All Calendar Events",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.2,
      "position": [
        440,
        0
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Extract patient name + phone from all calendar events\nconst events = $input.all();\nconst patients = new Map(); // phoneKey -> { name, phone }\n\n// Normalize phone to +359 international format\nfunction toInternational(p) {\n  let phone = String(p || '').replace(/[\\s()\\-.]/g, '');\n  if (phone.startsWith('00359')) phone = '+359' + phone.slice(5);\n  else if (phone.startsWith('0') && phone.length >= 10) phone = '+359' + phone.slice(1);\n  else if (/^[89]\\d{8}$/.test(phone)) phone = '+359' + phone;\n  else if (!phone.startsWith('+') && phone.length >= 9) phone = '+359' + phone;\n  return phone;\n}\n\n// Extract core 9-digit number for deduplication\nfunction phoneKey(p) {\n  return String(p || '').replace(/[\\s()\\-.+]/g, '').replace(/^359/, '').replace(/^0/, '');\n}\n\nfor (const item of events) {\n  const event = item.json;\n  const summary = (event.summary || '').trim();\n  // Strip any HTML tags from description\n  const description = (event.description || '').replace(/<[^>]*>/g, ' ');\n  \n  // Skip all-day events, events without title\n  if (!summary || event.start?.date) continue;\n  \n  // Skip blocked/system events\n  const lower = summary.toLowerCase();\n  if (lower.includes('\u0431\u043b\u043e\u043a\u0438\u0440\u0430\u043d') || lower.includes('\u043f\u043e\u0447\u0438\u0432\u043a\u0430') || lower.includes('blocked') || lower.includes('block')) continue;\n  \n  // Extract phone from description - multiple patterns from most to least specific\n  let phone = '';\n  const phonePatterns = [\n    /(?:\ud83d\udcde\\s*)?(?:\u0442\u0435\u043b|phone|tel)[.:\\s]+([+\\d\\s()\\-]{7,})/i,\n    /(?:\ud83d\udcf1\\s*)([+\\d\\s()\\-]{7,})/,\n    /((?:\\+359|00359)\\d{8,9})/,\n    /(0[89]\\d{7,8})/,\n    /\\b(\\d{9,10})\\b/\n  ];\n  \n  for (const pattern of phonePatterns) {\n    const match = description.match(pattern);\n    if (match) {\n      phone = match[1].replace(/[\\s()\\-]/g, '').trim();\n      break;\n    }\n  }\n  \n  if (!phone || phone.length < 7) continue;\n  \n  // Store in international format\n  const intlPhone = toInternational(phone);\n  const key = phoneKey(phone);\n  \n  // Keep first occurrence of each patient\n  if (!patients.has(key)) {\n    patients.set(key, {\n      name: summary,\n      phone: intlPhone\n    });\n  }\n}\n\nconst patientList = Array.from(patients.values());\n\nreturn [{\n  json: {\n    extractedCount: patientList.length,\n    totalEvents: events.length,\n    patients: patientList\n  }\n}];"
      },
      "id": "extract-patients",
      "name": "Extract Patients from Events",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1hv4XAfHhScA40Bm1kQ3I-Ih4SJuCBpOJxTOYDNb167g"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Patients"
        },
        "options": {
          "returnAllMatches": true
        }
      },
      "id": "read-existing-patients",
      "name": "Read Existing Patients",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        660,
        200
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "chooseBranch",
        "output": "wait"
      },
      "id": "wait-for-both",
      "name": "Wait for Both",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        880,
        100
      ]
    },
    {
      "parameters": {
        "jsCode": "// Compare extracted patients with existing and find new ones\nconst extracted = $('Extract Patients from Events').first().json;\nconst existingRows = $('Read Existing Patients').all().map(item => item.json);\n\n// Core 9-digit number for comparison\nfunction phoneKey(p) {\n  return String(p || '').replace(/[\\s()\\-.+]/g, '').replace(/^359/, '').replace(/^0/, '');\n}\n\n// Build sets for lookup\nconst existingPhoneKeys = new Set();\nconst existingNames = new Set();\n\nexistingRows.forEach(p => {\n  if (p.phone) existingPhoneKeys.add(phoneKey(p.phone));\n  if (p.name) existingNames.add(p.name.toLowerCase().trim());\n});\n\n// Filter out already existing patients (by phone OR by name)\nconst newPatients = extracted.patients.filter(p => {\n  const key = phoneKey(p.phone);\n  const normName = p.name.toLowerCase().trim();\n  return !existingPhoneKeys.has(key) && !existingNames.has(normName);\n});\n\nif (newPatients.length === 0) {\n  return [{\n    json: {\n      noNewPatients: true,\n      summary: `\u041e\u0431\u0449\u043e \u0441\u044a\u0431\u0438\u0442\u0438\u044f: ${extracted.totalEvents}, \u0418\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438 \u043f\u0430\u0446\u0438\u0435\u043d\u0442\u0438: ${extracted.extractedCount}, \u0412\u0435\u0447\u0435 \u0432 \u0442\u0430\u0431\u043b\u0438\u0446\u0430\u0442\u0430: ${existingRows.length}, \u041d\u043e\u0432\u0438: 0`\n    }\n  }];\n}\n\nreturn newPatients.map(p => ({\n  json: {\n    noNewPatients: false,\n    name: p.name,\n    phone: p.phone,\n    createdAt: new Date().toISOString().split('T')[0]\n  }\n}));"
      },
      "id": "find-new-patients",
      "name": "Find New Patients",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.noNewPatients }}",
              "value2": true
            }
          ]
        }
      },
      "id": "has-new-patients",
      "name": "Has New Patients?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1320,
        100
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1hv4XAfHhScA40Bm1kQ3I-Ih4SJuCBpOJxTOYDNb167g"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Patients"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "name": "={{ $json.name }}",
            "phone": "={{ \"'\" + $json.phone }}",
            "createdAt": "={{ $json.createdAt }}"
          }
        },
        "options": {}
      },
      "id": "append-patients",
      "name": "Append New Patients",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1540,
        200
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Final summary\nconst items = $input.all();\nreturn [{\n  json: {\n    success: true,\n    message: `\u2705 \u0414\u043e\u0431\u0430\u0432\u0435\u043d\u0438 ${items.length} \u043d\u043e\u0432\u0438 \u043f\u0430\u0446\u0438\u0435\u043d\u0442\u0438 \u0432 Patients sheet`,\n    addedCount: items.length,\n    patients: items.map(i => ({ name: i.json.name, phone: i.json.phone }))\n  }\n}];"
      },
      "id": "summary",
      "name": "Summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        200
      ]
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Set Date Range",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Date Range": {
      "main": [
        [
          {
            "node": "Get All Calendar Events",
            "type": "main",
            "index": 0
          },
          {
            "node": "Read Existing Patients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All Calendar Events": {
      "main": [
        [
          {
            "node": "Extract Patients from Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Patients from Events": {
      "main": [
        [
          {
            "node": "Wait for Both",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Existing Patients": {
      "main": [
        [
          {
            "node": "Wait for Both",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Wait for Both": {
      "main": [
        [
          {
            "node": "Find New Patients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find New Patients": {
      "main": [
        [
          {
            "node": "Has New Patients?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has New Patients?": {
      "main": [
        [],
        [
          {
            "node": "Append New Patients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append New Patients": {
      "main": [
        [
          {
            "node": "Summary",
            "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 - Patients Backfill from Calendar. Uses googleCalendar, googleSheets. Event-driven trigger; 10 nodes.

Source: https://github.com/Georgi-Piskov/RODOPI-DENT/blob/07217e5d65fd897a41528db70a4869149ca9a5b1/n8n-workflows/23-patients-backfill.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

This workflow automates the process of creating Google Calendar events from a Google Sheet. It ensures each row in the sheet is evaluated for its current status and: Creates new events in Google Calen

Google Sheets, Google Calendar
Data & Sheets

This workflow monitors a Notion database for new or updated tasks and checks if the scheduled time is available in Google Calendar. It logs every task into Google Sheets for tracking and auditing. Bas

Google Calendar, Notion Trigger, Google Sheets +1
Data & Sheets

🚨 DISCLAIMER (READ FIRST):

Chain Llm, OpenRouter Chat, Google Sheets +2
Data & Sheets

This workflow monitors Google Calendar for events indicating that a customer will visit the company today or the next day, retrieves the required details, and sends reminder notifications to the relev

Google Calendar, Google Sheets, HTTP Request +1
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