{
  "id": "",
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "Timezone-aware email drip campaign with daily send limits (no wait nodes) - Gmail + Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "ab68acee-89bb-46ca-8639-beb8b3040e75",
      "name": "Trigger EU_UK 10:00 UTC",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -608,
        304
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 10 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "10a49736-3249-4683-ac02-91cdd36f2683",
      "name": "Determine Group",
      "type": "n8n-nodes-base.code",
      "position": [
        -416,
        512
      ],
      "parameters": {
        "jsCode": "// Maps the current UTC hour to an audience region plus a daily send cap.\n// Ranges are non-overlapping so each trigger hits exactly one group.\nconst hour = new Date().getUTCHours();\nlet group, daily_limit;\n\nif (hour >= 9 && hour < 13) {\n  // EU & UK working hours (morning Europe)\n  group = 'EU_UK';\n  daily_limit = 45;\n} else if (hour >= 14 && hour < 20) {\n  // North America working hours (morning/midday NA)\n  group = 'NA';\n  daily_limit = 90;\n} else {\n  // Remaining hours cover Australia / Asia-Pacific\n  group = 'AU';\n  daily_limit = 15;\n}\n\nreturn [{ json: { group, daily_limit } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6693ed50-b9f3-47a9-8848-38f56e7a2be4",
      "name": "Read Contacts",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -160,
        512
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "607a2e0f-f9f7-4056-8c83-45a3dfee211b",
      "name": "Filter and Limit",
      "type": "n8n-nodes-base.code",
      "position": [
        64,
        512
      ],
      "parameters": {
        "jsCode": "const group = $('Determine Group').first().json.group;\nconst daily_limit = $('Determine Group').first().json.daily_limit;\n\nconst countryGroups = {\n  'Austria': 'EU_UK', 'Belgium': 'EU_UK', 'France': 'EU_UK',\n  'Germany': 'EU_UK', 'Italy': 'EU_UK', 'Netherlands': 'EU_UK',\n  'Norway': 'EU_UK', 'Spain': 'EU_UK', 'Sweden': 'EU_UK',\n  'Switzerland': 'EU_UK', 'United Kingdom': 'EU_UK',\n  'Canada': 'NA', 'United States': 'NA',\n  'Australia': 'AU'\n};\n\nconst filtered = items.filter(item => {\n  const country   = (item.json['Country'] || '').trim();\n  const emailSent = (item.json['email_sent'] || '').toString().trim();\n  const email     = (item.json['Email'] || '').trim();\n  return (\n    countryGroups[country] === group &&\n    emailSent === '' &&\n    email.includes('@')\n  );\n});\n\nreturn filtered.slice(0, daily_limit);"
      },
      "typeVersion": 2
    },
    {
      "id": "2c74a24d-e217-4a71-a9aa-1a096e38c7ce",
      "name": "Build Email",
      "type": "n8n-nodes-base.set",
      "position": [
        288,
        512
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "field-001",
              "name": "to",
              "type": "string",
              "value": "={{ $json['Email'] }}"
            },
            {
              "id": "field-002",
              "name": "contact_email",
              "type": "string",
              "value": "={{ $json['Email'] }}"
            },
            {
              "id": "field-003",
              "name": "subject",
              "type": "string",
              "value": "=Quick question to {{ $('Read Contacts').item.json.Company }} Team"
            },
            {
              "id": "field-004",
              "name": "body",
              "type": "string",
              "value": "=<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"margin:0;padding:0;background:#ffffff;\">\n  <tr>\n    <td align=\"left\" style=\"padding:0;margin:0;\">\n      <div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.55;color:#111111;\">\n        <p style=\"margin:0 0 14px 0;\">Hi {{ $json['First Name'] }},</p>\n\n        <p style=\"margin:0 0 14px 0;\">\n          [Write your opening line here. Mention something specific about the recipient or their company.]\n        </p>\n\n        <p style=\"margin:0 0 14px 0;\">\n          [Explain the problem you help solve, in one short paragraph.]\n        </p>\n\n        <p style=\"margin:0 0 14px 0;\">\n          <strong>[Your product or service name]</strong> helps [target persona] to [outcome], without [common pain point].\n        </p>\n\n        <p style=\"margin:0 0 18px 0;\">\n          Happy to share more if relevant - feel free to reply with <strong>\"yes\"</strong> and I will send details.\n        </p>\n\n        <p style=\"margin:0;border-top:1px solid #dddddd;padding-top:12px;font-size:14px;color:#444444;\">\n          Best regards,<br><br>\n          <strong>[Your Name] - [Your Company]</strong><br>\n          [Your title]<br>\n          <a href=\"https://example.com\" target=\"_blank\" style=\"color:#0066cc;\">https://example.com</a>\n        </p>\n      </div>\n    </td>\n  </tr>\n</table>"
            },
            {
              "id": "field-005",
              "name": "group_sent",
              "type": "string",
              "value": "={{ $('Determine Group').first().json.group }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "de7b7941-e2c4-4d39-bd6c-0477ee0b08fb",
      "name": "Send Gmail",
      "type": "n8n-nodes-base.gmail",
      "onError": "continueErrorOutput",
      "position": [
        512,
        512
      ],
      "parameters": {
        "sendTo": "={{ $json.to }}",
        "message": "={{ $json.body }}",
        "options": {
          "senderName": "",
          "appendAttribution": false
        },
        "subject": "={{ $json.subject }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "fb630893-1e98-4cbe-afdb-4e54daae2004",
      "name": "Update Row Success",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        960,
        352
      ],
      "parameters": {
        "columns": {
          "value": {
            "status": "sent",
            "error_msg": "",
            "sent_date": "={{ $now.toFormat('yyyy-MM-dd HH:mm:ss') }}",
            "email_sent": "yes",
            "group_sent": "={{ $('Determine Group').item.json.group }}",
            "row_number": "={{ $('Read Contacts').item.json.row_number }}"
          },
          "schema": [
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "email_sent",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "email_sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sent_date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sent_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "group_sent",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "group_sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "error_msg",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "error_msg",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "f1f12bdd-cfe1-4d81-a3c2-60dfcf41630d",
      "name": "Update Row Error",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        960,
        672
      ],
      "parameters": {
        "columns": {
          "value": {
            "status": "failed",
            "error_msg": "={{ $json.error?.message || $json.error || 'Unknown error' }}",
            "sent_date": "={{ $now.toFormat('yyyy-MM-dd HH:mm:ss') }}",
            "email_sent": "no",
            "group_sent": "={{ $('Determine Group').item.json.group }}",
            "row_number": "={{ $('Read Contacts').item.json.row_number }}"
          },
          "schema": [
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "email_sent",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "email_sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sent_date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sent_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "group_sent",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "group_sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "error_msg",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "error_msg",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultUrl": "",
          "cachedResultName": ""
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "48d24d3e-064b-4ec5-a905-a90cdf5edf5e",
      "name": "Trigger NA 18:00 UTC",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -608,
        512
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 18 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "d4309fb4-993f-47fe-9d1b-06cade209599",
      "name": "Trigger AU 1:00 UTC",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -608,
        704
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 01 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "0bb21b83-de9a-485c-a4c9-6a779da3ab4a",
      "name": "Sticky Note 0001",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2576,
        368
      ],
      "parameters": {
        "color": 7,
        "width": 1600,
        "height": 520,
        "content": "## Timezone-aware email drip campaign (no wait nodes)\n\nSend outreach emails at the right working hour for each audience region, with a daily cap per region, status tracking in Google Sheets, and per-row error handling. No `Wait` nodes required - timing is driven entirely by three region-specific Cron schedules.\n\n### What it does\n- Three schedule triggers fire at the local working-hours window of each region (EU/UK, NA, AU)\n- A shared code node inspects the current UTC hour and picks the matching region + daily send limit\n- Reads your contact sheet, filters by country -> region mapping, skips rows already marked sent, and caps at the daily limit\n- Sends a personalised Gmail message\n- Writes `email_sent`, `sent_date`, `status`, `group_sent`, and `error_msg` back to the same row\n\n### Credentials required\n- **Google Sheets OAuth2** - reads contacts and writes status back\n- **Gmail OAuth2** - sends the outreach email\n\n### Setup (10-15 min)\n1. Create a Google Sheet with the columns listed in the \"Sheet schema\" sticky note\n2. In all three **Google Sheets** nodes, pick your sheet and the contacts tab\n3. Edit the **Build Email** node: replace the placeholder subject line and HTML body with your own copy\n4. In the **Send Gmail** node, set `senderName` to your name\n5. Optional: adjust the daily caps inside **Determine Group** (currently 45 / 90 / 15)\n6. Optional: edit the country -> region mapping inside **Filter and Limit** to match your contact list\n7. Activate the workflow\n"
      },
      "typeVersion": 1
    },
    {
      "id": "252c415a-c310-417f-a7e0-c38b115781f3",
      "name": "Sticky Note 0002",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -912,
        208
      ],
      "parameters": {
        "color": 4,
        "width": 200,
        "height": 560,
        "content": "## Step 1 - Three timezone-specific triggers\n\nThree independent `Schedule Trigger` nodes fire on weekdays (`Mon-Fri`):\n\n- **EU_UK** at 10:00 UTC  (morning in Europe)\n- **NA** at 18:00 UTC     (late morning on US East coast)\n- **AU** at 01:00 UTC     (noon in eastern Australia)\n\nAll three feed into the same `Determine Group` node, so there is exactly ONE shared pipeline instead of three copies.\n\nEdit the cron expressions to match your preferred send windows."
      },
      "typeVersion": 1
    },
    {
      "id": "0b27bf50-9c79-4eb0-bff8-c050874c9d32",
      "name": "Sticky Note 0003",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -448,
        16
      ],
      "parameters": {
        "color": 3,
        "width": 200,
        "height": 468,
        "content": "## Step 2 - Determine region + daily cap\n\nReads the current UTC hour and returns:\n- `group`: one of `EU_UK`, `NA`, `AU`\n- `daily_limit`: max emails for that region today (default 45 / 90 / 15)\n\nRanges are non-overlapping so each trigger maps to exactly one region. Adjust the numbers here to change your send volume per region."
      },
      "typeVersion": 1
    },
    {
      "id": "31ec967f-fd8a-4cb8-b9e4-6ad8086e08f8",
      "name": "Sticky Note 0004",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -224,
        160
      ],
      "parameters": {
        "color": 5,
        "width": 200,
        "height": 340,
        "content": "## Step 3 - Read contacts from Google Sheets\n\nReads every row from your contacts tab. You must:\n1. Select your own spreadsheet in this node after import\n2. Pick the tab containing the contact list\n\nSee the \"Sheet schema\" sticky note for the exact columns expected."
      },
      "typeVersion": 1
    },
    {
      "id": "e70bc4b8-84f0-4142-9d80-0f0a9d440b52",
      "name": "Sticky Note 0005",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        -64
      ],
      "parameters": {
        "color": 6,
        "width": 200,
        "height": 568,
        "content": "## Step 4 - Filter by region, skip already-sent, cap at daily limit\n\nFor the active `group`:\n- Matches each row's `Country` against the country -> region lookup in the code\n- Drops rows whose `email_sent` column is not empty\n- Drops rows without a valid `@` in the `Email` column\n- Slices the result down to the region's `daily_limit`\n\nAdd or remove countries in the `countryGroups` object inside this node to match your audience."
      },
      "typeVersion": 1
    },
    {
      "id": "3b186678-144c-41d7-b769-7ee8c479453d",
      "name": "Sticky Note 0006",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        224,
        -32
      ],
      "parameters": {
        "color": 4,
        "width": 200,
        "height": 524,
        "content": "## Step 5 - Build personalised email\n\nA `Set` node assembles the outgoing email into these fields:\n- `to` - the recipient address\n- `subject` - subject line (supports expressions like `{{ $json['Company'] }}`)\n- `body` - HTML body (supports `{{ $json['First Name'] }}` and any other column)\n- `group_sent` - recorded later in the sheet for audit\n\nReplace the placeholder subject and HTML body with your own copy."
      },
      "typeVersion": 1
    },
    {
      "id": "f8fa6bd7-e6fb-4108-a542-771b9b40f011",
      "name": "Sticky Note 0007",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        464,
        128
      ],
      "parameters": {
        "color": 5,
        "width": 200,
        "height": 360,
        "content": "## Step 6 - Send via Gmail\n\nSends through your connected Gmail account. Set `senderName` in the node options to your display name.\n\n`onError: continueRegularOutput` means a failed send does NOT stop the batch - the error object flows downstream so the failed row can be logged separately."
      },
      "typeVersion": 1
    },
    {
      "id": "be031510-a25c-4085-9f4e-2b7b7d1b338f",
      "name": "Sticky Note 0008",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        -16
      ],
      "parameters": {
        "color": 3,
        "width": 300,
        "height": 300,
        "content": "## Step 7 - Success / error branching\n\nThe `IF` node checks whether the Gmail response contains an `error` object:\n- **true (error exists)** -> `Update Row Error` writes `status = failed` + the error message\n- **false (no error)** -> `Update Row Success` writes `status = sent`\n\nBoth branches match on the `Email` column so the right row is updated."
      },
      "typeVersion": 1
    },
    {
      "id": "5a937088-6dd9-4406-a7a2-8d6540e33749",
      "name": "Sticky Note 0009",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1216,
        336
      ],
      "parameters": {
        "color": 6,
        "width": 340,
        "height": 556,
        "content": "## Sheet schema\n\nCreate a tab (e.g. `Contacts`) with these columns in the first row:\n\n- `First Name`\n- `Company`  *(used in the subject placeholder; optional if you rewrite the subject)*\n- `Email`\n- `Country`  *(must match a key in the country -> region map)*\n- `email_sent`   *(leave blank; filled by the workflow)*\n- `sent_date`    *(leave blank)*\n- `status`       *(leave blank)*\n- `group_sent`   *(leave blank)*\n- `error_msg`    *(leave blank)*\n\n### Supported countries out of the box\nEU_UK: Austria, Belgium, France, Germany, Italy, Netherlands, Norway, Spain, Sweden, Switzerland, United Kingdom\nNA: Canada, United States\nAU: Australia\n\nAdd more inside the `Filter and Limit` node."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "",
  "connections": {
    "Send Gmail": {
      "main": [
        [
          {
            "node": "Update Row Success",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update Row Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Email": {
      "main": [
        [
          {
            "node": "Send Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Contacts": {
      "main": [
        [
          {
            "node": "Filter and Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Determine Group": {
      "main": [
        [
          {
            "node": "Read Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter and Limit": {
      "main": [
        [
          {
            "node": "Build Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Trigger AU 1:00 UTC": {
      "main": [
        [
          {
            "node": "Determine Group",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Trigger NA 18:00 UTC": {
      "main": [
        [
          {
            "node": "Determine Group",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Trigger EU_UK 10:00 UTC": {
      "main": [
        [
          {
            "node": "Determine Group",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}