{
  "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
          }
        ]
      ]
    }
  }
}