{ "name": "Google Sheets lead list \u2192 emails, phones & deliverability (Apify, by ShepHouse \u2013 AI-operated)", "nodes": [ { "id": "n1", "name": "Run daily at 8 AM", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.2, "position": [ 0, 0 ], "parameters": { "rule": { "interval": [ { "triggerAtHour": 8 } ] } } }, { "id": "n2", "name": "Or run manually", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ 0, 200 ], "parameters": {} }, { "id": "n3", "name": "Read websites tab", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.5, "position": [ 220, 100 ], "parameters": { "documentId": { "__rl": true, "mode": "url", "value": "", "cachedResultName": "Paste your Google Sheet URL" }, "sheetName": { "__rl": true, "mode": "name", "value": "websites" }, "options": {} }, "credentials": { "googleSheetsOAuth2Api": { "id": "", "name": "Google Sheets account" } }, "alwaysOutputData": true }, { "id": "n4", "name": "Pick unprocessed rows, normalise, dedupe", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 440, 100 ], "parameters": { "jsCode": "// Input tab columns: website | status. Rows with status 'done' are skipped.\n// Rows are only marked 'done' AFTER a successful Apify call, so failed domains are retried next run.\nconst seen = new Set(), out = [];\nfor (const { json: r } of $input.all()) {\n if (!r || String(r.status || '').toLowerCase() === 'done') continue;\n const s = String(r.website || '').trim().toLowerCase();\n const m = s.match(/^(?:[a-z]+:\\/\\/)?(?:www\\.)?([a-z0-9.-]+\\.[a-z]{2,})(?::\\d+)?(?:[\\/?#].*)?$/);\n if (!m || seen.has(m[1])) continue;\n seen.add(m[1]);\n out.push({ json: { website: r.website, domain: m[1] } });\n}\nreturn out;" }, "alwaysOutputData": true }, { "id": "n5", "name": "Anything to enrich?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 660, 100 ], "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "c1", "leftValue": "={{ $json.domain }}", "rightValue": "", "operator": { "type": "string", "operation": "exists", "singleValue": true } } ], "combinator": "and" }, "options": {} } }, { "id": "n6", "name": "Nothing new: stop (no Apify call)", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [ 880, 260 ], "parameters": {} }, { "id": "n7", "name": "Batches of 25 websites", "type": "n8n-nodes-base.splitInBatches", "typeVersion": 3, "position": [ 880, 80 ], "parameters": { "batchSize": 25, "options": {} } }, { "id": "n8", "name": "Apify: website-contact-enricher (run-sync)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1100, 80 ], "parameters": { "method": "POST", "url": "https://api.apify.com/v2/acts/shephouse~website-contact-enricher/run-sync-get-dataset-items", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth", "sendQuery": true, "queryParameters": { "parameters": [ { "name": "timeout", "value": "280" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ websites: $input.all().map(i => i.json.domain), maxPagesPerDomain: 6, defaultCountry: 'US', delaySecs: 1 }) }}", "options": { "timeout": 300000 } }, "credentials": { "httpHeaderAuth": { "id": "", "name": "Apify API token (header auth)" } }, "executeOnce": true, "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 5000, "onError": "continueErrorOutput" }, { "id": "n9", "name": "Flatten one row per website", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1320, 0 ], "parameters": { "jsCode": "// One clean row per website. Domains the Actor returned nothing for get status NO_RESULT (not charged).\nconst batch = $('Batches of 25 websites').all().map(i => i.json);\nconst byDomain = {};\nfor (const { json: r } of $input.all()) if (r && r.domain) byDomain[String(r.domain).toLowerCase().replace(/^www\\./,'')] = r;\nreturn batch.map(b => {\n const r = byDomain[b.domain] || {};\n const d = r.deliverability || {};\n return { json: {\n website: b.website, domain: b.domain, status: r.status || 'NO_RESULT',\n contactability_score: r.contactabilityScore ?? 0,\n emails: (r.emails || []).join(', '),\n phones_e164: (r.phones || []).map(p => p.e164).filter(Boolean).join(', '),\n deliverability: d.status || 'UNKNOWN', mail_provider: d.mailProvider || '',\n spf: d.spf && d.spf.present ? (d.spf.policy || 'present') : 'missing',\n dmarc: d.dmarc && d.dmarc.present ? (d.dmarc.policy || 'present') : 'missing',\n linkedin: (r.socialLinks || {}).linkedin || '',\n checked_at: r.checkedAt || new Date().toISOString(),\n } };\n});" } }, { "id": "n10", "name": "Route: deliverability, phone, score", "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [ 1540, 0 ], "parameters": { "rules": { "values": [ { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "r1", "leftValue": "={{ $json.deliverability === 'READY' && $json.emails !== '' && $json.contactability_score >= 50 }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Email-ready" }, { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose", "version": 2 }, "conditions": [ { "id": "r2", "leftValue": "={{ $json.phones_e164 !== '' && $json.contactability_score >= 30 }}", "rightValue": true, "operator": { "type": "boolean", "operation": "true", "singleValue": true } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "Phone-first" } ] }, "options": { "fallbackOutput": "extra", "renameFallbackOutput": "Skipped" } } }, { "id": "n11", "name": "Sheet: email-ready", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.5, "position": [ 1780, -140 ], "parameters": { "operation": "appendOrUpdate", "documentId": { "__rl": true, "mode": "url", "value": "", "cachedResultName": "Paste your Google Sheet URL" }, "sheetName": { "__rl": true, "mode": "name", "value": "email-ready" }, "columns": { "mappingMode": "autoMapInputData", "value": {}, "matchingColumns": [ "domain" ] }, "options": {} }, "credentials": { "googleSheetsOAuth2Api": { "id": "", "name": "Google Sheets account" } } }, { "id": "n12", "name": "Sheet: phone-first", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.5, "position": [ 1780, 0 ], "parameters": { "operation": "appendOrUpdate", "documentId": { "__rl": true, "mode": "url", "value": "", "cachedResultName": "Paste your Google Sheet URL" }, "sheetName": { "__rl": true, "mode": "name", "value": "phone-first" }, "columns": { "mappingMode": "autoMapInputData", "value": {}, "matchingColumns": [ "domain" ] }, "options": {} }, "credentials": { "googleSheetsOAuth2Api": { "id": "", "name": "Google Sheets account" } } }, { "id": "n13", "name": "Sheet: skipped", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.5, "position": [ 1780, 140 ], "parameters": { "operation": "appendOrUpdate", "documentId": { "__rl": true, "mode": "url", "value": "", "cachedResultName": "Paste your Google Sheet URL" }, "sheetName": { "__rl": true, "mode": "name", "value": "skipped" }, "columns": { "mappingMode": "autoMapInputData", "value": {}, "matchingColumns": [ "domain" ] }, "options": {} }, "credentials": { "googleSheetsOAuth2Api": { "id": "", "name": "Google Sheets account" } } }, { "id": "n14", "name": "Mark input rows done", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.5, "position": [ 1540, 200 ], "parameters": { "operation": "update", "documentId": { "__rl": true, "mode": "url", "value": "", "cachedResultName": "Paste your Google Sheet URL" }, "sheetName": { "__rl": true, "mode": "name", "value": "websites" }, "columns": { "mappingMode": "defineBelow", "value": { "website": "={{ $json.website }}", "status": "done" }, "matchingColumns": [ "website" ] }, "options": {} }, "credentials": { "googleSheetsOAuth2Api": { "id": "", "name": "Google Sheets account" } } }, { "id": "n15", "name": "Failed batch: one row per domain", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1320, 320 ], "parameters": { "jsCode": "// Apify still failing after 3 tries (bad token, timeout, out of credit...). Log each domain; input rows stay unmarked so the next run retries them.\nconst err = $input.first().json.error || $input.first().json;\nconst msg = typeof err === 'string' ? err : (err.message || JSON.stringify(err)).slice(0, 300);\nreturn $('Batches of 25 websites').all().map(i => ({ json: { domain: i.json.domain, website: i.json.website, error: msg, failed_at: new Date().toISOString() } }));" } }, { "id": "n16", "name": "Sheet: failed (log)", "type": "n8n-nodes-base.googleSheets", "typeVersion": 4.5, "position": [ 1540, 320 ], "parameters": { "operation": "append", "documentId": { "__rl": true, "mode": "url", "value": "", "cachedResultName": "Paste your Google Sheet URL" }, "sheetName": { "__rl": true, "mode": "name", "value": "failed" }, "columns": { "mappingMode": "autoMapInputData", "value": {} }, "options": {} }, "credentials": { "googleSheetsOAuth2Api": { "id": "", "name": "Google Sheets account" } } }, { "id": "n17", "name": "Next batch", "type": "n8n-nodes-base.noOp", "typeVersion": 1, "position": [ 1780, 280 ], "parameters": {} }, { "id": "n18", "name": "About", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -60, -460 ], "parameters": { "content": "## Google Sheets lead list \u2192 emails, phones & deliverability\nBuilt by **ShepHouse**, an **AI-operated** micro-company (this template and its notes were produced by AI agents; a human owner oversees the business).\n\nReads websites from a Sheet, enriches them in batches with the Apify Actor `shephouse/website-contact-enricher` (public emails, validated E.164 phones, MX/SPF/DMARC check, 0\u2013100 contactability score), then writes each lead to the right tab.\n\n**Cost:** Actor is pay-per-result, ~$3 per 1,000 websites with results; websites with nothing found are not charged (plus a tiny run-start fee). Apify's free plan includes monthly credit.", "height": 380, "width": 520, "color": 4 } }, { "id": "n19", "name": "Setup", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 520, -460 ], "parameters": { "content": "## Setup (5 min)\n1. Make a Google Sheet with tabs **websites** (columns `website`, `status`), **email-ready**, **phone-first**, **skipped**, **failed**. Row 1 = headers in result tabs can be blank; n8n adds them.\n2. Paste the Sheet URL into every Google Sheets node and connect a Google Sheets credential.\n3. Create a **Header Auth** credential: name `Authorization`, value `Bearer ` and select it in the Apify node.\n4. Add websites, run manually once, then activate the daily schedule.", "height": 380, "width": 520, "color": 6 } }, { "id": "n20", "name": "Routing", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 1500, -460 ], "parameters": { "content": "## Routing & error handling\n- **email-ready**: MX/SPF ok (`READY`), at least one email, score \u2265 50.\n- **phone-first**: has a validated phone, score \u2265 30. Check do-not-call rules before calling.\n- **skipped**: no MX, no contacts or low score.\n- Apify call retries 3\u00d7 (5 s apart). If it still fails, every domain in the batch is logged to **failed** and its input row is **not** marked done, so the next run retries it.\n- Input rows are marked `done` only after a successful call. Re-runs update rows by `domain` (no duplicates).\n- Only email public business contacts and follow CAN-SPAM/GDPR.", "height": 380, "width": 520, "color": 5 } } ], "connections": { "Run daily at 8 AM": { "main": [ [ { "node": "Read websites tab", "type": "main", "index": 0 } ] ] }, "Or run manually": { "main": [ [ { "node": "Read websites tab", "type": "main", "index": 0 } ] ] }, "Read websites tab": { "main": [ [ { "node": "Pick unprocessed rows, normalise, dedupe", "type": "main", "index": 0 } ] ] }, "Pick unprocessed rows, normalise, dedupe": { "main": [ [ { "node": "Anything to enrich?", "type": "main", "index": 0 } ] ] }, "Anything to enrich?": { "main": [ [ { "node": "Batches of 25 websites", "type": "main", "index": 0 } ], [ { "node": "Nothing new: stop (no Apify call)", "type": "main", "index": 0 } ] ] }, "Batches of 25 websites": { "main": [ [], [ { "node": "Apify: website-contact-enricher (run-sync)", "type": "main", "index": 0 } ] ] }, "Apify: website-contact-enricher (run-sync)": { "main": [ [ { "node": "Flatten one row per website", "type": "main", "index": 0 } ], [ { "node": "Failed batch: one row per domain", "type": "main", "index": 0 } ] ] }, "Flatten one row per website": { "main": [ [ { "node": "Route: deliverability, phone, score", "type": "main", "index": 0 }, { "node": "Mark input rows done", "type": "main", "index": 0 } ] ] }, "Route: deliverability, phone, score": { "main": [ [ { "node": "Sheet: email-ready", "type": "main", "index": 0 } ], [ { "node": "Sheet: phone-first", "type": "main", "index": 0 } ], [ { "node": "Sheet: skipped", "type": "main", "index": 0 } ] ] }, "Mark input rows done": { "main": [ [ { "node": "Batches of 25 websites", "type": "main", "index": 0 } ] ] }, "Failed batch: one row per domain": { "main": [ [ { "node": "Sheet: failed (log)", "type": "main", "index": 0 } ] ] }, "Sheet: failed (log)": { "main": [ [ { "node": "Next batch", "type": "main", "index": 0 } ] ] }, "Next batch": { "main": [ [ { "node": "Batches of 25 websites", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" }, "pinData": {}, "meta": { "templateCredsSetupCompleted": false } }