This workflow corresponds to n8n.io template #11329 — 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": "OH01AzmPYqPkOEkE",
"name": "Automate Personalized HR Email Outreach with Rate Limiting",
"tags": [],
"nodes": [
{
"id": "802728cf-d421-4192-abb7-aacca3475e8b",
"name": "Main Sticky",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2864,
256
],
"parameters": {
"color": 2,
"width": 500,
"height": 696,
"content": "## Automate Personalized HR Email Outreach with Rate Limiting\n\nThis workflow streamlines HR outreach by fetching contact data, validating emails, enforcing daily sending limits, and sending personalized emails with attachments, all while logging activity.\n\n### How it works\n1. Read HR contact data from Google Sheets.\n2. Remove duplicates and validate email formats.\n3. Apply dynamic daily email sending limits.\n4. Generate personalized email content.\n5. Download resumes for attachments.\n6. Send emails via Gmail with attachments.\n7. Log sending status (success/failure) to Google Sheets.\n\n### Setup\n1. Configure Google Sheets credentials.\n2. Configure Gmail OAuth2 credentials.\n3. Update 'Google Sheets - Read HR Data' with your document and sheet IDs.\n4. Define email content in 'Email Creator' node.\n5. Set 'Download Resume' URL to your resume repository.\n6. Update 'Log to Google Sheets' with your tracking sheet IDs.\n\n### Customization\nAdjust the 'Rate Limiter' node's RAMP_START and LIMIT_BY_WEEK variables to match your desired sending schedule and volume."
},
"typeVersion": 1
},
{
"id": "0c65668e-bad4-4c65-a3e4-b0a8c2a957f5",
"name": "Email Validation1",
"type": "n8n-nodes-base.if",
"notes": "Validates email format and filters generic emails",
"position": [
-1504,
416
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "88d7ea8a-c3c3-4b6f-ac8a-aac877f13bda",
"operator": {
"type": "string",
"operation": "regex"
},
"leftValue": "={{$json[\"Email\"]}}",
"rightValue": "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
},
{
"id": "07f8bfdf-368f-46a9-9600-be3556706046",
"operator": {
"type": "string",
"operation": "notRegex"
},
"leftValue": "={{$json[\"Email\"]}}",
"rightValue": "^(?:info|support|sales|admin|no[-.]?reply|noreply|contact|help|service|marketing|team|hello|hi)@"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "ee9cdafc-6566-4e83-9d2e-690aef94b650",
"name": "Rate Limiter",
"type": "n8n-nodes-base.code",
"position": [
-1136,
400
],
"parameters": {
"jsCode": "// Get input items\nconst items = $input.all();\n\n// Config - Updated to start from current date\nconst RAMP_START = new Date('2025-09-21'); // Changed from '2025-09-28'\nconst LIMIT_BY_WEEK = [150]; // weeks 1..4+, capped at last\n\n// Week since start (1-based, clamped)\nconst msPerWeek = 7 * 24 * 60 * 60 * 1000;\nconst weeksSinceStart = Math.floor((Date.now() - RAMP_START.getTime()) / msPerWeek) + 1;\nconst dailyLimit = LIMIT_BY_WEEK[Math.min(weeksSinceStart, LIMIT_BY_WEEK.length) - 1];\n\n// Local \"today\" to avoid UTC boundary surprises (00:00 local)\nconst now = new Date();\nconst today = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;\n\n// Get/reset counter\nlet sentToday = $getWorkflowStaticData('node');\nif (!sentToday.date || sentToday.date !== today) {\n sentToday.date = today;\n sentToday.count = 0;\n}\n\n// Calculate how many emails we can still send today\nconst remainingToday = Math.max(0, dailyLimit - sentToday.count);\n\nconsole.log('Rate limiting info:', { \n today, \n currentCount: sentToday.count, \n dailyLimit, \n remainingToday, \n weeksSinceStart,\n totalInputEmails: items.length\n});\n\n// If no emails can be sent today, return empty array\nif (remainingToday === 0) {\n console.log('Daily limit reached - no emails will be sent');\n return [];\n}\n\n// Take only the emails we can send today\nconst emailsToSend = items.slice(0, remainingToday);\n\n// DON'T UPDATE COUNTER HERE - wait until emails are actually sent\nconsole.log(`Preparing to send ${emailsToSend.length} emails. Current count: ${sentToday.count}/${dailyLimit}`);\n\n// Return ALL emails to send with metadata\nreturn emailsToSend.map((item, index) => ({\n json: { \n ...item.json, \n canSend: true, \n currentSentToday: sentToday.count,\n dailyLimit, \n weeksSinceStart,\n batchSize: emailsToSend.length,\n emailIndex: index + 1,\n totalToSend: emailsToSend.length\n }\n}));"
},
"typeVersion": 2
},
{
"id": "554bee55-818f-444c-b337-6fdd80ea385d",
"name": "Email Creator",
"type": "n8n-nodes-base.code",
"position": [
-928,
400
],
"parameters": {
"jsCode": "YOUR_URL_HERE"
},
"typeVersion": 2
},
{
"id": "26af4f1e-94f2-48b6-a9e1-46cbf45124a3",
"name": "Download Resume",
"type": "n8n-nodes-base.httpRequest",
"notes": "Downloads resume as binary data",
"position": [
-80,
416
],
"parameters": {
"url": "YOUR_GOOGLE_DRIVE_URL_HERE",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
}
},
"executeOnce": true,
"typeVersion": 4.2,
"alwaysOutputData": true
},
{
"id": "ae321150-8ac7-45de-89b7-ae22f7b80d6b",
"name": "Send Gmail",
"type": "n8n-nodes-base.gmail",
"notes": "Send email to actual recipient with resume",
"position": [
144,
416
],
"parameters": {
"sendTo": "={{$json.Email}}",
"message": "={{ $json.emailBody }}",
"options": {
"attachmentsUi": {
"attachmentsBinary": [
{}
]
}
},
"subject": "={{ $json.emailSubject }}"
},
"typeVersion": 2.1,
"continueOnFail": true,
"alwaysOutputData": true
},
{
"id": "7f75bd28-658e-4bc0-93ff-80977d88fd69",
"name": "Google Sheets - Read HR Data",
"type": "n8n-nodes-base.googleSheets",
"notes": "Import your HR contacts CSV to Google Sheets first",
"position": [
-2016,
416
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "YOUR_RESOURCE_ID_HERE"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_RESOURCE_ID_HERE"
}
},
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "4cbb8ff6-55b8-4726-b1a8-cf5f236cca53",
"name": "Remove Duplicates",
"type": "n8n-nodes-base.removeDuplicates",
"notes": "Removes duplicate email addresses",
"position": [
-1712,
416
],
"parameters": {
"options": {}
},
"typeVersion": 2
},
{
"id": "2f2f8673-627e-42f7-88ab-8a64d2fc0e2e",
"name": "Update Counter1",
"type": "n8n-nodes-base.code",
"position": [
576,
320
],
"parameters": {
"jsCode": "// Update the counter for successfully sent email\nconst item = $input.first();\n\n// Get today's date\nconst now = new Date();\nconst today = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;\n\n// Get/update counter\nlet sentToday = $getWorkflowStaticData('node');\nif (!sentToday.date || sentToday.date !== today) {\n sentToday.date = today;\n sentToday.count = 0;\n}\n\n// Increment counter by 1 for this successfully sent email\nsentToday.count += 1;\n\nconsole.log(`Email sent successfully to: ${item.json.Email}`);\nconsole.log(`Total sent today: ${sentToday.count}`);\n\n// Return the item with success status\nreturn [{\n json: {\n ...item.json,\n emailStatus: 'sent',\n totalSentToday: sentToday.count,\n sentDate: new Date().toISOString(),\n success: true\n }\n}];"
},
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "cca22795-e112-450a-a86c-5f34e3d36fe5",
"name": "Handle Failures1",
"type": "n8n-nodes-base.code",
"position": [
576,
480
],
"parameters": {
"jsCode": "// Handle failed email\nconst item = $input.first();\n\n// Extract error message if available\nlet errorMessage = 'Unknown error';\nif (item.json.error) {\n errorMessage = typeof item.json.error === 'string' ? item.json.error : JSON.stringify(item.json.error);\n} else if (item.json.message) {\n errorMessage = item.json.message;\n}\n\nconsole.log(`Email failed to send to: ${item.json.Email}`);\nconsole.log(`Error: ${errorMessage}`);\n\n// Return item with failed status\nreturn [{\n json: {\n ...item.json,\n emailStatus: 'failed',\n failureReason: errorMessage,\n failedDate: new Date().toISOString(),\n success: false\n }\n}];"
},
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "779d1514-6b77-4c16-b88f-4e4db7c45d8a",
"name": "Log to Google Sheets1",
"type": "n8n-nodes-base.googleSheets",
"notes": "Log sent emails for tracking",
"position": [
1104,
400
],
"parameters": {
"columns": {
"value": {
"Name ": "={{ $json.Name }}",
"email ": "={{ $json.Email }}",
"status ": "={{ $json.emailStatus }}",
"Company ": "={{ $json.Company }}",
"sentDate ": "={{ $now.format('MM-DD HH:mm:ss') }}",
"emailSubject": "={{ $json.emailSubject }}"
},
"schema": [
{
"id": "email ",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "email ",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Name ",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Name ",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Company ",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Company ",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "status ",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "status ",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "sentDate ",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "sentDate ",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "emailSubject",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "emailSubject",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "YOUR_RESOURCE_ID_HERE"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_RESOURCE_ID_HERE"
}
},
"typeVersion": 4.7
},
{
"id": "49081a57-e8e2-4d7c-a74a-ff1ea2f8fbde",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"position": [
816,
400
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "5afe75fb-0362-442c-b2d5-25873059a159",
"name": "emailStatus",
"type": "string",
"value": "={{ $json.emailStatus }}"
},
{
"id": "63ca4777-d636-4bbc-8e90-84b13485b820",
"name": "failureReason",
"type": "string",
"value": "={{ $json.failureReason }}"
},
{
"id": "8d5ea9a4-b935-492a-913c-7e3b644bb1de",
"name": "SNo",
"type": "number",
"value": "={{ $('Download Resume').item.json.SNo }}"
},
{
"id": "cd380045-b087-441f-9207-916ad351b779",
"name": "Name",
"type": "string",
"value": "={{ $('Download Resume').item.json.Name }}"
},
{
"id": "f897bb52-37a1-4979-b042-00bdbea25470",
"name": "Email",
"type": "string",
"value": "={{ $('Download Resume').item.json.Email }}"
},
{
"id": "3ac6efb9-23c2-4810-ad72-28a9bd1fdc01",
"name": "Title",
"type": "string",
"value": "={{ $('Download Resume').item.json.Title }}"
},
{
"id": "044f8868-2669-461f-ba70-bd067489d37b",
"name": "Company",
"type": "string",
"value": "={{ $('Download Resume').item.json.Company }}"
},
{
"id": "7cd30591-5fa5-497b-8eac-0732afd712db",
"name": "emailSubject",
"type": "string",
"value": "={{ $('Download Resume').item.json.emailSubject }}"
},
{
"id": "cf53b800-264f-4813-9da9-8c072f46356a",
"name": "firstName",
"type": "string",
"value": "={{ $('Download Resume').item.json.firstName }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "668eb556-46e1-4820-8367-4d7bd06130e3",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-2272,
352
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 9
}
]
}
},
"typeVersion": 1.2
},
{
"id": "c3ef6f9f-f42a-4b99-a477-30a424d0d03a",
"name": "If",
"type": "n8n-nodes-base.if",
"position": [
352,
416
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "88364e2e-629a-43b0-8c40-30196f20b27c",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.id }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "c9d72b91-1196-4a99-813b-545c23fa6e65",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-560,
400
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "6f82fe6f-57e4-457f-ad69-c1fa4aa7c422",
"name": "Wait",
"type": "n8n-nodes-base.wait",
"position": [
-320,
416
],
"parameters": {
"amount": 60
},
"typeVersion": 1.1
},
{
"id": "84bab284-deed-4df4-a33d-e567a18311dc",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-2272,
512
],
"parameters": {},
"typeVersion": 1
},
{
"id": "0e97630e-7045-4e67-98a9-acbb573877f8",
"name": "Section 1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2320,
256
],
"parameters": {
"color": 7,
"width": 508,
"height": 424,
"content": "## 1. Trigger & Data Import"
},
"typeVersion": 1
},
{
"id": "50b86aa6-ea5b-4ec5-940f-7ec64011a101",
"name": "Section 2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1760,
256
],
"parameters": {
"color": 7,
"width": 492,
"height": 424,
"content": "## 2. Data Cleanup & Validation"
},
"typeVersion": 1
},
{
"id": "b935df91-6f2b-414e-9cf1-6e964539acbf",
"name": "Section 3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1216,
256
],
"parameters": {
"color": 7,
"width": 508,
"height": 424,
"content": "## 3. Rate Limiting & Email Prep"
},
"typeVersion": 1
},
{
"id": "dbad85bc-bcd6-446c-b334-a20d11dc45ed",
"name": "Section 4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-656,
256
],
"parameters": {
"color": 7,
"width": 1612,
"height": 424,
"content": "## 4. Send & Handle Email"
},
"typeVersion": 1
},
{
"id": "168165d4-9520-4bfb-8f7f-e6d2335311e8",
"name": "Section 5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1008,
256
],
"parameters": {
"color": 7,
"width": 300,
"height": 424,
"content": "## 5. Log Results"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"connections": {
"If": {
"main": [
[
{
"node": "Update Counter1",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Failures1",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "Download Resume",
"type": "main",
"index": 0
}
]
]
},
"Send Gmail": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields1": {
"main": [
[
{
"node": "Log to Google Sheets1",
"type": "main",
"index": 0
}
]
]
},
"Rate Limiter": {
"main": [
[
{
"node": "Email Creator",
"type": "main",
"index": 0
}
]
]
},
"Email Creator": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Download Resume": {
"main": [
[
{
"node": "Send Gmail",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"Update Counter1": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
},
"Handle Failures1": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Google Sheets - Read HR Data",
"type": "main",
"index": 0
}
]
]
},
"Email Validation1": {
"main": [
[
{
"node": "Rate Limiter",
"type": "main",
"index": 0
}
]
]
},
"Remove Duplicates": {
"main": [
[
{
"node": "Email Validation1",
"type": "main",
"index": 0
}
]
]
},
"Log to Google Sheets1": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets - Read HR Data": {
"main": [
[
{
"node": "Remove Duplicates",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Google Sheets - Read HR Data",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow streamlines HR outreach by fetching contact data, validating emails, enforcing daily sending limits, and sending personalized emails with attachments, all while logging activity. Read HR contact data from Google Sheets. Remove duplicates and validate email formats.…
Source: https://n8n.io/workflows/11329/ — 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.
Automatically extract structured information from emails using AI-powered document analysis. This workflow processes emails from specified domains, classifies them by type, and extracts structured dat
What This Flow Does
This n8n template allows you to automatically monitor your company's budget by comparing live Bexio accounting data against targets defined in Google Sheets, sending automated weekly email reports. It
Activate this workflow once and every Friday at 5PM it automatically pulls your week's meeting data from Fireflies, calculates seven metrics, and emails a formatted report to your manager inbox. It tr
This workflow automatically monitors solar energy production every 2 hours by fetching data from the Energidataservice API. If the energy output falls below a predefined threshold, it instantly notifi