AutomationFlowsEmail & Gmail › Send Ramped Cold Email Sequences with Gmail, Google Sheets, and Anthropic Claude

Send Ramped Cold Email Sequences with Gmail, Google Sheets, and Anthropic Claude

Byshafeel @shafeel on n8n.io

This workflow automates a 3-step cold email sequence from Gmail using leads in Google Sheets, generates personalized copy with Anthropic Claude, enforces a gradual daily sending cap, schedules follow-ups in the same thread, and marks leads as replied when inbox responses are…

Cron / scheduled trigger★★★★★ complexity33 nodesGoogle SheetsHTTP RequestGmail
Email & Gmail Trigger: Cron / scheduled Nodes: 33 Complexity: ★★★★★ Added:

This workflow corresponds to n8n.io template #16141 — 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 →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Cold Email Engine (Sender + Follow-ups + Reply Detection)",
  "nodes": [
    {
      "id": "every-35-min-weekdays-9-4",
      "name": "Every 35 min (weekdays 9-4)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/35 9-16 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "get-leads",
      "name": "Get leads",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        220,
        0
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1tpCenP_1Qmj4f3DCQPO3iSd7Kz2fdz_G0m7BYuiQpio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "pick-next-lead",
      "name": "Pick next lead",
      "type": "n8n-nodes-base.code",
      "position": [
        440,
        0
      ],
      "parameters": {
        "jsCode": "// Warm-up ramp + daily cap + pick the next un-contacted lead.\nconst staticData = $getWorkflowStaticData('global');\nconst now = new Date();\nif (!staticData.startDate) { staticData.startDate = now.toISOString(); }\nconst start = new Date(staticData.startDate);\nconst dayNum = Math.floor((now - start) / (1000 * 60 * 60 * 24)) + 1;\n\nlet cap;\nif (dayNum <= 7) cap = 5;\nelse if (dayNum <= 14) cap = 8;\nelse if (dayNum <= 21) cap = 12;\nelse cap = 15;\n\nconst todayStr = now.toISOString().slice(0, 10);\nconst rows = items.map(i => i.json);\nconst sentToday = rows.filter(r => String(r.sent_at || '').slice(0, 10) === todayStr).length;\nif (sentToday >= cap) {\n  return [{ json: { proceed: false, reason: 'Daily cap reached', cap, sentToday, dayNum } }];\n}\n\nconst next = rows.find(r => {\n  const status = String(r.status || '').trim().toLowerCase();\n  const email = String(r['Email'] || '').trim();\n  return email.includes('@') && (status === '' || status === 'pending');\n});\nif (!next) {\n  return [{ json: { proceed: false, reason: 'No pending leads left', cap, sentToday, dayNum } }];\n}\nreturn [{ json: { proceed: true, cap, sentToday, dayNum, lead: next, row_number: next.row_number } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "within-daily-limit",
      "name": "Within daily limit?",
      "type": "n8n-nodes-base.if",
      "position": [
        660,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.proceed }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "build-email-prompt",
      "name": "Build email prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        0
      ],
      "parameters": {
        "jsCode": "const lead = $json.lead || {};\n\nconst system = `You are Shafeel writing a SHORT, casual, confident cold email to one prospect. You build AI agent systems and outbound automation for companies and agencies, done-for-you on commission.\n\nSTYLE RULES:\n- 3 to 4 short sentences total. Never more.\n- Plain text only. Sound like one founder writing to another: confident, direct, human, not salesy.\n- Open with \"Hey [first name],\" then ONE genuine, specific sentence about their company based on the company description provided. Make it real and personal, never generic flattery.\n- NEVER use em-dashes or double hyphens. Use commas, periods, or new sentences instead.\n- No buzzwords, no exclamation marks, no marketing-speak, no ALL CAPS, no links, no images.\n- Use ONLY the true facts below. Never invent results, numbers, clients, or claims.\n- Close with the call to action, then sign off on its own line with just: Shafeel\n\nTRUE FACTS (do not go beyond these):\n- I recently added $20K for a SaaS client in Indiana by building them an outbound system.\n- I build AI agent systems and outbound automation for clients, working on commission.\n- Guarantee I can make: I will get you your next 5 clients or I will work for free until I do.\n\nCALL TO ACTION (phrase it naturally, do not paste any link):\n- Offer a quick 15 minute call later today or tomorrow afternoon, and say you will send a Google Meet link.\n\nOutput STRICT JSON only, no markdown, exactly:\n{\"subject\": \"...\", \"body\": \"...\"}\nThe subject must be casual, lowercase-ish, under 5 words, and must not look like an ad.`;\n\nconst userMsg = `Write the cold email now for this lead.\nFirst name: ${lead['First Name'] || 'there'}\nCompany: ${lead['Company Name'] || ''}\nTitle: ${lead['Title'] || ''}\nIndustry: ${lead['Industry'] || ''}\nCompany description (use this for the personalized opening line): ${lead['Company Short Description'] || lead['Headline'] || 'no description available'}\n\nIf no company description is available, open with a genuine line based on their title or industry instead. Keep it to 3 or 4 short sentences. No em-dashes. Sign off as Shafeel.`;\n\nconst body = { model: 'claude-sonnet-4-6', max_tokens: 500, system, messages: [{ role: 'user', content: userMsg }] };\nreturn [{ json: { body } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "write-email-claude",
      "name": "Write email (Claude)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1100,
        0
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "anthropicApi"
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "parse-email",
      "name": "Parse email",
      "type": "n8n-nodes-base.code",
      "position": [
        1320,
        0
      ],
      "parameters": {
        "jsCode": "const resp = $json;\nlet text = '';\ntry { text = resp.content && resp.content[0] && resp.content[0].text ? resp.content[0].text : ''; } catch (e) { text = ''; }\ntext = text.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/```\\s*$/i, '').trim();\nlet parsed;\ntry { parsed = JSON.parse(text); } catch (e) { parsed = { subject: 'quick question', body: text || 'Hi, reaching out.' }; }\n\nconst decide = $('Pick next lead').item.json;\nconst lead = decide.lead || {};\nreturn [{ json: {\n  to: lead['Email'],\n  first_name: lead['First Name'] || '',\n  company: lead['Company Name'] || '',\n  row_number: decide.row_number,\n  subject: String(parsed.subject || 'quick question').slice(0, 120),\n  body: String(parsed.body || '').trim()\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "send-email",
      "name": "Send email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1540,
        0
      ],
      "parameters": {
        "sendTo": "={{ $json.to }}",
        "message": "={{ $json.body }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "={{ $json.subject }}",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "build-row-update",
      "name": "Build row update",
      "type": "n8n-nodes-base.set",
      "position": [
        1760,
        0
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "row-number-a",
              "name": "row_number",
              "type": "number",
              "value": "={{ $('Parse email').item.json.row_number }}"
            },
            {
              "id": "status-a",
              "name": "status",
              "type": "string",
              "value": "active"
            },
            {
              "id": "sent-at-a",
              "name": "sent_at",
              "type": "string",
              "value": "={{ $now.toFormat('yyyy-LL-dd HH:mm:ss') }}"
            },
            {
              "id": "subject-sent-a",
              "name": "subject_sent",
              "type": "string",
              "value": "={{ $('Parse email').item.json.subject }}"
            },
            {
              "id": "stage-a",
              "name": "stage",
              "type": "number",
              "value": "1"
            },
            {
              "id": "thread-id-a",
              "name": "thread_id",
              "type": "string",
              "value": "={{ $json.threadId }}"
            },
            {
              "id": "message-id-a",
              "name": "message_id",
              "type": "string",
              "value": "={{ $json.id }}"
            }
          ]
        },
        "includeOtherFields": false
      },
      "typeVersion": 3.4
    },
    {
      "id": "mark-lead-as-sent",
      "name": "Mark lead as sent",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1980,
        0
      ],
      "parameters": {
        "columns": {
          "value": {
            "stage": "={{ $json.stage }}",
            "status": "={{ $json.status }}",
            "sent_at": "={{ $json.sent_at }}",
            "thread_id": "={{ $json.thread_id }}",
            "message_id": "={{ $json.message_id }}",
            "row_number": "={{ $json.row_number }}",
            "subject_sent": "={{ $json.subject_sent }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ]
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1tpCenP_1Qmj4f3DCQPO3iSd7Kz2fdz_G0m7BYuiQpio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "pause-after-send",
      "name": "Pause after send",
      "type": "n8n-nodes-base.wait",
      "position": [
        2200,
        0
      ],
      "parameters": {
        "unit": "seconds",
        "amount": "={{ Math.floor(Math.random() * 90) + 30 }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "nothing-to-send",
      "name": "Nothing to send",
      "type": "n8n-nodes-base.noOp",
      "position": [
        880,
        160
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "every-40-min-weekdays-9-4",
      "name": "Every 40 min (weekdays 9-4)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        460
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/40 9-16 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "get-leads-follow-up",
      "name": "Get leads (follow-up)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        220,
        460
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1tpCenP_1Qmj4f3DCQPO3iSd7Kz2fdz_G0m7BYuiQpio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "decide-follow-up",
      "name": "Decide follow-up",
      "type": "n8n-nodes-base.code",
      "position": [
        440,
        460
      ],
      "parameters": {
        "jsCode": "// Pick the next ACTIVE lead whose follow-up is due.\n// Shares the same daily ramp cap as the initial sender (counts sent_at == today).\nconst sd = $getWorkflowStaticData('global');\nconst now = new Date();\nif (!sd.startDate) sd.startDate = now.toISOString();\nconst dayNum = Math.floor((now - new Date(sd.startDate)) / 864e5) + 1;\n\nlet cap;\nif (dayNum <= 7) cap = 5;\nelse if (dayNum <= 14) cap = 8;\nelse if (dayNum <= 21) cap = 12;\nelse cap = 15;\n\nconst today = now.toISOString().slice(0, 10);\nconst rows = items.map(i => i.json);\nconst sentToday = rows.filter(r => String(r.sent_at || '').slice(0, 10) === today).length;\nif (sentToday >= cap) {\n  return [{ json: { proceed: false, reason: 'Daily cap reached', cap, sentToday } }];\n}\n\n// Days to wait: 3 days before email 2 (stage 1), 4 more before email 3 (stage 2).\nconst DELAY = { 1: 3, 2: 4 };\nconst daysSince = ts => {\n  if (!ts) return 999;\n  const d = new Date(String(ts).slice(0, 10));\n  return (now - d) / 864e5;\n};\n\nconst next = rows.find(r => {\n  const status = String(r.status || '').trim().toLowerCase();\n  const stage = Number(r.stage || 0);\n  const email = String(r['Email'] || '').trim();\n  return email.includes('@') && status === 'active' && (stage === 1 || stage === 2) && daysSince(r.sent_at) >= DELAY[stage];\n});\nif (!next) {\n  return [{ json: { proceed: false, reason: 'No follow-ups due', cap, sentToday } }];\n}\n\nreturn [{ json: {\n  proceed: true, cap, sentToday,\n  lead: next, row_number: next.row_number,\n  stage: Number(next.stage || 0),\n  message_id: next.message_id,\n  thread_id: next.thread_id,\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "follow-up-due",
      "name": "Follow-up due?",
      "type": "n8n-nodes-base.if",
      "position": [
        660,
        460
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.proceed }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "build-follow-up",
      "name": "Build follow-up",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        460
      ],
      "parameters": {
        "jsCode": "// ====== SET YOUR NAME ======\nconst SENDER_NAME = 'Shafeel';\n// ===========================\n\nconst lead = $json.lead || {};\nconst stage = Number($json.stage);   // 1 => sending email 2; 2 => sending email 3 (final)\nconst fuNum = stage;\n\nconst system = `You are ${SENDER_NAME} writing a SHORT follow-up to a cold email the prospect did not reply to. This is follow-up number ${fuNum} of 2.\n\nRULES:\n- 1 to 3 sentences only. Shorter than the first email.\n- Friendly and low pressure. No guilt, no \"just bumping this\", no pushiness.\n- Add ONE new angle, a quick proof point, or a simple yes or no question. Do NOT repeat the whole pitch.\n- If this is follow-up number 2, make it a short, polite final check-in and offer to close the loop.\n- NEVER use em-dashes or double hyphens. No links, no images. Plain text.\n- Sign off on its own line with just: ${SENDER_NAME}\n\nOutput STRICT JSON only, no markdown, exactly:\n{\"subject\": \"\", \"body\": \"...\"}\nLeave subject as an empty string, the email is sent as a reply in the same thread.`;\n\nconst userMsg = `Prospect first name: ${lead['First Name'] || 'there'}\nCompany: ${lead['Company Name'] || ''}\nWhat they do: ${lead['Company Short Description'] || lead['Headline'] || 'unknown'}`;\n\nconst body = { model: 'claude-sonnet-4-6', max_tokens: 300, system, messages: [{ role: 'user', content: userMsg }] };\nreturn [{ json: { body } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "write-follow-up-claude",
      "name": "Write follow-up (Claude)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1100,
        460
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "anthropicApi"
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "parse-follow-up",
      "name": "Parse follow-up",
      "type": "n8n-nodes-base.code",
      "position": [
        1320,
        460
      ],
      "parameters": {
        "jsCode": "// Parse the follow-up text and carry the IDs needed to reply + update the row.\nconst resp = $json;\nlet text = '';\ntry { text = resp.content && resp.content[0] && resp.content[0].text ? resp.content[0].text : ''; } catch (e) { text = ''; }\ntext = text.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '').replace(/```\\s*$/i, '').trim();\nlet p;\ntry { p = JSON.parse(text); } catch (e) { p = { subject: '', body: text || 'Just following up in case this is useful.' }; }\n\nconst d = $('Decide follow-up').item.json;\nreturn [{ json: {\n  message_id: d.message_id,\n  thread_id: d.thread_id,\n  row_number: d.row_number,\n  stage: d.stage,\n  next_stage: Number(d.stage) + 1,\n  body: String(p.body || '').trim(),\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "send-reply",
      "name": "Send reply",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1540,
        460
      ],
      "parameters": {
        "message": "={{ $json.body }}",
        "options": {
          "appendAttribution": false
        },
        "emailType": "text",
        "messageId": "={{ $json.message_id }}",
        "operation": "reply"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "mark-stage-sent",
      "name": "Mark stage + sent",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1760,
        460
      ],
      "parameters": {
        "columns": {
          "value": {
            "stage": "={{ $('Parse follow-up').item.json.next_stage }}",
            "status": "={{ $('Parse follow-up').item.json.next_stage >= 3 ? 'done' : 'active' }}",
            "sent_at": "={{ $now.toFormat('yyyy-LL-dd HH:mm:ss') }}",
            "row_number": "={{ $('Parse follow-up').item.json.row_number }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ]
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1tpCenP_1Qmj4f3DCQPO3iSd7Kz2fdz_G0m7BYuiQpio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "pause-after-reply",
      "name": "Pause after reply",
      "type": "n8n-nodes-base.wait",
      "position": [
        1980,
        460
      ],
      "parameters": {
        "unit": "seconds",
        "amount": "={{ Math.floor(Math.random() * 90) + 30 }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "no-follow-up-due",
      "name": "No follow-up due",
      "type": "n8n-nodes-base.noOp",
      "position": [
        880,
        620
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "check-inbox-3x-daily",
      "name": "Check inbox 3x daily",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        920
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 10,13,16 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "get-recent-inbox",
      "name": "Get recent inbox",
      "type": "n8n-nodes-base.gmail",
      "position": [
        220,
        920
      ],
      "parameters": {
        "limit": 100,
        "simple": true,
        "filters": {
          "q": "in:inbox newer_than:3d"
        },
        "operation": "getAll",
        "returnAll": false
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "get-inbox-senders",
      "name": "Get inbox senders",
      "type": "n8n-nodes-base.code",
      "position": [
        440,
        920
      ],
      "parameters": {
        "jsCode": "// Collect unique sender email addresses from recent inbox messages.\nconst emails = items.map(i => {\n  const f = i.json.from || i.json.From || '';\n  const m = String(f).match(/[\\w.+-]+@[\\w.-]+\\.[A-Za-z]{2,}/);\n  return m ? m[0].toLowerCase() : null;\n}).filter(Boolean);\nreturn [{ json: { senders: Array.from(new Set(emails)) } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "get-leads-replies",
      "name": "Get leads (replies)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        660,
        920
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1tpCenP_1Qmj4f3DCQPO3iSd7Kz2fdz_G0m7BYuiQpio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "match-repliers",
      "name": "Match repliers",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        920
      ],
      "parameters": {
        "jsCode": "// Any ACTIVE lead who shows up as an inbox sender has replied -> mark them 'replied'\n// so the follow-up flow stops emailing them.\nconst senders = $('Get inbox senders').item.json.senders || [];\nconst set = new Set(senders.map(s => String(s).toLowerCase()));\nconst rows = items.map(i => i.json);\n\nconst out = rows.filter(r => {\n  const email = String(r['Email'] || '').trim().toLowerCase();\n  const status = String(r.status || '').trim().toLowerCase();\n  return email && set.has(email) && status === 'active';\n}).map(r => ({ json: { row_number: r.row_number, status: 'replied' } }));\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "mark-replied",
      "name": "Mark replied",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1100,
        920
      ],
      "parameters": {
        "columns": {
          "value": {
            "status": "={{ $json.status }}",
            "row_number": "={{ $json.row_number }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ]
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Leads"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1tpCenP_1Qmj4f3DCQPO3iSd7Kz2fdz_G0m7BYuiQpio"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "overview",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        -140
      ],
      "parameters": {
        "width": 500,
        "height": 560,
        "content": "## AI cold email engine: sender + follow-ups + reply detection\n\nA complete cold outreach system on Gmail and Claude, built to protect the sending domain.\n\n### How it works\n- **Sender (top):** every 35 min on weekdays, picks the next un-emailed lead within a daily cap that ramps from 5 to 15 over four weeks, writes a personalized email with Claude, sends it, and marks the row active with its thread IDs.\n- **Follow-ups (middle):** chases leads who have not replied. Email 2 after 3 days, email 3 after 4 more, each as a reply in the same thread. After the third the lead is marked done.\n- **Reply detection (bottom):** scans the inbox a few times a day and marks anyone who replied so they leave the sequence.\nA shared daily cap across all three flows keeps total volume safe.\n\n### Setup\nConnect Gmail, Google Sheets, and an Anthropic credential. The Leads sheet needs an Apollo-style header row plus the columns status, sent_at, subject_sent, stage, thread_id, message_id."
      },
      "typeVersion": 1
    },
    {
      "id": "section-sender",
      "name": "Section - Sender",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -40,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 2480,
        "height": 380,
        "content": "## 1. Sender\nPicks the next new lead within the daily ramp cap, writes a personalized email, sends it, and records the thread IDs."
      },
      "typeVersion": 1
    },
    {
      "id": "section-follow-ups",
      "name": "Section - Follow-ups",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -40,
        300
      ],
      "parameters": {
        "color": 7,
        "width": 2260,
        "height": 380,
        "content": "## 2. Follow-ups\nReplies to non-repliers in the same thread: email 2 after 3 days, email 3 after 4 more."
      },
      "typeVersion": 1
    },
    {
      "id": "section-replies",
      "name": "Section - Replies",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -40,
        760
      ],
      "parameters": {
        "color": 7,
        "width": 1360,
        "height": 360,
        "content": "## 3. Reply detection\nMarks anyone who replied so they stop receiving follow-ups."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "timezone": "America/New_York",
    "executionOrder": "v1"
  },
  "connections": {
    "Get leads": {
      "main": [
        [
          {
            "node": "Pick next lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send email": {
      "main": [
        [
          {
            "node": "Build row update",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send reply": {
      "main": [
        [
          {
            "node": "Mark stage + sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse email": {
      "main": [
        [
          {
            "node": "Send email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Follow-up due?": {
      "main": [
        [
          {
            "node": "Build follow-up",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No follow-up due",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match repliers": {
      "main": [
        [
          {
            "node": "Mark replied",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick next lead": {
      "main": [
        [
          {
            "node": "Within daily limit?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build follow-up": {
      "main": [
        [
          {
            "node": "Write follow-up (Claude)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse follow-up": {
      "main": [
        [
          {
            "node": "Send reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build row update": {
      "main": [
        [
          {
            "node": "Mark lead as sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decide follow-up": {
      "main": [
        [
          {
            "node": "Follow-up due?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get recent inbox": {
      "main": [
        [
          {
            "node": "Get inbox senders",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get inbox senders": {
      "main": [
        [
          {
            "node": "Get leads (replies)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark lead as sent": {
      "main": [
        [
          {
            "node": "Pause after send",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark stage + sent": {
      "main": [
        [
          {
            "node": "Pause after reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build email prompt": {
      "main": [
        [
          {
            "node": "Write email (Claude)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get leads (replies)": {
      "main": [
        [
          {
            "node": "Match repliers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Within daily limit?": {
      "main": [
        [
          {
            "node": "Build email prompt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Nothing to send",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check inbox 3x daily": {
      "main": [
        [
          {
            "node": "Get recent inbox",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write email (Claude)": {
      "main": [
        [
          {
            "node": "Parse email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get leads (follow-up)": {
      "main": [
        [
          {
            "node": "Decide follow-up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write follow-up (Claude)": {
      "main": [
        [
          {
            "node": "Parse follow-up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 35 min (weekdays 9-4)": {
      "main": [
        [
          {
            "node": "Get leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 40 min (weekdays 9-4)": {
      "main": [
        [
          {
            "node": "Get leads (follow-up)",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow automates a 3-step cold email sequence from Gmail using leads in Google Sheets, generates personalized copy with Anthropic Claude, enforces a gradual daily sending cap, schedules follow-ups in the same thread, and marks leads as replied when inbox responses are…

Source: https://n8n.io/workflows/16141/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

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

Gmail, HTTP Request, AWS S3 +1
Email & Gmail

What This Flow Does

Gmail, Google Sheets, HTTP Request +1
Email & Gmail

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

Google Sheets, HTTP Request, Gmail
Email & Gmail

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

HTTP Request, Gmail, Google Sheets
Email & Gmail

This workflow runs daily to pull cloud spend from a billing API, compare it to a Google Sheets rolling baseline, and alert on cost spikes by creating a Jira incident, posting to Slack, emailing Financ

HTTP Request, Google Sheets, Jira +2