This workflow corresponds to n8n.io template #15185 — we link there as the canonical source.
This workflow follows the Gmail → Google Sheets 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 →
{
"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
}
]
]
}
}
}
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.
gmailOAuth2googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Three scheduled triggers fire on weekdays at region-appropriate working hours: EU/UK at 10:00 UTC, North America at 18:00 UTC, and Australia at 01:00 UTC All three feed one shared pipeline, so there is no duplicated logic across regions A code node detects the active region from…
Source: https://n8n.io/workflows/15185/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow runs on scheduled weekly and monthly triggers to generate unified marketing performance reports. It processes multiple websites by collecting analytics data, paid ads performance, and CR
Watch target companies for C-level and VP hiring signals, then send AI-personalized outreach emails when leadership roles are posted.
Boost your meeting conversion rates with this Automated Meeting Booking Sequence! This workflow automatically follows up with unbooked leads after 24 hours, sends personalized emails with calendar lin
Monitor customers for competitor tech adoption via PredictLeads and alert CSMs to prevent churn.
This workflow is designed for marketing teams, data analysts, and business owners who need to consistently track key performance indicators (KPIs). It saves hours of manual data collection and reporti