This workflow corresponds to n8n.io template #16142 — we link there as the canonical source.
This workflow follows the Google Sheets → HTTP Request 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": "aDuYx4W3amWAx9xO",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "LinkedIn Lead Enrichment Pipeline",
"tags": [],
"nodes": [
{
"id": "7b9a32b4-f18e-4a10-a64b-4b6a7fc5aa6b",
"name": "SerpAPI - Find LinkedIn URL",
"type": "n8n-nodes-base.httpRequest",
"position": [
272,
4608
],
"parameters": {
"url": "https://serpapi.com/search.json",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "api_key",
"value": "YOUR_SERPAPI_KEY"
},
{
"name": "q",
"value": "={{ $json['First Name'] }} {{ $json['Last Name'] }} YOUR_LOCATION site:linkedin.com"
},
{
"name": "engine",
"value": "google"
},
{
"name": "num",
"value": "5"
},
{
"name": "gl",
"value": "ca"
},
{
"name": "hl",
"value": "en"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.4
},
{
"id": "d6429f45-29d1-4bc0-a59a-5c66cca42525",
"name": "Google Sheets - Read Leads (Pipeline 1)",
"type": "n8n-nodes-base.googleSheets",
"position": [
-176,
4608
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupColumn": "enrichment_status"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1100974956,
"cachedResultUrl": "",
"cachedResultName": "Your Sheet Tab"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultUrl": "",
"cachedResultName": "Your Lead Sheet"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "e957f6c5-78b6-4327-b89a-f2f5759598a3",
"name": "Filter & Clean Search Results",
"type": "n8n-nodes-base.code",
"position": [
496,
4608
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item;\nconst results = item.json.organic_results || [];\nconst BLOCKED_DOMAINS = [\n 'facebook.com', 'instagram.com', 'twitter.com', 'tiktok.com',\n 'youtube.com', 'reddit.com', 'wikipedia.org',\n 'yellowpages', 'canada411', 'yelp.com', 'yelp.ca',\n 'whitepages', 'zoominfo.com', 'crunchbase.com', 'bloomberg.com',\n 'remax.ca', 'remax-quebec.com', 'rew.ca', 'realtor.ca',\n 'zolo.ca', 'housesigma.com', 'homelife.ca', 'rankmyagent.com',\n 'rate-my-agent.com', 'royallepage.ca', 'century21.ca',\n 'zoocasa.com', 'kijiji.ca', 'flowercityrealty.com',\n 'executivehomesrealty.ca', 'openroom.ca', 'rentals.ca',\n 'researchgate.net', 'studylib.net', 'datanyze.com',\n 'rocketreach.co', 'nature.com', 'worldbank.org',\n 'pubs.acs.org', 'acpjournals.org', 'hfg.org', 'imdb.com',\n 'osgoode.yorku.ca', 'digitalcommons', 'ubc.ca', 'umanitoba.ca',\n 'gov.uk', 'gloucester-ma.gov', 'transitchicago.com',\n 'cloudfront.net', 'codalab.org', 'pestend.ca',\n 'find-and-update.company-information',\n 'westburyrentals.com', 'myrentalunit.ca',\n 'forumproperties.com', 'thaparteam.ca',\n 'ontario.ca/document', 'digital.ontarioreports.ca'\n];\n\nconst cleanResults = results\n .filter(r => {\n if (!r.link) return false;\n if (!r.link.startsWith('http')) return false;\n if (r.link.match(/\\.(pdf|csv|txt|doc|xls|xlsx|png|jpg|jpeg)(\\?|$)/i)) return false;\n if (BLOCKED_DOMAINS.some(b => r.link.toLowerCase().includes(b))) return false;\n return true;\n })\n .slice(0, 3) // \u2190 changed from 5 to 3\n .map(r => ({\n url: r.link,\n title: r.title || '',\n snippet: (r.snippet || '').substring(0, 150) // \u2190 also reduced from 200 to 150\n }));\n\nreturn {\n clean_results: cleanResults,\n has_results: cleanResults.length > 0\n};"
},
"typeVersion": 2
},
{
"id": "5082b1b7-f46e-4ed5-9d1a-7581235758a4",
"name": "Batch Size Limit",
"type": "n8n-nodes-base.limit",
"position": [
48,
4608
],
"parameters": {
"maxItems": 700
},
"typeVersion": 1
},
{
"id": "bfd7f498-3f59-4d2d-9735-082e588db33a",
"name": "Has Search Results?",
"type": "n8n-nodes-base.if",
"position": [
720,
4608
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4f5a2e35-5370-4c03-a53c-21d8be982144",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.has_results }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "3d2208a0-3f18-41d5-b926-ae73dbd0956f",
"name": "Prepare AI Input",
"type": "n8n-nodes-base.set",
"position": [
944,
4608
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "6471eb39-1688-4763-80cf-eb26fed02154",
"name": "clean_results",
"type": "array",
"value": "={{ $json.clean_results }}"
},
{
"id": "3bdbea53-7a64-4e31-8825-d1e3d06b1032",
"name": "person_name",
"type": "string",
"value": "={{ $('Limit').item.json['First Name'] }} {{ $('Limit').item.json['Last Name'] }}"
},
{
"id": "07151515-b5fa-4e59-a16a-0bcfbc9583a9",
"name": "Licensed as",
"type": "string",
"value": "={{ $('Limit').item.json['Licensed as'] }}"
},
{
"id": "c60b67da-cb6c-43b9-97c2-47e14e2c77ba",
"name": "First Name",
"type": "string",
"value": "={{ $('Limit').item.json['First Name'] }}"
},
{
"id": "0bac7c50-8172-4613-8874-7ac49fad718f",
"name": "Last Name",
"type": "string",
"value": "={{ $('Limit').item.json['Last Name'] }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "379e329f-8967-4cb4-ac6b-a7823f2e3107",
"name": "Google Sheets - Read Leads (Pipeline 2)",
"type": "n8n-nodes-base.googleSheets",
"position": [
2384,
4656
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "true",
"lookupColumn": "is_linkedin"
},
{
"lookupValue": "false",
"lookupColumn": "proceed_to_prospeo"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1100974956,
"cachedResultUrl": "",
"cachedResultName": "Your Sheet Tab"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultUrl": "",
"cachedResultName": "Your Lead Sheet"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "881a4dc9-7e39-4843-89a0-05cbd408166d",
"name": "Rate Limit Wait",
"type": "n8n-nodes-base.wait",
"position": [
2736,
4720
],
"parameters": {},
"typeVersion": 1.1
},
{
"id": "91ae756e-ee10-40fe-9794-f5a05f4dd8d0",
"name": "Process Each Lead",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2896,
4976
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "5a6d747a-ceda-4d7c-a366-5686f78ca23f",
"name": "Google Sheets - Save LinkedIn URL",
"type": "n8n-nodes-base.googleSheets",
"position": [
1984,
4608
],
"parameters": {
"columns": {
"value": {
"serp_url": "={{ $json.serp_url }}",
"ai_reason": "={{ $json.ai_reason }}",
"confidence": "={{ $json.ai_confidence }}",
"row_number": 0,
"is_linkedin": "={{ $json.is_linkedin }}",
"enrichment_status": "={{ $json.enrichment_status }}",
"Complete legal name": "={{ $json['Complete legal name'] }}"
},
"schema": [
{
"id": "Complete legal name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Complete legal name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "First Name",
"type": "string",
"display": true,
"required": false,
"displayName": "First Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Last Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Last Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Licensed as",
"type": "string",
"display": true,
"required": false,
"displayName": "Licensed as",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Employer Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Employer Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Principal Condominium Manager",
"type": "string",
"display": true,
"required": false,
"displayName": "Principal Condominium Manager",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Business Address",
"type": "string",
"display": true,
"required": false,
"displayName": "Business Address",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Business Email Address",
"type": "string",
"display": true,
"required": false,
"displayName": "Business Email Address",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Business Phone Number",
"type": "string",
"display": true,
"required": false,
"displayName": "Business Phone Number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "serp_url",
"type": "string",
"display": true,
"required": false,
"displayName": "serp_url",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "is_linkedin",
"type": "string",
"display": true,
"required": false,
"displayName": "is_linkedin",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "confidence",
"type": "string",
"display": true,
"required": false,
"displayName": "confidence",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "enrichment_status",
"type": "string",
"display": true,
"required": false,
"displayName": "enrichment_status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ai_reason",
"type": "string",
"display": true,
"required": false,
"displayName": "ai_reason",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Complete legal name"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1100974956,
"cachedResultUrl": "",
"cachedResultName": "Your Sheet Tab"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultUrl": "",
"cachedResultName": "Your Lead Sheet"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "941c3ea2-77e4-4a67-a7a6-526d10bb340e",
"name": "AI - Validate LinkedIn Match",
"type": "@n8n/n8n-nodes-langchain.anthropic",
"position": [
1408,
4608
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "claude-haiku-4-5-20251001",
"cachedResultName": "claude-haiku-4-5-20251001"
},
"options": {
"system": "You are a data enrichment assistant for a licensed property manager database in Texas, US. Your job is to analyze Google search results and identify the single best matching URL for a given person.\n\nCRITICAL: Your response must start with { and end with }. Nothing before or after. No explanation. No markdown. No code blocks. Just raw JSON. Keep reason under 10 words.\n\nCONFIDENCE RULE: Only return a URL when you are medium or high confidence. If confidence is low or you are unsure, return null for url and none for type. A wrong result is worse than no result.\n\nPRIORITY ORDER:\n1. LinkedIn personal profile (linkedin.com/in/) where person matches\n2. Any legitimate business website connected to this person\n\nIMPORTANT CONTEXT CHANGE: The search query used was name + Texas only \u2014 no job title was included. This means search results may include people from ANY industry in Texas. You MUST verify the person's role from the LinkedIn headline. Do NOT accept a profile based on name match alone. Role verification is now mandatory.\n\nLINKEDIN HEADLINE RULE (most important):\nBase your decision PRIMARILY on the LinkedIn headline. If the headline clearly shows a wrong role, REJECT immediately. Only use snippet as supporting evidence, never as primary reason to accept.\n\nACCEPT a LinkedIn profile only if ALL of these are true:\n- Person's first name appears in the title or URL\n- LinkedIn HEADLINE shows current or past role in: property management, condominium management, building management, facilities management, strata management, real estate leasing, OLCM, CMRAO, property administrator, building superintendent, or closely related field\n- Confidence is medium or high\n- Do NOT reject based on LinkedIn URL country code alone\n\nREJECT a LinkedIn profile if ANY of these are true:\n- HEADLINE shows NO connection to property/condominium/building management\n- HEADLINE shows wrong industry: petroleum, oil, gas, automotive, pharma, food, retail, computing, software, banking, insurance, airline, hospital, military, mining, academic research, startup\n- HEADLINE shows: REALTOR, Real Estate Agent, Real Estate Broker, mortgage broker\n- HEADLINE shows: entrepreneur, co-founder, founder (unless combined with property management company)\n- HEADLINE shows: Accountant, Financial Analyst, CPA, Controller, Bookkeeper\n- HEADLINE shows: student, intern, PhD researcher, academic\n- First name does not match at all\n- Location explicitly outside Canada AND no Canadian company in result\n- Confidence is low\n\nACCEPT a website only if ALL of these are true:\n- Legitimate business website\n- Person's name OR property management company in title, URL, or snippet\n- Business is in property management or closely related field\n- Confidence is medium or high\n\nREJECT a website if ANY of these are true:\n- Real estate agent listing: remax, royallepage, homelife, century21, zoocasa, zolo, realtor\n- News article, research paper, government document, obituary, legal document\n- Generic directory with no specific connection to this person\n- Social media: facebook, instagram, twitter, tiktok\n- Wrong industry: healthcare, hospital, educational institution, government, religious organization\n- No mention of person's name or employer\n- Confidence is low\n\nRespond with ONLY this JSON, max 50 tokens, no text before or after:\n{\"url\":\"matched url or null\",\"type\":\"linkedin or website or none\",\"confidence\":\"high or medium or low\",\"reason\":\"max 10 words\"}",
"maxTokens": 500,
"temperature": 0
},
"messages": {
"values": [
{
"content": "=Person name: {{ $json['First Name'] }} {{ $json['Last Name'] }}\n\nSearch results:\n{{ JSON.stringify($json.clean_results) }}\n\nRespond with ONLY this JSON starting with { :\n{\"url\":\"matched url or null\",\"type\":\"linkedin or website or none\",\"confidence\":\"high or medium or low\",\"reason\":\"one line\"}"
}
]
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 1
},
{
"id": "11987a6e-1c3d-42df-9e83-16035e0ac401",
"name": "Rate Limit Wait (AI)",
"type": "n8n-nodes-base.wait",
"position": [
1168,
4608
],
"parameters": {
"amount": 3
},
"typeVersion": 1.1
},
{
"id": "ea55b3cd-7cc8-4111-a6b7-49128f4635b2",
"name": "Parse AI Response",
"type": "n8n-nodes-base.code",
"position": [
1760,
4608
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": " const item = $input.item;\n\n// \u2500\u2500 Extract text \u2014 Haiku first, then Gemini fallback \u2500\u2500\u2500\u2500\u2500\nlet aiText = '';\n\n// Haiku response path (content[0].text)\nif (item.json.content?.[0]?.text) {\n aiText = item.json.content[0].text;\n}\n// Gemini Simplify ON path\nelse if (item.json.text) {\n aiText = item.json.text;\n}\n// Gemini Simplify OFF path\nelse if (item.json.candidates?.[0]?.content?.parts?.[0]?.text) {\n aiText = item.json.candidates[0].content.parts[0].text;\n}\nelse if (item.json.content?.parts?.[0]?.text) {\n aiText = item.json.content.parts[0].text;\n}\n\n// Remove markdown backticks if present\naiText = aiText.replace(/```json\\n?|```/g, '').trim();\n\n// \u2500\u2500 Extract JSON using regex \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet cleaned = '';\nconst jsonMatch = aiText.match(/\\{[\\s\\S]*\\}/);\nif (jsonMatch) {\n cleaned = jsonMatch[0];\n}\n\n// \u2500\u2500 Parse JSON \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet aiResult = { url: null, type: 'none', confidence: 'low', reason: 'parse error' };\ntry {\n aiResult = JSON.parse(cleaned);\n} catch(e) {\n const urlMatch = aiText.match(/\"url\"\\s*:\\s*\"([^\"]+)\"/);\n const typeMatch = aiText.match(/\"type\"\\s*:\\s*\"([^\"]+)\"/);\n const confMatch = aiText.match(/\"confidence\"\\s*:\\s*\"([^\"]+)\"/);\n const reasonMatch = aiText.match(/\"reason\"\\s*:\\s*\"([^\"]+)\"/);\n\n aiResult = {\n url: urlMatch ? urlMatch[1] : null,\n type: typeMatch ? typeMatch[1] : 'none',\n confidence: confMatch ? confMatch[1] : 'low',\n reason: reasonMatch ? reasonMatch[1] : 'no match found'\n };\n}\n\n// \u2500\u2500 Sanitize \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst validTypes = ['linkedin', 'website', 'none'];\nconst type = validTypes.includes(aiResult.type) ? aiResult.type : 'none';\nlet url = (type !== 'none' && aiResult.url && aiResult.url !== 'null')\n ? aiResult.url : null;\nconst confidence = aiResult.confidence || 'low';\nconst reason = aiResult.reason || '';\n\n// \u2500\u2500 Enrichment status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet enrichmentStatus = 'no_results';\nif (url && type === 'linkedin') enrichmentStatus = 'linkedin_found';\nif (url && type === 'website') enrichmentStatus = 'website_found_direct';\n\n// \u2500\u2500 Force no_results if low confidence + no match \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (\n confidence === 'low' &&\n reason.toLowerCase().includes('no match')\n) {\n enrichmentStatus = 'no_results';\n url = null;\n}\n\n// \u2500\u2500 Get clean_results from Edit Fields node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst cleanResults = $('Edit Fields').item.json.clean_results || [];\n\n// \u2500\u2500 Extract serp_title and serp_company \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet serpTitle = null;\nlet serpCompany = null;\n\nif (url) {\n const matchedResult = cleanResults.find(r => r.url === url);\n if (matchedResult) {\n serpTitle = matchedResult.title || null;\n\n if (type === 'linkedin' && serpTitle) {\n const atIndex = serpTitle.indexOf(' at ');\n if (atIndex !== -1) {\n const raw = serpTitle\n .substring(atIndex + 4)\n .replace(/\\.{2,}$/, '')\n .trim();\n const INVALID = [\n 'university', 'college', 'student', 'school',\n 'hospital', 'pharma', 'bank', 'petroleum', 'automotive'\n ];\n if (\n !INVALID.some(k => raw.toLowerCase().includes(k)) &&\n raw.length >= 3 &&\n raw.length <= 80\n ) {\n serpCompany = raw;\n }\n }\n }\n }\n}\n\n// \u2500\u2500 Get sheet fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sheetData = $('Limit').item.json;\n\n// \u2500\u2500 Build full person name \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst personName = `${sheetData['First Name'] || ''} ${sheetData['Last Name'] || ''}`.trim();\n\nreturn {\n 'Complete legal name': sheetData['Complete legal name'] || null,\n 'First Name': sheetData['First Name'] || null,\n 'Last Name': sheetData['Last Name'] || null,\n 'Licensed as': sheetData['Licensed as'] || null,\n person_name: personName,\n serp_title: serpTitle,\n serp_url: url,\n serp_company: serpCompany,\n serp_location: sheetData.serp_location || null,\n is_linkedin: type === 'linkedin',\n company_name: null,\n website: type === 'website' ? url : null,\n email: null,\n email_valid: null,\n confidence: sheetData.confidence || null,\n ai_confidence: confidence,\n ai_reason: reason,\n enrichment_status: enrichmentStatus\n};"
},
"typeVersion": 2
},
{
"id": "b50db274-7c57-4195-85ba-05e6d434f760",
"name": "SerpAPI - Find LinkedIn URL (Pass 2)",
"type": "n8n-nodes-base.httpRequest",
"position": [
272,
5312
],
"parameters": {
"url": "https://serpapi.com/search.json",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "api_key",
"value": "YOUR_SERPAPI_KEY"
},
{
"name": "q",
"value": "={{ $json['First Name'] }} {{ $json['Last Name'] }} {{ $json['Employer Name'] }} Ontario site:linkedin.com"
},
{
"name": "engine",
"value": "google"
},
{
"name": "num",
"value": "5"
},
{
"name": "gl",
"value": "ca"
},
{
"name": "hl",
"value": "en"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.4
},
{
"id": "52519a4e-d11a-488f-a0b3-5742fd0779e5",
"name": "Google Sheets - Read No-Result Leads",
"type": "n8n-nodes-base.googleSheets",
"position": [
-240,
5328
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "true",
"lookupColumn": "is_linkedin"
},
{
"lookupValue": "false",
"lookupColumn": "proceed_to_prospeo"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1100974956,
"cachedResultUrl": "",
"cachedResultName": "Your Sheet Tab"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultUrl": "",
"cachedResultName": "Your Lead Sheet"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "634adebe-2160-4681-8a1b-1d40043517b1",
"name": "Filter & Clean Results (Pass 2)",
"type": "n8n-nodes-base.code",
"position": [
496,
5312
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item;\nconst results = item.json.organic_results || [];\nconst BLOCKED_DOMAINS = [\n 'facebook.com', 'instagram.com', 'twitter.com', 'tiktok.com',\n 'youtube.com', 'reddit.com', 'wikipedia.org',\n 'yellowpages', 'canada411', 'yelp.com', 'yelp.ca',\n 'whitepages', 'zoominfo.com', 'crunchbase.com', 'bloomberg.com',\n 'remax.ca', 'remax-quebec.com', 'rew.ca', 'realtor.ca',\n 'zolo.ca', 'housesigma.com', 'homelife.ca', 'rankmyagent.com',\n 'rate-my-agent.com', 'royallepage.ca', 'century21.ca',\n 'zoocasa.com', 'kijiji.ca', 'flowercityrealty.com',\n 'executivehomesrealty.ca', 'openroom.ca', 'rentals.ca',\n 'researchgate.net', 'studylib.net', 'datanyze.com',\n 'rocketreach.co', 'nature.com', 'worldbank.org',\n 'pubs.acs.org', 'acpjournals.org', 'hfg.org', 'imdb.com',\n 'osgoode.yorku.ca', 'digitalcommons', 'ubc.ca', 'umanitoba.ca',\n 'gov.uk', 'gloucester-ma.gov', 'transitchicago.com',\n 'cloudfront.net', 'codalab.org', 'pestend.ca',\n 'find-and-update.company-information',\n 'westburyrentals.com', 'myrentalunit.ca',\n 'forumproperties.com', 'thaparteam.ca',\n 'ontario.ca/document', 'digital.ontarioreports.ca'\n];\n\nconst cleanResults = results\n .filter(r => {\n if (!r.link) return false;\n if (!r.link.startsWith('http')) return false;\n if (r.link.match(/\\.(pdf|csv|txt|doc|xls|xlsx|png|jpg|jpeg)(\\?|$)/i)) return false;\n if (BLOCKED_DOMAINS.some(b => r.link.toLowerCase().includes(b))) return false;\n return true;\n })\n .slice(0, 3) // \u2190 changed from 5 to 3\n .map(r => ({\n url: r.link,\n title: r.title || '',\n snippet: (r.snippet || '').substring(0, 150) // \u2190 also reduced from 200 to 150\n }));\n\nreturn {\n clean_results: cleanResults,\n has_results: cleanResults.length > 0\n};"
},
"typeVersion": 2
},
{
"id": "f9f21901-aef5-40e7-bc88-8a259588ac2f",
"name": "Batch Size Limit (Pass 2)",
"type": "n8n-nodes-base.limit",
"position": [
80,
5312
],
"parameters": {
"maxItems": 31
},
"typeVersion": 1
},
{
"id": "1942f7c2-2a72-402c-9edd-7d6038ac5f88",
"name": "Has Search Results? (Pass 2)",
"type": "n8n-nodes-base.if",
"position": [
688,
5328
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4f5a2e35-5370-4c03-a53c-21d8be982144",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.has_results }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "bb571b1e-017c-4f6d-8e78-6a6e7a92a8d2",
"name": "Prepare AI Input (Pass 2)",
"type": "n8n-nodes-base.set",
"position": [
912,
5312
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "6471eb39-1688-4763-80cf-eb26fed02154",
"name": "clean_results",
"type": "array",
"value": "={{ $json.clean_results }}"
},
{
"id": "3bdbea53-7a64-4e31-8825-d1e3d06b1032",
"name": "person_name",
"type": "string",
"value": "={{ $('Limit1').item.json['First Name'] }} {{ $('Limit1').item.json['Last Name'] }}"
},
{
"id": "07151515-b5fa-4e59-a16a-0bcfbc9583a9",
"name": "Licensed as",
"type": "string",
"value": "={{ $('Limit1').item.json['Licensed as'] }}"
},
{
"id": "c60b67da-cb6c-43b9-97c2-47e14e2c77ba",
"name": "First Name",
"type": "string",
"value": "={{ $('Limit1').item.json['First Name'] }}"
},
{
"id": "0bac7c50-8172-4613-8874-7ac49fad718f",
"name": "Last Name",
"type": "string",
"value": "={{ $('Limit1').item.json['Last Name'] }}"
},
{
"id": "bc9ba76d-e6cb-4c62-b6c9-d0f6d74d055f",
"name": "Employer Name",
"type": "string",
"value": "={{ $('Limit1').item.json['Employer Name'] }}"
},
{
"id": "ef205822-ea3a-49e5-b819-8480de3821fa",
"name": "row_number",
"type": "number",
"value": "={{ $('Limit1').item.json.row_number }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "32fb0df2-5251-43b1-9a0f-6168510edbf6",
"name": "Google Sheets - Save Results (Pass 2)",
"type": "n8n-nodes-base.googleSheets",
"position": [
1840,
5312
],
"parameters": {
"columns": {
"value": {
"serp_url": "={{ $json.serp_url }}",
"ai_reason": "={{ $json.ai_reason }}",
"confidence": "={{ $json.ai_confidence }}",
"row_number": 0,
"is_linkedin": "={{ $json.is_linkedin }}",
"enrichment_status": "={{ $json.enrichment_status }}",
"Complete legal name": "={{ $json['Complete legal name'] }}"
},
"schema": [
{
"id": "Complete legal name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Complete legal name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "First Name",
"type": "string",
"display": true,
"required": false,
"displayName": "First Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Last Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Last Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Licensed as",
"type": "string",
"display": true,
"required": false,
"displayName": "Licensed as",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Employer Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Employer Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Principal Condominium Manager",
"type": "string",
"display": true,
"required": false,
"displayName": "Principal Condominium Manager",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Business Address",
"type": "string",
"display": true,
"required": false,
"displayName": "Business Address",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Business Email Address",
"type": "string",
"display": true,
"required": false,
"displayName": "Business Email Address",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Business Phone Number",
"type": "string",
"display": true,
"required": false,
"displayName": "Business Phone Number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "serp_url",
"type": "string",
"display": true,
"required": false,
"displayName": "serp_url",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "is_linkedin",
"type": "string",
"display": true,
"required": false,
"displayName": "is_linkedin",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "confidence",
"type": "string",
"display": true,
"required": false,
"displayName": "confidence",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "enrichment_status",
"type": "string",
"display": true,
"required": false,
"displayName": "enrichment_status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ai_reason",
"type": "string",
"display": true,
"required": false,
"displayName": "ai_reason",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Complete legal name"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1100974956,
"cachedResultUrl": "",
"cachedResultName": "Your Sheet Tab"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEET_ID",
"cachedResultUrl": "",
"cachedResultName": "Your Lead Sheet"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "1f55fe0a-c349-4424-8c7e-ab827d81ba6a",
"name": "AI - Validate LinkedIn Match (Pass 2)",
"type": "@n8n/n8n-nodes-langchain.anthropic",
"position": [
1328,
5312
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "claude-haiku-4-5-20251001",
"cachedResultName": "claude-haiku-4-5-20251001"
},
"options": {
"system": "You are a data enrichment assistant for a licensed property manager database in Texas, US. Your job is to analyze Google search results and identify the single best matching URL for a given person.\n\nCRITICAL: Your response must start with { and end with }. Nothing before or after. No explanation. No markdown. No code blocks. Just raw JSON. Keep reason under 10 words.\n\nCONFIDENCE RULE: Only return a URL when you are medium or high confidence. If confidence is low or you are unsure, return null for url and none for type. A wrong result is worse than no result.\n\nPRIORITY ORDER:\n1. LinkedIn personal profile (linkedin.com/in/) where person matches\n2. Any legitimate business website connected to this person\n\nIMPORTANT CONTEXT CHANGE: The search query used was name + Texas only \u2014 no job title was included. This means search results may include people from ANY industry in Texas. You MUST verify the person's role from the LinkedIn headline. Do NOT accept a profile based on name match alone. Role verification is now mandatory.\n\nLINKEDIN HEADLINE RULE (most important):\nBase your decision PRIMARILY on the LinkedIn headline. If the headline clearly shows a wrong role, REJECT immediately. Only use snippet as supporting evidence, never as primary reason to accept.\n\nACCEPT a LinkedIn profile only if ALL of these are true:\n- Person's first name appears in the title or URL\n- LinkedIn HEADLINE shows current or past role in: property management, condominium management, building management, facilities management, strata management, real estate leasing, OLCM, CMRAO, property administrator, building superintendent, or closely related field\n- Confidence is medium or high\n- Do NOT reject based on LinkedIn URL country code alone\n\nREJECT a LinkedIn profile if ANY of these are true:\n- HEADLINE shows NO connection to property/condominium/building management\n- HEADLINE shows wrong industry: petroleum, oil, gas, automotive, pharma, food, retail, computing, software, banking, insurance, airline, hospital, military, mining, academic research, startup\n- HEADLINE shows: REALTOR, Real Estate Agent, Real Estate Broker, mortgage broker\n- HEADLINE shows: entrepreneur, co-founder, founder (unless combined with property management company)\n- HEADLINE shows: Accountant, Financial Analyst, CPA, Controller, Bookkeeper\n- HEADLINE shows: student, intern, PhD researcher, academic\n- First name does not match at all\n- Location explicitly outside Canada AND no Canadian company in result\n- Confidence is low\n\nACCEPT a website only if ALL of these are true:\n- Legitimate business website\n- Person's name OR property management company in title, URL, or snippet\n- Business is in property management or closely related field\n- Confidence is medium or high\n\nREJECT a website if ANY of these are true:\n- Real estate agent listing: remax, royallepage, homelife, century21, zoocasa, zolo, realtor\n- News article, research paper, government document, obituary, legal document\n- Generic directory with no specific connection to this person\n- Social media: facebook, instagram, twitter, tiktok\n- Wrong industry: healthcare, hospital, educational institution, government, religious organization\n- No mention of person's name or employer\n- Confidence is low\n\nRespond with ONLY this JSON, max 50 tokens, no text before or after:\n{\"url\":\"matched url or null\",\"type\":\"linkedin or website or none\",\"confidence\":\"high or medium or low\",\"reason\":\"max 10 words\"}",
"maxTokens": 300,
"temperature": 0
},
"messages": {
"values": [
{
"content": "=Person name: {{ $json['First Name'] }} {{ $json['Last Name'] }}\n\nEmployer: {{ $json['Employer Name'] }}\n\nSearch results:\n{{ JSON.stringify($json.clean_results) }}\n\nRespond with ONLY this JSON:\n{\"url\":\"matched url or null\",\"type\":\"linkedin or website or none\",\"confidence\":\"high or medium or low\",\"reason\":\"max 10 words\"}"
}
]
}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 1
},
{
"id": "8b52c99e-0427-44b2-bf23-7ff354f3e469",
"name": "Parse AI Response (Pass 2)",
"type": "n8n-nodes-base.code",
"position": [
1632,
5312
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item;\n\n// \u2500\u2500 Extract text \u2014 Haiku first, then Gemini fallback \u2500\u2500\u2500\u2500\u2500\nlet aiText = '';\n\n// Haiku response path (content[0].text)\nif (item.json.content?.[0]?.text) {\n aiText = item.json.content[0].text;\n}\n// Gemini Simplify ON path\nelse if (item.json.text) {\n aiText = item.json.text;\n}\n// Gemini Simplify OFF path\nelse if (item.json.candidates?.[0]?.content?.parts?.[0]?.text) {\n aiText = item.json.candidates[0].content.parts[0].text;\n}\nelse if (item.json.content?.parts?.[0]?.text) {\n aiText = item.json.content.parts[0].text;\n}\n\n// Remove markdown backticks if present\naiText = aiText.replace(/```json\\n?|```/g, '').trim();\n\n// \u2500\u2500 Extract JSON using regex \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet cleaned = '';\nconst jsonMatch = aiText.match(/\\{[\\s\\S]*\\}/);\nif (jsonMatch) {\n cleaned = jsonMatch[0];\n}\n\n// \u2500\u2500 Parse JSON \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet aiResult = { url: null, type: 'none', confidence: 'low', reason: 'parse error' };\ntry {\n aiResult = JSON.parse(cleaned);\n} catch(e) {\n const urlMatch = aiText.match(/\"url\"\\s*:\\s*\"([^\"]+)\"/);\n const typeMatch = aiText.match(/\"type\"\\s*:\\s*\"([^\"]+)\"/);\n const confMatch = aiText.match(/\"confidence\"\\s*:\\s*\"([^\"]+)\"/);\n const reasonMatch = aiText.match(/\"reason\"\\s*:\\s*\"([^\"]+)\"/);\n\n aiResult = {\n url: urlMatch ? urlMatch[1] : null,\n type: typeMatch ? typeMatch[1] : 'none',\n confidence: confMatch ? confMatch[1] : 'low',\n reason: reasonMatch ? reasonMatch[1] : 'no match found'\n };\n}\n\n// \u2500\u2500 Sanitize \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst validTypes = ['linkedin', 'website', 'none'];\nconst type = validTypes.includes(aiResult.type) ? aiResult.type : 'none';\nlet url = (type !== 'none' && aiResult.url && aiResult.url !== 'null')\n ? aiResult.url : null;\nconst confidence = aiResult.confidence || 'low';\nconst reason = aiResult.reason || '';\n\n// \u2500\u2500 Enrichment status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet enrichmentStatus = 'no_results';\nif (url && type === 'linkedin') enrichmentStatus = 'linkedin_found';\nif (url && type === 'website') enrichmentStatus = 'website_found_direct';\n\n// \u2500\u2500 Force no_results if low confidence + no match \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (\n confidence === 'low' &&\n reason.toLowerCase().includes('no match')\n) {\n enrichmentStatus = 'no_results';\n url = null;\n}\n\n// \u2500\u2500 Get clean_results from Edit Fields node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst cleanResults = $('Edit Fields1').item.json.clean_results || [];\n\n// \u2500\u2500 Extract serp_title and serp_company \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet serpTitle = null;\nlet serpCompany = null;\n\nif (url) {\n const matchedResult = cleanResults.find(r => r.url === url);\n if (matchedResult) {\n serpTitle = matchedResult.title || null;\n\n if (type === 'linkedin' && serpTitle) {\n const atIndex = serpTitle.indexOf(' at ');\n if (atIndex !== -1) {\n const raw = serpTitle\n .substring(atIndex + 4)\n .replace(/\\.{2,}$/, '')\n .trim();\n const INVALID = [\n 'university', 'college', 'student', 'school',\n 'hospital', 'pharma', 'bank', 'petroleum', 'automotive'\n ];\n if (\n !INVALID.some(k => raw.toLowerCase().includes(k)) &&\n raw.length >= 3 &&\n raw.length <= 80\n ) {\n serpCompany = raw;\n }\n }\n }\n }\n}\n\n// \u2500\u2500 Get sheet fields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sheetData = $('Limit1').item.json;\n\n// \u2500\u2500 Build full person name \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst personName = `${sheetData['First Name'] || ''} ${sheetData['Last Name'] || ''}`.trim();\n\nreturn {\n 'Complete legal name': sheetData['Complete legal name'] || null,\n 'First Name': sheetData['First Name'] || null,\n 'Last Name': sheetData['Last Name'] || null,\n 'Licensed as': sheetData['Licensed as'] || null,\n 'Employer Name': sheetData['Employer Name'] || null, // \u2190 added\n person_name: personName,\n serp_title: serpTitle,\n serp_url: url,\n serp_company: serpCompany,\n serp_location: sheetData.serp_location || null,\n is_linkedin: type === 'linkedin',\n company_name: null,\n website: type === 'website' ? url : null,\n email: null,\n email_valid: null,\n confidence: sheetData.confidence || null,\n ai_confidence: confidence,\n ai_reason: reason,\n enrichment_status: enrichmentStatus\n};"
},
"typeVersion": 2
},
{
"id": "d45c8b5a-7f89-4704-9943-4c8a7d6b9dbc",
"name": "Filter Unprocessed Records",
"type": "n8n-nodes-base.filter",
"position": [
-80,
5488
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "22ce8fb2-0888-4b4b-92f1-71acf8ee6508",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.row_number }}",
"rightValue": 3333
}
]
}
},
"typeVersion": 2.3
},
{
"id": "3ddfbad2-e969-4a85-ab95-8ec4df2885e8",
"name": "Rate Limit Wait (Pass 3)",
"type": "n8n-nodes-base.wait",
"position": [
1376,
5472
],
"parameters": {
"amount": 3
},
"typeVersion": 1.1
},
{
"id": "14054169-9250-4852-96c3-05407e86713d",
"name": "Batch Size Limit (Pipeline 2)",
"type": "n8n-nodes-base.limit",
"position": [
2576,
4928
],
"parameters": {
"maxItems": 51
},
"typeVersion": 1
},
{
"id": "67a3c9c9-a294-48a2-9252-e3ec9f5cb3db",
"name": "Filter LinkedIn Found",
"type": "n8n-nodes-base.filter",
"position": [
1840,
5504
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "22ce8fb2-0888-4b4b-92f1-71acf8ee6508",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.is_linkedin }}",
"rightValue": 51
}
]
}
},
"typeVersion": 2.3
},
{
"id": "82dc2a93-2a81-444c-885f-1601a20c470f",
"name": "Filter Ready for Apify",
"type": "n8n-nodes-base.filter",
"position": [
2400,
4928
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "22ce8fb2-0888-4b4b-92f1-71acf8ee6508",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.row_number }}",
"rightValue": 2317
}
]
},
"looseTypeValidation": true
},
"typeVersion": 2.3
},
{
"id": "ca72366e-f933-4b18-abc3-d180abda241d",
"name": "Apify - Scrape LinkedIn Profile",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
2912,
4720
],
"parameters": {
"url": "https://api.apify.com/v2/actors/harvestapi~linkedin-profile-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN_HERE",
"method": "POST",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
},
"jsonBody": "={\n \"urls\": [\"{{ $json.serp_url }}\"],\n \"scrapeMode\": \"email\"\n}",
"sendBody": true,
"specifyBody": "json"
},
"notesInFlow": false,
"retryOnFail": true,
"typeVersion": 4.4
},
{
"id": "74e8c077-43cd-44fd-ad2b-b7d75b72ed8d",
"name": "SerpAPI - Find Company Domain",
"type": "n8n-nodes-base.httpRequest",
"position": [
3312,
4720
],
"parameters": {
"url": "https://serpapi.com/search.json",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "api_key",
"value": "YOUR_SERPAPI_KEY"
},
{
"name": "q",
"value": "={{ $json['First Name'] }} {{ $json['Last Name'] }} YOUR_LOCATION site:linkedin.com"
},
{
"name": "engine",
"value": "google"
},
{
"name": "num",
"value": "5"
},
{
"name": "gl",
"value": "ca"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.4
},
{
"id": "8d03dcdb-a9be-4810-ab27-8559287dbaa3",
"name": "Prepare Domain Search",
"type": "n8n-nodes-base.set",
"position": [
3536,
4720
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "5a412912-0d59-4e40-8499-c7bf4c1bff6c",
"name": "company_name_clean",
"type": "string",
"value": "={{ $('Parse Linkedin data').item.json.company_name_clean }}"
},
{
"id": "ca265875-a221-4aed-8bd9-f8e147fe12fc",
"name": "company_name",
"type": "string",
"value": "={{ $('Parse Linkedin data').item.json.company_name }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "fedb5601-aa1d-4e22-89f0-1750da466476",
"name": "Extract Company Domain",
"type": "n8n-nodes-base.code",
"position": [
3696,
4720
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\n\n// \u2500\u2500 Company name \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst companyName = item.company_name_clean || \n item.company_name || \n (item.search_parameters?.q || '').replace(/\\s+Ontario\\s*$/i, '').trim() ||\n 'unknown';\n\n// \u2500\u2500 Skip domains \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst SKIP_DOMAINS = [\n 'linkedin.com', 'facebook.com', 'instagram.com',\n 'twitter.com', 'youtube.com', 'wikipedia.org',\n 'yellowpages', 'canada411', 'yelp.com', 'yelp.ca',\n 'zoominfo.com', 'crunchbase.com', 'bloomberg.com',\n 'remax.ca', 'realtor.ca', 'zoocasa.com',\n 'bbb.org', 'glassdoor.com', 'indeed.com',\n 'kijiji.ca', 'rentals.ca', 'padmapper.com',\n 'cmrao.ca', 'ontario.ca', 'canada.ca',\n 'google.com', 'serpapi.com',\n 'tripadvisor.com', 'lnkd.in', 'tinyurl.com',\n 'nationalpost.com', 'bit.ly', 'nextdoor.com',\n 'mapquest.com', 'apple.com', 'bing.com'\n];\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction extractDomain(url) {\n if (!url) return null;\n let s = url.trim();\n s = s.replace(/^https?:\\/\\//i, '');\n s = s.replace(/^www\\./i, '');\n s = s.split('/')[0];\n s = s.split('?')[0];\n s = s.split('#')[0];\n s = s.toLowerCase().trim();\n if (s.includes('.') && s.length > 4 && !s.includes(' ')) {\n return s;\n }\n return null;\n}\n\nfunction isSkipped(url) {\n if (!url) return true;\n const lower = url.toLowerCase();\n return SKIP_DOMAINS.some(s => lower.includes(s));\n}\n\n// Extract domain from snippet text\nfunction extractFromSnippet(snippet) {\n if (!snippet) return null;\n // Look for explicit URL in snippet\n const urlMatch = snippet.match(/https?:\\/\\/[^\\s,;\\\"\\'<>]+/);\n if (urlMatch) {\n const url = urlMatch[0].replace(/[.,;)>]$/, '');\n if (!isSkipped(url)) {\n return extractDomain(url);\n }\n }\n // Look for domain pattern like \"www.something.com\" or \"something.ca\"\n const domainMatch = snippet.match(/(?:www\\.)?([a-zA-Z0-9-]+\\.(?:com|ca|org|net|co))/);\n if (domainMatch) {\n const d = domainMatch[0].replace(/^www\\./, '').toLowerCase();\n if (!isSkipped(d) && d.length > 4) {\n return d;\n }\n }\n return null;\n}\n\nlet domain = null;\nlet domainSource = null;\nlet domainMethod = null;\n\n// \u2500\u2500 Method 1: Knowledge Graph website \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst kgWebsite = item.knowledge_graph?.website;\nif (kgWebsite && !isSkipped(kgWebsite)) {\n const d = extractDomain(kgWebsite);\n if (d) {\n domain = d;\n domainSource = kgWebsite;\n domainMethod = 'knowledge_graph_website';\n }\n}\n\n// \u2500\u2500 Method 2: Knowledge Graph appointment providers \u2500\u2500\u2500\u2500\u2500\u2500\nif (!domain) {\n const
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.
anthropicApigoogleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow enriches leads from Google Sheets by finding and validating LinkedIn profile URLs with SerpAPI and Anthropic, then scraping matched profiles with Apify to extract a relevant company and discover its website domain via SerpAPI, saving results back to Google Sheets.…
Source: https://n8n.io/workflows/16142/ — 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.
The competitive edge, delivered. This Customer Intelligence Engine simultaneously analyzes the web, Reddit, and X/Twitter to generate a professional, actionable executive briefing.
Your Cold Email is Now Researched. This pipeline finds specific bottlenecks on prospect websites and instantly crafts an irresistible pitch
Automatically turn top performing Instagram reels into 7 new ready to use content scripts. This workflow scrapes high performing posts from a chosen Instagram profile, downloads and transcribes the re
Ask questions like “How much did I spend on food last month?” and get instant answers from your financial data — directly in Telegram.
The Problem That it Solves