{
  "name": "Gmail Mail Merge",
  "nodes": [
    {
      "parameters": {
        "content": "## Gmail Mail Merge: Send Personalized Emails from Google Sheets\n\nSend personalized emails to hundreds of contacts from a Google Sheet without manual copy-paste. Each recipient gets a customized message based on their row in the sheet.\n\n**Setup time:** 10 minutes  ·  **Level:** Beginner\n\n**You'll need accounts/credentials for:** Google Sheets, Gmail\n\n**Replace these before running:**\n- `REPLACE_WITH_SPREADSHEET`\n\n**Setup steps:**\n1. Connect Google Sheets and Gmail (OAuth) in their respective nodes.\n2. Replace REPLACE_WITH_SPREADSHEET with your actual Google Sheet ID (from the URL).\n3. Update the Code node to map your sheet columns to the email personalization variables.\n4. Configure the Schedule Trigger timing or set up the manual trigger for on-demand execution.\n5. Test with 2-3 rows first, then run on your full contact list.\n\nFree templates and updates: navkhan103.github.io",
        "height": 300,
        "width": 400,
        "color": 4
      },
      "id": "sticky-setup",
      "name": "Setup — read me first",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        -120
      ]
    },
    {
      "parameters": {
        "content": "### How it runs\n\nStarts from **Schedule Trigger** (or manual trigger).\n\n1. A Schedule Trigger runs the workflow (or a manual trigger).\n2. A Google Sheets node reads all rows from your contact spreadsheet.\n3. A Code node loops through each row, personalizing the email subject and body with data from each row.\n4. A Gmail node sends each personalized email to the recipient's address from the sheet.",
        "height": 300,
        "width": 400,
        "color": 7
      },
      "id": "sticky-how",
      "name": "How it runs",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        220
      ]
    },
    {
      "parameters": {
        "content": "### If something breaks\n\nThe read nodes here **retry 3× automatically** (5s apart), which absorbs rate limits and brief timeouts.\n\nNodes that **send or write deliberately do not retry** — a send that reached the far end but lost its response would otherwise fire twice, so you'd get duplicate messages or duplicate rows.\n\nTo be told when one of those fails, build a workflow starting with an **Error Trigger** node that posts to Slack or email, then set it under **three-dot menu > Settings > Error Workflow**. Do it once and point every workflow at it.",
        "height": 380,
        "width": 400,
        "color": 3
      },
      "id": "sticky-fail",
      "name": "If something breaks",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        560
      ]
    },
    {
      "parameters": {
        "rule": {
          "times": [
            {
              "hour": 9,
              "minute": 0
            }
          ],
          "daysOfMonth": "*",
          "months": "*",
          "weekdays": [
            2,
            3,
            4,
            5,
            6
          ]
        }
      },
      "id": "f9a0b1c2-0001-4000-8000-000000000001",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "REPLACE_WITH_SPREADSHEET"
        },
        "sheetName": "Sheet1",
        "range": "A:Z"
      },
      "id": "f9a0b1c2-0002-4000-8000-000000000002",
      "name": "Read Contacts",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Loop through each row in the sheet\nconst rows = $input.all();\nconst results = [];\n\nfor (const item of rows) {\n  const row = item.json;\n  \n  // Customize these fields based on your sheet structure\n  const recipientEmail = row.email || row.Email;\n  const recipientName = row.name || row.Name;\n  \n  // Skip rows without email addresses\n  if (!recipientEmail) continue;\n  \n  // Build personalized subject and body\n  const personalizedSubject = `Hi ${recipientName}, your custom subject here`;\n  const personalizedBody = `Dear ${recipientName},\\n\\nYour personalized message goes here. You can reference other fields from your sheet like company: ${row.company || 'N/A'}.\\n\\nBest regards,\\nYour Team`;\n  \n  results.push({\n    json: {\n      toEmail: recipientEmail,\n      subject: personalizedSubject,\n      body: personalizedBody\n    }\n  });\n}\n\nreturn results;"
      },
      "id": "f9a0b1c2-0003-4000-8000-000000000003",
      "name": "Personalize Emails",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "toEmail": "={{ $json.toEmail }}",
        "subject": "={{ $json.subject }}",
        "htmlContent": "={{ $json.body.replace(/\\n/g, '<br>') }}"
      },
      "id": "f9a0b1c2-0004-4000-8000-000000000004",
      "name": "Send Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        660,
        0
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Read Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Contacts": {
      "main": [
        [
          {
            "node": "Personalize Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Personalize Emails": {
      "main": [
        [
          {
            "node": "Send Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}