AutomationFlowsMarketing & Ads › Capture Website Form Leads with Deduped Logging in Google Sheets and Telegram

Capture Website Form Leads with Deduped Logging in Google Sheets and Telegram

ByDmytro @dmytro on n8n.io

This workflow receives website form submissions via webhook, normalizes lead fields, detects duplicates using a phone/email dedupe key, and writes the lead to Google Sheets as a new row or an update. It then sends a Telegram alert with different messaging for new versus repeat…

Webhook trigger★★★★☆ complexity17 nodesGoogle SheetsTelegram
Marketing & Ads Trigger: Webhook Nodes: 17 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17618 — we link there as the canonical source.

This workflow follows the Google Sheets → Telegram 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": "Capture website form leads into Google Sheets with duplicate detection for HVAC, plumbing and home service businesses",
  "tags": [],
  "nodes": [
    {
      "id": "sticky-0",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        -256
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Capture website form leads into Google Sheets with duplicate detection for HVAC, plumbing and home service businesses\n\n### How it works\n\nThis workflow captures website form submissions through a webhook, immediately confirms receipt, and normalizes incoming lead fields from JSON or form-encoded posts. It checks Google Sheets for an existing lead, then either updates the existing row for repeat enquiries or appends a new row for first-time leads. The workflow finishes by sending a Telegram alert tailored to whether the enquiry was new or a repeat.\n\n### Setup steps\n\n- Configure the webhook URL in the website form so submissions are sent to the n8n webhook endpoint.\n- Create a sheet named Leads with these headers in row 1: received_at, last_enquiry_at, full_name, phone, dedupe_key, email, message, source, enquiry_count.\n- Connect Google Sheets credentials and select the same spreadsheet and sheet in all three Google Sheets nodes.\n- In Update Lead in Sheets, select dedupe_key under Column to Match On. This field resets whenever the document is re-selected, and the node fails without it.\n- Connect Telegram bot credentials and set the chat ID in both alert nodes.\n- Review the ALIASES block in Map Lead Fields and add any field names your own form uses.\n\n### Customization\n\nChange the duplicate detection key in the Map Lead Fields code node to match on email instead of phone, or on both. Customize the Telegram message templates, and add extra columns to the sheet and to the output of Map Lead Fields to track things such as job type or service area."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -240,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 304,
        "content": "## Receive and normalize lead\n\nAccepts the website form submission, returns an immediate webhook response, and standardizes the incoming lead fields for downstream processing."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-2",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        688,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 320,
        "content": "## Check existing records\n\nLooks up the normalized lead in Google Sheets and branches the workflow depending on whether a matching lead already exists."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-3",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        -224
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 320,
        "content": "## Update repeat enquiry\n\nBuilds the changed fields for a repeat enquiry and updates the matching Google Sheets row without overwriting unchanged lead details."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-4",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 304,
        "content": "## Add first-time lead\n\nPrepares a complete row for a new prospect and appends it to the Google Sheets lead tracker."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-5",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1792,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 608,
        "content": "## Send lead alerts\n\nSends Telegram notifications for both workflow outcomes, with separate alerts for repeat enquiries and newly added leads."
      },
      "typeVersion": 1
    },
    {
      "id": "webhook-new-lead",
      "name": "When Lead Form Submitted",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -200,
        60
      ],
      "parameters": {
        "path": "new-lead",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "respond-received",
      "name": "Confirm Form Submission",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        20,
        60
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\"status\":\"received\"}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "code-normalise",
      "name": "Map Lead Fields",
      "type": "n8n-nodes-base.code",
      "position": [
        280,
        60
      ],
      "parameters": {
        "jsCode": "// Accepts JSON or form-encoded posts from any web form and maps the usual\n// field-name variants onto the exact column names used in the sheet.\n\n// ---------- ADD YOUR OWN FIELD NAMES HERE ----------\nconst ALIASES = {\n  full_name: ['name', 'full_name', 'fullname', 'your-name', 'contact_name', 'first_name'],\n  phone: ['phone', 'telephone', 'tel', 'phone_number', 'phonenumber', 'mobile', 'your-phone'],\n  email: ['email', 'e-mail', 'email_address', 'your-email'],\n  message: ['message', 'comments', 'details', 'description', 'job_description', 'your-message'],\n  source: ['source', 'utm_source', 'form_name', 'page'],\n};\n// ---------------------------------------------------\n\nfunction pick(body, aliases) {\n  const keys = Object.keys(body);\n  for (const alias of aliases) {\n    const match = keys.find((k) => k.toLowerCase() === alias);\n    if (match && String(body[match]).trim() !== '') return String(body[match]).trim();\n  }\n  return '';\n}\n\nreturn $input.all().map((item) => {\n  const body = item.json.body || item.json || {};\n\n  const phone = pick(body, ALIASES.phone);\n  const email = pick(body, ALIASES.email);\n\n  // Digits only, with a leading US country code dropped, so the same person\n  // typing their number two different ways still counts as one lead.\n  const digits = phone.replace(/\\D/g, '').replace(/^1(?=\\d{10}$)/, '');\n\n  // Prefixed so the key is never a bare number: a digits-only value gets stored\n  // as a number by the spreadsheet and the lookup then stops matching.\n  const dedupe_key = digits ? `p:${digits}` : (email ? `e:${email.toLowerCase()}` : '');\n\n  const now = new Date().toISOString();\n\n  // The keys below match the sheet headers exactly, which lets the Google Sheets\n  // nodes map them automatically with no manual column mapping.\n  return {\n    json: {\n      received_at: now,\n      last_enquiry_at: now,\n      full_name: pick(body, ALIASES.full_name),\n      phone,\n      dedupe_key,\n      email,\n      message: pick(body, ALIASES.message),\n      source: pick(body, ALIASES.source) || 'website',\n      enquiry_count: 1,\n    },\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "sheets-lookup",
      "name": "Check Existing Lead in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        740,
        60
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.dedupe_key }}",
              "lookupColumn": "dedupe_key"
            }
          ]
        },
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_YOUR_SPREADSHEET_ID"
        }
      },
      "typeVersion": 4.5,
      "alwaysOutputData": true
    },
    {
      "id": "if-already-known",
      "name": "If Lead Exists in Sheets",
      "type": "n8n-nodes-base.if",
      "position": [
        980,
        60
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "row-exists",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.dedupe_key }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "code-build-update",
      "name": "Build Updated Lead Row",
      "type": "n8n-nodes-base.code",
      "position": [
        1220,
        -60
      ],
      "parameters": {
        "jsCode": "// Builds only the columns that change on a repeat enquiry. Anything not listed\n// here keeps its original value in the sheet.\nconst lead = $('Map Lead Fields').first().json;\nconst existing = $input.first().json;\n\nreturn [\n  {\n    json: {\n      dedupe_key: lead.dedupe_key,\n      last_enquiry_at: lead.last_enquiry_at,\n      message: lead.message,\n      source: lead.source,\n      enquiry_count: Number(existing.enquiry_count || 1) + 1,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "sheets-update",
      "name": "Update Lead in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1460,
        -60
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "dedupe_key"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_YOUR_SPREADSHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "code-build-new",
      "name": "Create New Lead Row",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        256
      ],
      "parameters": {
        "jsCode": "// The lookup node replaces the item with an empty object when no match is found,\n// so pull the normalised lead back in before the row is written.\nreturn [{ json: $('Map Lead Fields').first().json }];"
      },
      "typeVersion": 2
    },
    {
      "id": "sheets-append",
      "name": "Append Lead to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1456,
        256
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_YOUR_SPREADSHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "telegram-repeat",
      "name": "Notify Repeat Inquiry via Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1840,
        -60
      ],
      "parameters": {
        "text": "=Repeat enquiry (already in the sheet)\n\nName: {{ $('Map Lead Fields').first().json.full_name }}\nPhone: {{ $('Map Lead Fields').first().json.phone }}\nEmail: {{ $('Map Lead Fields').first().json.email }}\nSource: {{ $('Map Lead Fields').first().json.source }}\n\n{{ $('Map Lead Fields').first().json.message }}",
        "chatId": "REPLACE_WITH_YOUR_CHAT_ID",
        "resource": "message",
        "operation": "sendMessage",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "telegram-new",
      "name": "Notify New Lead via Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1840,
        180
      ],
      "parameters": {
        "text": "=New lead\n\nName: {{ $('Map Lead Fields').first().json.full_name }}\nPhone: {{ $('Map Lead Fields').first().json.phone }}\nEmail: {{ $('Map Lead Fields').first().json.email }}\nSource: {{ $('Map Lead Fields').first().json.source }}\n\n{{ $('Map Lead Fields').first().json.message }}",
        "chatId": "REPLACE_WITH_YOUR_CHAT_ID",
        "resource": "message",
        "operation": "sendMessage",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "typeVersion": 1.2
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Map Lead Fields": {
      "main": [
        [
          {
            "node": "Check Existing Lead in Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create New Lead Row": {
      "main": [
        [
          {
            "node": "Append Lead to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Lead to Sheets": {
      "main": [
        [
          {
            "node": "Notify New Lead via Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Lead in Sheets": {
      "main": [
        [
          {
            "node": "Notify Repeat Inquiry via Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Updated Lead Row": {
      "main": [
        [
          {
            "node": "Update Lead in Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Confirm Form Submission": {
      "main": [
        [
          {
            "node": "Map Lead Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Lead Exists in Sheets": {
      "main": [
        [
          {
            "node": "Build Updated Lead Row",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create New Lead Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Lead Form Submitted": {
      "main": [
        [
          {
            "node": "Confirm Form Submission",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Existing Lead in Sheets": {
      "main": [
        [
          {
            "node": "If Lead Exists in Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow receives website form submissions via webhook, normalizes lead fields, detects duplicates using a phone/email dedupe key, and writes the lead to Google Sheets as a new row or an update. It then sends a Telegram alert with different messaging for new versus repeat…

Source: https://n8n.io/workflows/17618/ — original creator credit. Request a take-down →

More Marketing & Ads workflows → · Browse all categories →

Related workflows

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

Marketing & Ads

01 - Lead dal sito → Registro + Notifiche. Uses googleSheets, telegram, gmail. Webhook trigger; 6 nodes.

Google Sheets, Telegram, Gmail
Marketing & Ads

Ad agencies needing automated lead capture. Sales teams fighting fraud and scoring leads. B2B SaaS companies nurturing prospects. Marketing pros boosting sales pipelines. Captures leads via Webhook fr

HTTP Request, Google Sheets, Slack +2
Marketing & Ads

This workflow captures real estate property inquiries via a webhook, validates and normalizes lead details, upserts the lead into Google Sheets, sends an auto-reply through Gmail, and notifies your te

Google Sheets, Gmail, Slack +1
Marketing & Ads

This workflow captures real estate property inquiries via a webhook, saves the lead to Google Sheets, sends an auto-reply email through Gmail, and posts a notification to a Slack channel before return

Google Sheets, Gmail, Slack +1
Marketing & Ads

Capture inbound leads via webhook, validate and sanitize the data, deduplicate against Google Sheets, and store only clean leads ready to feed an AI-powered email/SMS nurture sequence. Webhook receive

Google Sheets