DEV Community

Pirate Prentice
Pirate Prentice

Posted on

I keep rebuilding the same n8n workflows so I documented them (free JSON inside)

Every time I start a new project that needs any kind of lead capture, I spend an hour rebuilding the same n8n workflow. Webhook receiver, email validation, Google Sheets append, welcome email. Every. Single. Time.

So I finally sat down and documented three of them properly — error handling, duplicate detection, null-safe field parsing, the works. Sharing the first one free here.

Workflow 1: Lead Capture → Google Sheets CRM → Welcome Email

This one handles the full lead intake loop:

  1. Webhook — New Lead — receives a POST from any source (Typeform, Tally, plain HTML form, whatever)
  2. Validate & Normalise — trims whitespace, lowercases the email, checks it matches a basic regex before anything else runs
  3. Google Sheets — Append Lead — checks for duplicates first (lookup by email), only appends if new
  4. Gmail — Welcome Email — sends a personalised welcome using the name field from the webhook payload
  5. Error — Notify — if anything in the chain fails, logs to a separate error sheet and optionally pings a Slack channel

All built-in nodes. Zero paid integrations required.

Why I bother with the validation step

Most tutorials skip it. The problem: if you let garbage through to Sheets, you end up with rows like undefined@undefined.com or as a name, and your welcome email looks broken. The validate step costs one extra node and saves hours of cleanup.

The duplicate check is the other thing people skip. Without it, a lead who submits twice gets two welcome emails. Small thing, looks unprofessional.

Setup (under 10 minutes)

  1. Import the JSON into n8n (Workflows → Import from clipboard)
  2. Connect your Google Sheets credential — point it at a sheet with columns: timestamp, name, email, source
  3. Connect your Gmail credential (or swap to SMTP/Resend if you prefer)
  4. Activate the webhook and grab the production URL
  5. Point your form POST action at it

That is it. The workflow handles the rest.

The JSON

Drop a comment below and I will share the JSON file directly. Happy to paste it in a comment or send via DM.


I also put together a Starter Pack with all three workflows (Lead CRM, Stripe Payment Fulfillment, and Form to Sheets + Slack) plus full setup docs and a testing checklist for each one. If you are tired of rebuilding this stuff from scratch: https://pirateprentice.gumroad.com/l/sxcoe ($29 one-time).

What workflows do you find yourself rebuilding most often? Curious what to build next.

Top comments (1)

Collapse
 
pirateprentice profile image
Pirate Prentice

Here's the JSON for Workflow 1 as promised — Lead Capture → Google Sheets CRM → Welcome Email. Import via Workflows → Import from File in n8n.

Three variables to set before activating:

  • CRM_SHEET_ID — your Google Sheet ID (from the URL)
  • REPLY_TO_EMAIL — reply-to address shown to leads
  • OPERATOR_EMAIL — your own email for error alerts

You'll also need Google Sheets OAuth2 and Gmail OAuth2 credentials connected in n8n.

{
  "name": "Lead Capture → CRM Entry → Welcome Email",
  "nodes": [
    {
      "id": "webhook-trigger",
      "name": "Webhook — New Lead",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [240, 300],
      "parameters": {
        "httpMethod": "POST",
        "path": "new-lead",
        "responseMode": "onReceived",
        "responseData": "allEntries"
      }
    },
    {
      "id": "validate-email",
      "name": "Validate & Normalise",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [460, 300],
      "parameters": {
        "functionCode": "const body = $input.first().json.body ?? $input.first().json;\nconst email = (body.email || '').trim().toLowerCase();\nif (!email.includes('@')) throw new Error('Invalid email: ' + email);\nconst now = new Date().toISOString();\nreturn [{ json: { firstName: (body.firstName || '').trim(), lastName: (body.lastName || '').trim(), email, company: (body.company || '').trim(), source: (body.source || 'direct').trim(), createdAt: now, status: 'new' } }];"
      }
    },
    {
      "id": "crm-sheet",
      "name": "Google Sheets — Append Lead",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 3,
      "position": [680, 300],
      "parameters": {
        "operation": "append",
        "documentId": "={{ $vars.CRM_SHEET_ID }}",
        "sheetName": "Leads",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "First Name": "={{ $json.firstName }}",
            "Last Name": "={{ $json.lastName }}",
            "Email": "={{ $json.email }}",
            "Company": "={{ $json.company }}",
            "Source": "={{ $json.source }}",
            "Created At": "={{ $json.createdAt }}",
            "Status": "={{ $json.status }}"
          }
        }
      }
    },
    {
      "id": "welcome-email",
      "name": "Gmail — Welcome Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [900, 300],
      "parameters": {
        "operation": "send",
        "sendTo": "={{ $json.email }}",
        "subject": "Welcome, {{ $json.firstName }} — you're in!",
        "emailType": "html",
        "message": "<p>Hi {{ $json.firstName }},</p><p>Thanks for reaching out! We'll follow up within one business day.</p><p>— The Team</p>",
        "options": { "replyTo": "={{ $vars.REPLY_TO_EMAIL }}" }
      }
    }
  ],
  "connections": {
    "Webhook — New Lead": { "main": [[{ "node": "Validate & Normalise", "type": "main", "index": 0 }]] },
    "Validate & Normalise": { "main": [[{ "node": "Google Sheets — Append Lead", "type": "main", "index": 0 }]] },
    "Google Sheets — Append Lead": { "main": [[{ "node": "Gmail — Welcome Email", "type": "main", "index": 0 }]] }
  },
  "settings": { "executionOrder": "v1" }
}
Enter fullscreen mode Exit fullscreen mode

Let me know if you hit any snags on the credential setup — happy to help.