mirror of
https://github.com/khoaliber/n8nworkflows.xyz.git
synced 2026-04-20 01:24:33 +00:00
1 line
18 KiB
Plaintext
1 line
18 KiB
Plaintext
{"id":"lWv2iRsBTs-HSOpAEiGfx","meta":{"instanceId":"d7e6bcead07eae1bc3a177fad0abfbc0950c4bfe0f3ab8444c6466b7a3114819"},"name":"Extract Target ICP Leads from LinkedIn Comments using Apify","tags":[],"nodes":[{"id":"83546292-c6cc-46c6-b47b-8a798912be2f","name":"Sticky Note - Section 1","type":"n8n-nodes-base.stickyNote","position":[1008,640],"parameters":{"color":7,"width":392,"height":372,"content":"## 1. User Input Form\nCollects the LinkedIn post URL and ICP filtering criteria (job titles and target countries) from the user."},"typeVersion":1},{"id":"b4550385-fe87-4424-b203-c769e6f72d7e","name":"Sticky Note - Section 2","type":"n8n-nodes-base.stickyNote","position":[1408,640],"parameters":{"color":7,"width":984,"height":372,"content":"## 2. Scrape Comments with Pagination\nFetches all comments from the LinkedIn post using Apify. Handles pagination automatically to retrieve up to 1,000 comments."},"typeVersion":1},{"id":"3b03345d-0e16-4e5a-85d7-c062dfd86c67","name":"Sticky Note - Section 3","type":"n8n-nodes-base.stickyNote","position":[1872,16],"parameters":{"color":7,"width":1164,"height":564,"content":"## 3. Deduplicate & Enrich Profiles\nRemoves duplicate commenters and enriches each unique profile with detailed LinkedIn data including job titles, location, and company information."},"typeVersion":1},{"id":"703d388b-24b0-4536-9179-97246c52beb3","name":"Sticky Note - Section 4","type":"n8n-nodes-base.stickyNote","position":[3072,16],"parameters":{"color":7,"width":896,"height":324,"content":"## 4. Filter & Export Leads\nFilters enriched profiles against your ICP criteria and exports matching leads as a downloadable CSV file."},"typeVersion":1},{"id":"5038a982-ab86-46d3-800a-c3b3f08ecbbd","name":"Sticky Note - Warning","type":"n8n-nodes-base.stickyNote","position":[2480,400],"parameters":{"color":2,"width":280,"height":140,"content":"### ⚠️ API Costs\nApify charges per API call. Each profile enrichment is a separate call. Monitor your Apify usage to manage costs effectively."},"typeVersion":1},{"id":"58b19d9b-46fa-45f1-a92d-2ff701301770","name":"Prepare Pagination Requests","type":"n8n-nodes-base.code","position":[1456,768],"parameters":{"jsCode":"// Initialize pagination - create multiple page requests\nconst postUrl = $json['LinkedIn Post URL'];\n\n// Create array of page numbers (pages 1-10, max 1000 comments)\n// Adjust maxPages based on your needs\nconst maxPages = 10;\nconst pageRequests = [];\n\nfor (let page = 1; page <= maxPages; page++) {\n pageRequests.push({\n json: {\n postUrl: postUrl,\n pageNumber: page,\n _originalFormData: { ...$json } // Use spread operator to ensure it's an object\n }\n });\n}\n\nreturn pageRequests;"},"typeVersion":2},{"id":"25a3cd32-ac72-4f23-be8d-f9e8c4a4344d","name":"Loop Through Pages","type":"n8n-nodes-base.splitInBatches","position":[1680,768],"parameters":{"options":{}},"typeVersion":3},{"id":"4c7e03a5-6658-434f-aca4-fe97ad8fc0bd","name":"Scrape LinkedIn Comments","type":"n8n-nodes-base.httpRequest","position":[1904,784],"parameters":{"url":"https://api.apify.com/v2/acts/apimaestro~linkedin-post-comments-replies-engagements-scraper-no-cookies/run-sync-get-dataset-items","method":"POST","options":{"timeout":600000},"jsonBody":"={\n \"postIds\": [\"{{ $json.postUrl }}\"],\n \"page_number\": {{ $json.pageNumber }},\n \"sortOrder\": \"most recent\",\n \"limit\": 100\n}","sendBody":true,"specifyBody":"json","authentication":"predefinedCredentialType","nodeCredentialType":"apifyApi"},"credentials":{"apifyApi":{"id":"F5Q9ujslm5v6XFhg","name":"personal apify da"}},"typeVersion":4.3},{"id":"21112269-42ca-4b15-affb-a4eac8ab70d4","name":"Check for More Pages","type":"n8n-nodes-base.code","position":[2128,784],"parameters":{"jsCode":"// Check if this page returned results\nconst hasResults = items.length > 0 && items[0].json;\n\nif (!hasResults || items.length < 100) {\n // No results or less than 100 = last page, stop pagination\n // Return items to continue to Done branch\n return items;\n}\n\n// Has 100 results = might have more pages, continue loop\nreturn items;"},"typeVersion":2},{"id":"7cb7d4ea-a278-447c-987d-51c8ebd8655b","name":"Remove Duplicate Commenters","type":"n8n-nodes-base.code","position":[1904,112],"parameters":{"jsCode":"// Deduplicate commenters by profile URL\nconst seen = new Set();\nconst uniqueCommenters = [];\n\nfor (const item of items) {\n const profileUrl = item.json?.author?.profile_url;\n \n if (profileUrl && !seen.has(profileUrl)) {\n seen.add(profileUrl);\n uniqueCommenters.push({\n json: {\n name: item.json.author.name,\n profile_url: profileUrl\n }\n });\n }\n}\n\nreturn uniqueCommenters;"},"typeVersion":2},{"id":"9cae4959-0cf4-4e71-902f-ca75fc169384","name":"Enrich LinkedIn Profile","type":"n8n-nodes-base.httpRequest","position":[2576,208],"parameters":{"url":"https://api.apify.com/v2/acts/apimaestro~linkedin-profile-detail/run-sync-get-dataset-items","method":"POST","options":{"timeout":600000},"jsonBody":"={\n \"username\": \"{{ $json.profile_url }}\",\n \"includeEmail\": false\n}","sendBody":true,"specifyBody":"json","authentication":"predefinedCredentialType","nodeCredentialType":"apifyApi"},"typeVersion":4.3},{"id":"fc10825e-4a95-4e4d-aaf8-632e9fc8abb4","name":"Filter by ICP Criteria","type":"n8n-nodes-base.code","position":[3104,96],"parameters":{"jsCode":"// This node runs when the loop is complete\n// It receives ALL enriched profiles from the loop\n\n// Get the stored form data from the first item\nconst firstItem = items[0]?.json;\nif (!firstItem || !firstItem._formData) {\n throw new Error('Form data not found in items');\n}\n\nconst selectedTitles = firstItem._formData.selectedTitles || [];\nconst selectedCountries = firstItem._formData.selectedCountries || [];\n\n// Job title mapping with comprehensive synonyms\nconst titleMapping = {\n 'CEO': ['CEO', 'Chief Executive Officer', 'Chief Exec'],\n 'Founder': ['Founder', 'Co-Founder', 'Cofounder', 'Co Founder'],\n 'Co-Founder': ['Co-Founder', 'Cofounder', 'Founder', 'Co Founder'],\n 'CMO': ['CMO', 'Chief Marketing Officer'],\n 'COO': ['COO', 'Chief Operating Officer', 'Chief Operations Officer'],\n 'CTO': ['CTO', 'Chief Technology Officer', 'Chief Technical Officer'],\n 'CFO': ['CFO', 'Chief Financial Officer'],\n 'CRO': ['CRO', 'Chief Revenue Officer'],\n 'CPO': ['CPO', 'Chief Product Officer'],\n 'VP Marketing': ['VP Marketing', 'Vice President Marketing', 'VP of Marketing', 'Vice President of Marketing'],\n 'VP Sales': ['VP Sales', 'Vice President Sales', 'VP of Sales', 'Vice President of Sales'],\n 'VP Engineering': ['VP Engineering', 'Vice President Engineering', 'VP of Engineering', 'Vice President of Engineering'],\n 'Head of Marketing': ['Head of Marketing', 'Marketing Head'],\n 'Head of Sales': ['Head of Sales', 'Sales Head'],\n 'Head of Growth': ['Head of Growth', 'Growth Head']\n};\n\n// Country mapping with localized names\nconst countryMapping = {\n 'United States': ['United States', 'USA', 'US', 'America'],\n 'United Kingdom': ['United Kingdom', 'UK', 'Britain', 'Great Britain', 'England'],\n 'Germany': ['Germany', 'Deutschland'],\n 'France': ['France', 'République française'],\n 'Spain': ['Spain', 'España'],\n 'Netherlands': ['Netherlands', 'Nederland', 'Holland'],\n 'Canada': ['Canada'],\n 'Australia': ['Australia'],\n 'Ireland': ['Ireland', 'Éire'],\n 'Singapore': ['Singapore']\n};\n\n// Build expanded title list\nconst expandedTitles = [];\nfor (const title of selectedTitles) {\n if (titleMapping[title]) {\n expandedTitles.push(...titleMapping[title]);\n }\n}\n\n// Build expanded country list\nconst expandedCountries = [];\nfor (const country of selectedCountries) {\n if (countryMapping[country]) {\n expandedCountries.push(...countryMapping[country]);\n }\n}\n\n// Process all enriched profiles\nconst matchedProfiles = [];\n\nfor (const item of items) {\n const profile = item.json;\n const basicInfo = profile.basic_info || {};\n const experience = profile.experience || [];\n \n // Extract job titles from current roles (up to 3)\n const jobTitles = [];\n for (let i = 0; i < Math.min(3, experience.length); i++) {\n const exp = experience[i];\n if (exp && exp.title) {\n jobTitles.push(exp.title);\n }\n }\n \n // Also check headline\n if (basicInfo.headline) {\n jobTitles.push(basicInfo.headline);\n }\n \n // Extract location/country - combine all sources\n const locationParts = [];\n \n // From basic_info.location\n if (basicInfo.location) {\n if (basicInfo.location.country) locationParts.push(basicInfo.location.country);\n if (basicInfo.location.full) locationParts.push(basicInfo.location.full);\n if (basicInfo.location.city) locationParts.push(basicInfo.location.city);\n }\n \n // From experience locations\n for (let i = 0; i < Math.min(3, experience.length); i++) {\n const exp = experience[i];\n if (exp && exp.location) {\n locationParts.push(exp.location);\n }\n }\n \n const location = locationParts.join(' ');\n \n // Normalize for case-insensitive matching\n const jobTitlesLower = jobTitles.map(t => String(t).toLowerCase());\n const locationLower = String(location).toLowerCase();\n \n // Check job title match\n let titleMatch = false;\n let matchedTitle = '';\n for (const title of expandedTitles) {\n const titleLower = title.toLowerCase();\n for (const jobTitle of jobTitlesLower) {\n if (jobTitle.includes(titleLower)) {\n titleMatch = true;\n matchedTitle = title;\n break;\n }\n }\n if (titleMatch) break;\n }\n \n // Check country match\n let countryMatch = false;\n let matchedCountry = '';\n for (const country of expandedCountries) {\n if (locationLower.includes(country.toLowerCase())) {\n countryMatch = true;\n matchedCountry = country;\n break;\n }\n }\n \n // Add to matched profiles if both match\n if (titleMatch && countryMatch) {\n matchedProfiles.push({\n json: {\n fullName: basicInfo.fullname || ((basicInfo.first_name || '') + ' ' + (basicInfo.last_name || '')).trim() || '',\n profileUrl: basicInfo.profile_url || '',\n jobTitle: jobTitles[0] || '',\n jobTitles: jobTitles,\n location: basicInfo.location?.full || basicInfo.location?.country || '',\n matchedJobTitle: matchedTitle,\n matchedCountry: matchedCountry,\n headline: basicInfo.headline || '',\n currentCompany: basicInfo.current_company || '',\n email: basicInfo.email || null\n }\n });\n }\n}\n\nreturn matchedProfiles;"},"typeVersion":2},{"id":"8aa8e1b8-2156-44ce-b02e-e37689b83aa7","name":"Aggregate Matched Leads","type":"n8n-nodes-base.aggregate","position":[3328,96],"parameters":{"options":{},"aggregate":"aggregateAllItemData","destinationFieldName":"leads"},"typeVersion":1},{"id":"e4c5e020-4e1f-4c77-84ad-50c18097f14c","name":"ICP Lead Extractor Form","type":"n8n-nodes-base.formTrigger","position":[1168,768],"webhookId":"a9c991a5-e5cf-4d8e-be17-61cd8d0f8dfc","parameters":{"options":{"buttonLabel":"Extract Leads"},"formTitle":"LinkedIn ICP Lead Extractor","formFields":{"values":[{"fieldName":"LinkedIn Post URL","fieldLabel":"LinkedIn Post URL","placeholder":"https://www.linkedin.com/posts/...","requiredField":true},{"fieldName":"Target Job Titles","fieldType":"checkbox","fieldLabel":"Target Job Titles","fieldOptions":{"values":[{"option":"CEO"},{"option":"Founder"},{"option":"Co-Founder"},{"option":"CMO"},{"option":"COO"},{"option":"CTO"},{"option":"CFO"},{"option":"CRO"},{"option":"CPO"},{"option":"VP Marketing"},{"option":"VP Sales"},{"option":"VP Engineering"},{"option":"Head of Marketing"},{"option":"Head of Sales"},{"option":"Head of Growth"}]},"requiredField":true},{"fieldName":"Target Countries","fieldType":"checkbox","fieldLabel":"Target Countries","fieldOptions":{"values":[{"option":"United States"},{"option":"United Kingdom"},{"option":"Germany"},{"option":"France"},{"option":"Spain"},{"option":"Netherlands"},{"option":"Canada"},{"option":"Australia"},{"option":"Ireland"},{"option":"Singapore"}]},"requiredField":true}]},"responseMode":"lastNode","formDescription":"Extract LinkedIn profiles matching your target ICP from post comments. Fill in the LinkedIn post URL and select your target decision-maker job titles and countries."},"typeVersion":2.4},{"id":"1f8b597a-74ea-481c-a191-0d634fbad238","name":"Attach ICP Criteria to Profiles","type":"n8n-nodes-base.code","position":[2128,112],"parameters":{"jsCode":"// Store form selections in each item so they're accessible in the loop\nconst formTrigger = $('ICP Lead Extractor Form').first().json;\n\n// Form data can be at different locations depending on form version\nlet formData;\nif (formTrigger.data) {\n formData = formTrigger.data;\n} else {\n formData = formTrigger;\n}\n\nconst selectedTitles = formData['Target Job Titles'] || [];\nconst selectedCountries = formData['Target Countries'] || [];\n\n// Add form selections to each profile item\nconst itemsWithFormData = [];\nfor (const item of items) {\n itemsWithFormData.push({\n json: {\n ...item.json,\n _formData: {\n selectedTitles: selectedTitles,\n selectedCountries: selectedCountries\n }\n }\n });\n}\n\nreturn itemsWithFormData;"},"typeVersion":2},{"id":"9310e647-e69c-4727-92de-1b1620037f7e","name":"Loop Through Profiles","type":"n8n-nodes-base.splitInBatches","position":[2352,112],"parameters":{"options":{}},"typeVersion":3},{"id":"feb1028d-c4f1-4ca0-b881-49dc28ca5ba0","name":"Preserve ICP Criteria","type":"n8n-nodes-base.code","position":[2816,208],"parameters":{"mode":"runOnceForEachItem","jsCode":"// Preserve the _formData from the input through the enrichment\nconst enrichedProfile = $json;\nconst originalData = $('Loop Through Profiles').item.json;\n\n// Combine enriched profile with preserved form data\nreturn {\n json: {\n ...enrichedProfile,\n _formData: originalData._formData\n }\n};"},"typeVersion":2},{"id":"a3b1116c-4b4c-49f5-8a67-2e13084989d1","name":"Format Leads for CSV","type":"n8n-nodes-base.code","position":[3552,96],"parameters":{"jsCode":"// Flatten the aggregated leads for CSV export\nconst leads = $json.leads || [];\n\nif (leads.length === 0) {\n return [{\n json: {\n message: 'No matching profiles found'\n }\n }];\n}\n\n// Flatten each lead into CSV-ready format\nconst flattenedLeads = leads.map(lead => ({\n json: {\n 'Full Name': lead.fullName || '',\n 'LinkedIn Profile URL': lead.profileUrl || '',\n 'Job Title': lead.jobTitle || '',\n 'All Job Titles': Array.isArray(lead.jobTitles) ? lead.jobTitles.join(', ') : (lead.jobTitles || ''),\n 'Location': lead.location || '',\n 'Matched Job Title': lead.matchedJobTitle || '',\n 'Matched Country': lead.matchedCountry || '',\n 'Headline': lead.headline || '',\n 'Current Company': lead.currentCompany || '',\n 'Email': lead.email || ''\n }\n}));\n\nreturn flattenedLeads;"},"typeVersion":2},{"id":"27306d00-7f50-4de0-912b-c6ff17f7f56e","name":"Export to CSV","type":"n8n-nodes-base.convertToFile","position":[3776,96],"parameters":{"options":{"fileName":"=linkedin_icp_leads_{{ $now.toFormat('yyyy-MM-dd_HHmm') }}.csv"}},"typeVersion":1.1},{"id":"f27ee8df-c30a-4fbe-b346-5717e27622a6","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[400,304],"parameters":{"width":560,"height":704,"content":"## LinkedIn ICP Lead Extractor\n\nThis workflow extracts qualified leads from LinkedIn post comments based on your Ideal Customer Profile (ICP) job titles and countries.\n\n### How it works\n1. Submit a LinkedIn post URL via the form\n2. Select target job titles and countries\n3. Comments are scraped and deduplicated\n4. Each profile is enriched with detailed data\n5. Profiles are filtered against your ICP criteria\n6. Matching leads are exported as CSV\n\n### Setup steps\n- [ ] Create an Apify account at apify.com\n- [ ] Add your Apify API credentials in n8n\n- [ ] Click \"Execute Workflow\" to open the form\n- [ ] Submit a LinkedIn post URL with your ICP criteria\n\n### Requirements\n- Apify account with API access (Free tier is available on Apify)\n\n### Customization\n- Modify job titles/countries in the Form Trigger\n- Adjust pagination limits for larger posts\n- Connect output to your CRM instead of CSV"},"typeVersion":1}],"active":false,"pinData":{},"settings":{"availableInMCP":false,"executionOrder":"v1"},"versionId":"cf78e532-f7c1-4c7c-9580-d48f91a34810","connections":{"Loop Through Pages":{"main":[[{"node":"Remove Duplicate Commenters","type":"main","index":0}],[{"node":"Scrape LinkedIn Comments","type":"main","index":0}]]},"Check for More Pages":{"main":[[{"node":"Loop Through Pages","type":"main","index":0}]]},"Format Leads for CSV":{"main":[[{"node":"Export to CSV","type":"main","index":0}]]},"Loop Through Profiles":{"main":[[{"node":"Filter by ICP Criteria","type":"main","index":0}],[{"node":"Enrich LinkedIn Profile","type":"main","index":0}]]},"Preserve ICP Criteria":{"main":[[{"node":"Loop Through Profiles","type":"main","index":0}]]},"Filter by ICP Criteria":{"main":[[{"node":"Aggregate Matched Leads","type":"main","index":0}]]},"Aggregate Matched Leads":{"main":[[{"node":"Format Leads for CSV","type":"main","index":0}]]},"Enrich LinkedIn Profile":{"main":[[{"node":"Preserve ICP Criteria","type":"main","index":0}]]},"ICP Lead Extractor Form":{"main":[[{"node":"Prepare Pagination Requests","type":"main","index":0}]]},"Scrape LinkedIn Comments":{"main":[[{"node":"Check for More Pages","type":"main","index":0}]]},"Prepare Pagination Requests":{"main":[[{"node":"Loop Through Pages","type":"main","index":0}]]},"Remove Duplicate Commenters":{"main":[[{"node":"Attach ICP Criteria to Profiles","type":"main","index":0}]]},"Attach ICP Criteria to Profiles":{"main":[[{"node":"Loop Through Profiles","type":"main","index":0}]]}}} |