AutomationFlowsData & Sheets › Upsert Patients to Google Sheets via Webhook

Upsert Patients to Google Sheets via Webhook

Original n8n title: Rodopi Dent - Patients Upsert

Rodopi Dent - Patients Upsert. Uses googleSheets. Webhook trigger; 10 nodes.

Webhook trigger★★★★☆ complexity10 nodesGoogle Sheets
Data & Sheets Trigger: Webhook Nodes: 10 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 - Patients Upsert",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "patients-upsert",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "*"
        }
      },
      "id": "webhook",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Validate input\nconst body = $input.first().json.body;\n\nif (!body.name || !body.phone) {\n  return [{\n    json: {\n      valid: false,\n      error: '\u041b\u0438\u043f\u0441\u0432\u0430 \u0438\u043c\u0435 \u0438\u043b\u0438 \u0442\u0435\u043b\u0435\u0444\u043e\u043d'\n    }\n  }];\n}\n\n// Normalize phone to international +359 format\n// This prevents Google Sheets from stripping the leading 0\nlet phone = String(body.phone).replace(/[\\s()\\-.]/g, '');\nif (phone.startsWith('00359')) phone = '+359' + phone.slice(5);\nelse if (phone.startsWith('0') && phone.length >= 10) phone = '+359' + phone.slice(1);\nelse if (/^[89]\\d{8}$/.test(phone)) phone = '+359' + phone;\nelse if (!phone.startsWith('+') && phone.length >= 9) phone = '+359' + phone;\n\nreturn [{\n  json: {\n    valid: true,\n    name: body.name.trim(),\n    phone: phone\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": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1hv4XAfHhScA40Bm1kQ3I-Ih4SJuCBpOJxTOYDNb167g"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Patients"
        },
        "options": {
          "returnAllMatches": true
        }
      },
      "id": "read-patients",
      "name": "Read Patients",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        660,
        100
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Check if patient with this phone already exists\nconst inputData = $('Validate Input').first().json;\nconst existingPatients = $('Read Patients').all().map(item => item.json);\n\n// Extract core 9-digit number for robust comparison across formats\n// +359887493315 / 0887493315 / 887493315 all map to 887493315\nconst phoneKey = (p) => String(p || '').replace(/[\\s()\\-.+]/g, '').replace(/^359/, '').replace(/^0/, '');\n\nconst inputKey = phoneKey(inputData.phone);\nconst phoneExists = existingPatients.some(patient => phoneKey(patient.phone) === inputKey);\n\n// Also check by name (case-insensitive)\nconst nameExists = existingPatients.some(patient =>\n  (patient.name || '').toLowerCase().trim() === (inputData.name || '').toLowerCase().trim()\n);\n\nreturn [{\n  json: {\n    exists: phoneExists || nameExists,\n    name: inputData.name,\n    phone: inputData.phone\n  }\n}];"
      },
      "id": "check-exists",
      "name": "Check If Exists",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.exists }}",
              "value2": true
            }
          ]
        }
      },
      "id": "already-exists",
      "name": "Already Exists?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1100,
        100
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, message: '\u041f\u0430\u0446\u0438\u0435\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430', action: 'skipped', name: $json.name, phone: $json.phone }) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "respond-exists",
      "name": "Respond Already Exists",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1320,
        0
      ]
    },
    {
      "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": "={{ new Date().toISOString().split('T')[0] }}"
          }
        },
        "options": {}
      },
      "id": "append-patient",
      "name": "Append New Patient",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1320,
        200
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, message: '\u041f\u0430\u0446\u0438\u0435\u043d\u0442\u044a\u0442 \u0435 \u0434\u043e\u0431\u0430\u0432\u0435\u043d', action: 'created', name: $('Check If Exists').first().json.name, phone: $('Check If Exists').first().json.phone }) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "respond-created",
      "name": "Respond Created",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1540,
        200
      ]
    }
  ],
  "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": "Read Patients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Patients": {
      "main": [
        [
          {
            "node": "Check If Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check If Exists": {
      "main": [
        [
          {
            "node": "Already Exists?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Already Exists?": {
      "main": [
        [
          {
            "node": "Respond Already Exists",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Append New Patient",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append New Patient": {
      "main": [
        [
          {
            "node": "Respond Created",
            "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 Upsert. Uses googleSheets. Webhook trigger; 10 nodes.

Source: https://github.com/Georgi-Piskov/RODOPI-DENT/blob/f071a84326ea5adf54e4eb20a2ba3e34aac5b728/n8n-workflows/22-patients-upsert.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

[SANTOBET] FLUXO TODO - BACKUP. Uses googleSheets, httpRequest, googleSheetsTrigger. Webhook trigger; 57 nodes.

Google Sheets, HTTP Request, Google Sheets Trigger
Data & Sheets

FLUXO DISPARO DATA E HORA. Uses itemLists, googleSheets, httpRequest. Webhook trigger; 48 nodes.

Item Lists, Google Sheets, HTTP Request
Data & Sheets

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

Google Sheets, HTTP Request
Data & Sheets

Transform your n8n instance management with this advanced automation system featuring artificial intelligence-driven workflow selection. This template provides comprehensive maintenance operations wit

n8n, HTTP Request, Google Sheets +1
Data & Sheets

Nexus_v6(ล่าสุดจริงๆ)ล่าสุดไกไก. Uses googleSheets, httpRequest. Webhook trigger; 41 nodes.

Google Sheets, HTTP Request