This workflow corresponds to n8n.io template #13644 — we link there as the canonical source.
This workflow follows the Agent → 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": "akQ25uAX0U54kTCW",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "TOFU Sales Intelligence",
"tags": [],
"nodes": [
{
"id": "4f8ab4a4-9058-44bd-a715-0efc7bd39653",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"notes": "This webhook can be from any source, which is through new trial sign ups. It could be a website lead magnet, it could be an OpenClaw/CrewAI endpoint for your agent. Recommend adding a heather auth to protect your endpoint. ",
"position": [
-6880,
1104
],
"parameters": {
"path": "new-lead",
"options": {},
"httpMethod": "POST"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "89d9501f-7668-4195-96e9-b53e67fa9058",
"name": "Extract Email Root Domain",
"type": "n8n-nodes-base.set",
"notes": "This node will simply extract the root domain from the email address. ",
"position": [
-6656,
1008
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "1bfef44b-ad78-49f7-9b17-b5c0243cbe3d",
"name": "root_domain",
"type": "string",
"value": "={{ $json.email.split('@')[1] }}"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "666523a8-c730-468b-b32a-77067f828c90",
"name": "Check to make sure email is not null",
"type": "n8n-nodes-base.if",
"notes": "This node is a safety/error-handling check to ensure the email is not null/empty",
"position": [
-6208,
1008
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "8b7d6a14-7545-4684-a8a6-7fcd9543c44b",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $('Extract Email Root Domain').item.json.root_domain }}",
"rightValue": "null"
},
{
"id": "72cfc6f1-f802-4abd-b98f-f7ecab73f2a3",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $('Extract Email Root Domain').item.json.root_domain }}",
"rightValue": "null"
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "a7105881-9d32-4500-a093-e6688b2ea386",
"name": "Structured Output Parser1",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
-4416,
1216
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"output\": {\n \"company_name\": \"Cardinal Refer\",\n \"followers\": 48,\n \"employee_count\": 14,\n \"headquarters_location\": \"Stanford, California\",\n \"industry\": \"Healthcare\",\n \"description\": \"Founded in 2020 by two Stanford students...\"\n }\n}"
},
"typeVersion": 1.2
},
{
"id": "7afdd847-8471-4ba6-89a5-4122bfa4d066",
"name": "LinkedIn Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"notes": "This agent is configured to extract LinkedIn information such as followers, employee_count, headquarters_location, industry, and description. It uses scrap.io as the enrichment tool. ",
"position": [
-4624,
992
],
"parameters": {
"text": "=Based on the input below, extract the following information from the company linkedin page and ensure numeric values are returned as numbers, not strings: \n\n- Company Name (as text)\n- Linkedin followers (as a number)\n- Number of employees or staff (as a number)\n- Company headquarters location (City and country as text)\n- Industry (extract the primary industry category as shown on LinkedIn)\n- Description or overview of the company (as text). Do not make anything up. Just extract the description that has been put into the LinkedIn company Url and make it easy to read and proper english.\n\nOutput format should maintain consistent field names:\n{\n \"output\": {\n \"company_name\": string,\n \"followers\": number,\n \"employee_count\": number,\n \"headquarters_location\": string,\n \"industry\": string,\n \"description\": string\n }\n}\n\nInput the entire URL as described below using the website scraper tool\nInput: {{ $json.debugInfo.cleanLinkedInUrl }}",
"options": {
"systemMessage": "=You are a Linkedin data analyst and your job is to extract company information such as the followers, headquarters location, company description, industry, and staff (employee) count of a company page on linkedin and nothing else. Do not output any other numbers other than the scraped data. Use the website_tool to extract this information."
},
"promptType": "define",
"hasOutputParser": true
},
"notesInFlow": true,
"retryOnFail": true,
"typeVersion": 1.7
},
{
"id": "c03a70c8-72fa-4517-a9dc-3acb414fd22d",
"name": "Check if website exists",
"type": "n8n-nodes-base.httpRequest",
"notes": "This is an error handling node to ensure the website is responsive and valid.",
"onError": "continueRegularOutput",
"position": [
-6000,
1008
],
"parameters": {
"url": "=https://{{ $json.root_domain }}",
"method": "HEAD",
"options": {
"response": {
"response": {
"neverError": true,
"fullResponse": true
}
}
}
},
"notesInFlow": true,
"retryOnFail": true,
"typeVersion": 4.2
},
{
"id": "02ca2491-7ad5-48fa-a146-6fb7b8d7cb33",
"name": "Normalize Country",
"type": "@n8n/n8n-nodes-langchain.openAi",
"notes": "This normalizes the company location to ensure we always get structured output for scoring downstream.",
"position": [
-3808,
992
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-nano",
"cachedResultName": "GPT-4.1-NANO"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "=Given the location text: {{ $json.output.headquarters_location }}, return the official country name or \"Unknown\"."
},
{
"content": "=You are a helpful assistant that extracts standardized country names from any location string. The user will provide a location that might be a city, state, region, or country. \n\nYour job:\n1. Identify the country the location is in. \n2. Return only the official country name in English (e.g., \"United States\", \"Canada\", \"United Kingdom\", \"India\", etc.).\n3. If the location is a city and/or state within the United States (e.g., \"Denver, CO\", \"New York, NY\", \"Texas\"), return \"United States\".\n4. If you cannot determine the country with confidence, return \"Unknown\".\n5. Do not provide any additional text or explanations\u2014only the country name or \"Unknown\".\n\n"
}
]
},
"jsonOutput": "={{ true }}"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"notesInFlow": true,
"retryOnFail": true,
"typeVersion": 1.8
},
{
"id": "7834015a-904d-4acb-9e78-96429a83280b",
"name": "Score Country",
"type": "n8n-nodes-base.code",
"notes": "Location Score Assignment\n\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\nPurpose:\nAssigns a numeric locationScore (0\u201310) to each lead based on the company's country \u2014 prioritizes high-value markets like US, UK, Canada, etc.\n\nHow it works:\n\u2022 Pulls the country from the incoming item (from previous normalization step)\n\u2022 Normalizes to lowercase + trims whitespace\n\u2022 Matches against tiered lists of countries/regions\n\u2022 Assigns score:\n - 10: US\n - 8: Tier 2 (UK, Canada, Australia, NZ)\n - 7: Western Europe, Nordics, East Asia, Singapore/HK\n - 6: Middle East (UAE, Israel, etc.)\n - 4: Other EU\n - 3: LatAm\n - 2: South Asia, SE Asia, Eastern Europe\n - 1: Africa\n - 0: Rest of world / unknown\n\nOutput fields added:\n- normalizedCountry: original country value (for reference)\n- locationScore: integer 0\u201310\n\nWhy this structure?\n- Avoids node-name lookups (prevents execution hangs in recent n8n versions)\n- Uses direct input data access ($input.all() + item.json)\n- Safe fallbacks for missing/misnamed fields\n\nDebug tips:\n- If score is always 0: check upstream node outputs the country correctly (look at JSON preview)\n- To change scoring: edit the arrays or if/else conditions\n- Test with 1 item first (switch node to \"Execute Once\")\n\nThis keeps the workflow fast and reliable even with hundreds of leads.",
"position": [
-3472,
992
],
"parameters": {
"jsCode": "/**\n * Assign location score based on country\n * Assumes the country is already available in the incoming item (from previous node)\n * This avoids node-name lookups that cause hangs in recent n8n versions\n */\n\nconst newItems = [];\n\nfor (const item of $input.all()) {\n // Get country from incoming data - adjust field name if needed\n // Common possibilities: item.json.country, item.json.normalizedCountry, item.json.message.content.country, etc.\n let rawCountry = '';\n\n // Try most common paths - pick the one that matches your upstream output\n if (item.json.country) {\n rawCountry = item.json.country;\n } else if (item.json.normalizedCountry) {\n rawCountry = item.json.normalizedCountry;\n } else if (item.json.message?.content?.country) {\n rawCountry = item.json.message.content.country;\n } else if (item.json.output?.country) {\n rawCountry = item.json.output.country;\n }\n\n const country = (rawCountry || '').trim().toLowerCase();\n\n let locationScore = 0;\n\n // Your original tier lists (unchanged)\n const locUS = ['united states', 'usa', 'us'];\n const locTier2 = ['united kingdom', 'uk', 'canada', 'australia', 'new zealand'];\n const locWesternEurope = ['germany', 'france', 'netherlands', 'belgium', 'switzerland', 'austria', 'ireland', 'luxembourg'];\n const locNordic = ['sweden', 'norway', 'denmark', 'finland', 'iceland'];\n const locEastAsia = ['japan', 'south korea', 'korea', 'taiwan'];\n const locSingHK = ['singapore', 'hong kong'];\n const locMiddleEast = ['uae', 'israel', 'saudi arabia', 'qatar', 'kuwait', 'bahrain', 'oman'];\n const locOtherEU = ['spain', 'italy', 'portugal', 'poland', 'czech republic', 'greece', 'slovakia', 'hungary', 'lithuania', 'estonia', 'latvia', 'malta'];\n const locLatAm = ['brazil', 'mexico', 'argentina', 'chile', 'colombia', 'peru', 'uruguay', 'costa rica', 'panama'];\n const locSouthAsia = ['india', 'bangladesh', 'pakistan', 'sri lanka', 'nepal'];\n const locSEAsia = ['vietnam', 'indonesia', 'thailand', 'philippines', 'malaysia', 'myanmar', 'cambodia', 'laos', 'brunei'];\n const locEasternEurope = ['russia', 'ukraine', 'romania', 'bulgaria', 'serbia', 'croatia', 'slovenia', 'moldova', 'georgia', 'armenia', 'belarus', 'albania', 'montenegro', 'bosnia & herzegovina', 'north macedonia'];\n const locAfrica = ['south africa', 'nigeria', 'kenya', 'egypt', 'morocco', 'ghana', 'ethiopia', 'tanzania', 'tunisia', 'algeria', 'rwanda', 'botswana', 'uganda', 'senegal', 'zambia', 'namibia'];\n\n // Scoring\n if (locUS.includes(country)) {\n locationScore = 10;\n } else if (locTier2.includes(country)) {\n locationScore = 8;\n } else if (locWesternEurope.includes(country) ||\n locNordic.includes(country) ||\n locEastAsia.includes(country) ||\n locSingHK.includes(country)) {\n locationScore = 7;\n } else if (locMiddleEast.includes(country)) {\n locationScore = 6;\n } else if (locOtherEU.includes(country)) {\n locationScore = 4;\n } else if (locLatAm.includes(country)) {\n locationScore = 3;\n } else if (locSouthAsia.includes(country) ||\n locSEAsia.includes(country) ||\n locEasternEurope.includes(country)) {\n locationScore = 2;\n } else if (locAfrica.includes(country)) {\n locationScore = 1;\n } else {\n locationScore = 0;\n }\n\n newItems.push({\n json: {\n ...item.json, // keep all previous fields\n normalizedCountry: rawCountry, // original value for reference\n locationScore\n }\n });\n}\n\nreturn newItems;"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "695c763d-faf3-4a21-b4b6-5899d747d3a0",
"name": "Score Staff Count",
"type": "n8n-nodes-base.code",
"notes": "Headcount Scoring (Templatized)\n\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\nPurpose: Scores companies by employee count (headcountScore 0\u201310) based on customizable ranges.\n\nCustomization (edit in code):\n\u2022 sourceNodeName / sourceFieldPath \u2192 change if upstream node/field renamed\n\u2022 defaultScore \u2192 fallback when count missing/invalid\n\u2022 scoreRanges array \u2192 add/remove/edit buckets & scores\n Example: { min: 51, max: 200, score: 10 } = sweet spot\n\nCurrent ranges:\n- 1\u201310 \u2192 3\n- 11\u201350 \u2192 7\n- 51\u2013200 \u2192 10 (ideal target)\n- 201\u2013500 \u2192 9\n- 501\u20135000 \u2192 8\n- 5001+ \u2192 7\n- Missing \u2192 0\n\nOutput fields:\n- staffCount: cleaned number\n- headcountScore: 0\u201310\n\nTips:\n- Test changes with small data first\n- Use Infinity for \"and above\" ranges\n- Numbers only \u2014 non-numeric input \u2192 0",
"position": [
-3264,
992
],
"parameters": {
"jsCode": "/**\n * Templatized Code node: Score company headcount (staff size)\n * \n * Users can customize:\n * - The ranges and their corresponding scores\n * - The field name/path where employee_count comes from\n * - Default score for unknown/missing values\n * \n * How to customize:\n * 1. Edit the `scoreRanges` array below \u2014 add/remove/change ranges and scores\n * 2. Adjust `sourceFieldPath` if your employee_count is in a different spot\n * 3. Change `defaultScore` if you want a different fallback\n */\n\nconst newItems = [];\n\n// \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\u2500\n// USER CONFIGURATION SECTION \u2500\u2500 EDIT HERE\n// \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\u2500\n\nconst sourceNodeName = \"Sanitize Description\"; // Change if upstream node renamed\nconst sourceFieldPath = \"employee_count\"; // e.g. \"output.employee_count\", \"data.staff\", etc.\n\nconst defaultScore = 0; // Score when count is missing / invalid / 0\n\n// Define your scoring buckets here (lowest \u2192 highest)\n// Format: { min: number, max: number, score: number }\n// Use Infinity for \"and above\", null/undefined for open-ended\nconst scoreRanges = [\n { min: 1, max: 10, score: 3 }, // Very small (1\u201310)\n { min: 11, max: 50, score: 7 }, // Small-mid (11\u201350)\n { min: 51, max: 200, score: 10 }, // Sweet spot (51\u2013200)\n { min: 201, max: 500, score: 9 }, // Mid-size (201\u2013500)\n { min: 501, max: 5000, score: 8 }, // Large (501\u20135000)\n { min: 5001, max: Infinity, score: 7 } // Enterprise / very large (>5000)\n];\n\n// \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\u2500\n// END OF USER CONFIG \u2500\u2500 NO NEED TO EDIT BELOW\n// \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\u2500\n\nfor (const item of $input.all()) {\n // Safely retrieve the employee count (modern n8n style)\n let staffCount = 0;\n\n // Try to get it from the configured source\n const sourceData = item.json;\n if (sourceFieldPath.includes('.')) {\n // Deep path (e.g. \"output.employee_count\")\n const parts = sourceFieldPath.split('.');\n let current = sourceData;\n for (const part of parts) {\n current = current?.[part];\n if (current === undefined) break;\n }\n staffCount = Number(current) || 0;\n } else {\n // Simple top-level field\n staffCount = Number(sourceData?.[sourceFieldPath]) || 0;\n }\n\n // Find matching range and assign score\n let headcountScore = defaultScore;\n\n for (const range of scoreRanges) {\n if (staffCount >= range.min && (range.max === Infinity || staffCount <= range.max)) {\n headcountScore = range.score;\n break;\n }\n }\n\n newItems.push({\n json: {\n ...item.json,\n staffCount, // original value (cleaned to number)\n headcountScore\n }\n });\n}\n\nreturn newItems;"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "0a58f859-e986-4566-84d9-a0d20dfc3d1c",
"name": "Sanitize Description",
"type": "n8n-nodes-base.code",
"notes": "Sanitize Description\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nPurpose:\nCleans up the raw LinkedIn company description from the \"LinkedIn Agent\" node so it's Slack-friendly, readable, and short.\n\nWhat it does:\n\u2022 Safely grabs the description (handles missing data gracefully)\n\u2022 Replaces fancy/smart quotes with normal ones\n\u2022 Converts dashes/ellipses/newlines into clean single spaces\n\u2022 Collapses extra whitespace\n\u2022 Trims leading/trailing spaces\n\u2022 Caps length at ~280 chars (optional, good for messaging)\n\nWhy we need this:\nRaw LinkedIn descriptions often contain weird Unicode characters, multiple newlines, and promotional fluff that looks terrible in Slack notifications or short-form outputs.\n\nTips for debugging:\n- If the node hangs: double-check the upstream node is exactly named \"LinkedIn Agent\" (no extra spaces).\n- Test with a minimal version first: return items with hardcoded \"TEST OK\" to confirm execution.\n- Output includes original data + new field: sanitized_description\n\nExpected output field: sanitized_description (string)",
"position": [
-4032,
992
],
"parameters": {
"jsCode": "// Clean LinkedIn company description for Slack / short use\n// Safe access + aggressive sanitization\n\nconst newItems = [];\n\nfor (const item of $input.all()) {\n let desc = \"\";\n\n // Safe path: get description from upstream \"LinkedIn Agent\" node\n const agentOutput = $(\"LinkedIn Agent\").first()?.json?.output;\n if (agentOutput?.description) {\n desc = agentOutput.description;\n } else {\n // Fallback: try current item's json if description is already there\n desc = item.json.description || \"\";\n }\n\n // Sanitize for clean text output\n desc = desc\n .replace(/[\\u2018\\u2019\\u201C\\u201D]/g, \"'\") // fancy quotes \u2192 normal\n .replace(/[\\u2013\\u2014]/g, \"-\") // dashes\n .replace(/\\u2026/g, \"...\") // ellipsis\n .replace(/\\r\\n|\\r|\\n/g, \" \") // newlines \u2192 space\n .replace(/\\s+/g, \" \") // collapse whitespace\n .trim();\n\n // Optional: cap length for Slack-ish contexts\n if (desc.length > 280) {\n desc = desc.slice(0, 277) + \"...\";\n }\n\n // Pass through original data + add cleaned field\n newItems.push({\n json: {\n ...item.json,\n sanitized_description: desc\n }\n });\n}\n\nreturn newItems;"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "e8652025-9325-4ad9-815e-6612288f3af0",
"name": "OpenAI Chat Model1",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-4736,
1232
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-5.2",
"cachedResultName": "gpt-5.2"
},
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "9b1bbf8e-ecca-4bf7-baa4-27c95f0e1f3b",
"name": "Execution Data",
"type": "n8n-nodes-base.executionData",
"position": [
-6656,
1280
],
"parameters": {
"dataToSave": {
"values": [
{
"key": "email",
"value": "={{ $item(\"0\").$node[\"Webhook\"].json[\"body\"][\"data\"][\"item\"][\"email\"] }}"
}
]
}
},
"typeVersion": 1
},
{
"id": "804917d6-6098-4723-a3f3-ff37f891716e",
"name": "Industry Scoring",
"type": "n8n-nodes-base.code",
"notes": "Industry Normalization & Scoring (Templatized)\n\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\nPurpose: Converts raw industry strings \u2192 clean category + numeric score (higher = better fit).\n\nCustomization (edit in code):\n\u2022 sourceFieldPath \u2192 change if industry is nested (e.g. \"data.industry\")\n\u2022 fallbackCategory \u2192 default when no match\n\u2022 industryCategories array:\n - Add/remove categories\n - Change score (0\u201310 recommended)\n - Edit keywords (partial, case-insensitive matches)\n - Order matters: higher-priority categories first\n\nCurrent top priorities:\n- Technology/Media \u2192 10\n- Financial/Fintech \u2192 9\n- Most others \u2192 6 or lower\n\nOutput fields added:\n- normalizedIndustry: clean category name\n- originalIndustry: raw input for reference\n- industryScore: numeric score\n\nTips:\n- Test with varied industries first\n- Use broad keywords to catch variations\n- If too many \"Other\", add more specific keywords\n- First match wins \u2014 reorder array for priority\n\nKeeps workflow flexible for different lead criteria!",
"position": [
-3024,
992
],
"parameters": {
"jsCode": "/**\n * Templatized Code node: Normalize & Score Company Industry\n * \n * Maps raw industry strings to broader categories and assigns scores.\n * \n * Customization guide (edit in USER CONFIG section below):\n * 1. Add/remove/edit entries in `industryCategories`\n * 2. Adjust keyword matches (case-insensitive partial matches)\n * 3. Change fallback category & score\n * 4. Modify source field/path if needed\n */\n\nconst newItems = [];\n\n// \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\u2500\n// USER CONFIGURATION SECTION \u2500\u2500 EDIT HERE\n// \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\u2500\n\nconst sourceFieldPath = \"output.industry\"; // Path in incoming json, e.g. \"industry\", \"output.industry\", \"data.company.industry\"\n\nconst fallbackCategory = {\n category: \"Other Industries\",\n score: 3,\n note: \"No strong match found\"\n};\n\nconst industryCategories = [\n {\n category: \"Technology, Information and Media\",\n score: 10,\n keywords: [\n \"technology\", \"software\", \"it \", \"information technology\", \"saas\", \"cloud\",\n \"cybersecurity\", \"artificial intelligence\", \"data\", \"internet\", \"computer\",\n \"digital\", \"tech\", \"ai\", \"ml\", \"platform\", \"it services\", \"it consulting\"\n ]\n },\n {\n category: \"Financial Services\",\n score: 9,\n keywords: [\n \"financial\", \"fintech\", \"insurance\", \"investment\", \"wealth management\",\n \"capital markets\", \"venture capital\", \"private equity\"\n ]\n },\n {\n category: \"Healthcare\",\n score: 6,\n keywords: [\n \"healthcare\", \"health\", \"medical\", \"biotech\", \"pharmaceutical\", \"life sciences\",\n \"hospital\", \"clinical\", \"wellness\"\n ]\n },\n {\n category: \"Retail & E-commerce\",\n score: 6,\n keywords: [\n \"retail\", \"e-commerce\", \"ecommerce\", \"shop\", \"store\", \"marketplace\",\n \"commerce\", \"online shopping\"\n ]\n },\n {\n category: \"Manufacturing\",\n score: 6,\n keywords: [\n \"manufacturing\", \"industrial\", \"production\", \"factory\", \"fabrication\",\n \"assembly\", \"engineering\"\n ]\n },\n {\n category: \"Professional Services / Consulting\",\n score: 5,\n keywords: [\n \"professional services\", \"consulting\", \"management consulting\", \"strategy consulting\",\n \"business consulting\", \"advisory\", \"accounting\"\n ]\n },\n {\n category: \"Advertising & Marketing\",\n score: 6,\n keywords: [\n \"advertising\", \"marketing\", \"ad tech\", \"ad agency\", \"digital marketing\",\n \"marketing agency\", \"branding\"\n ]\n },\n {\n category: \"Education\",\n score: 4,\n keywords: [\n \"education\", \"learning\", \"training\", \"school\", \"university\", \"academic\",\n \"edtech\", \"teaching\"\n ]\n },\n {\n category: \"Travel & Hospitality\",\n score: 4,\n keywords: [\n \"travel\", \"hospitality\", \"hotel\", \"tourism\", \"airline\", \"vacation\",\n \"accommodation\"\n ]\n },\n {\n category: \"Consumer Goods\",\n score: 3,\n keywords: [\n \"consumer goods\", \"fmcg\", \"consumer products\", \"packaged goods\",\n \"consumer brands\"\n ]\n },\n {\n category: \"Real Estate\",\n score: 3,\n keywords: [\n \"real estate\", \"property\", \"realty\", \"housing\", \"commercial property\"\n ]\n },\n {\n category: \"Entertainment & Media\",\n score: 3,\n keywords: [\n \"entertainment\", \"gaming\", \"games\", \"music\", \"movies\", \"media production\",\n \"film\", \"video\", \"broadcasting\"\n ]\n }\n // Add more categories here as needed, highest priority first\n];\n\n// \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\u2500\n// END OF USER CONFIG \u2500\u2500 NO NEED TO EDIT BELOW\n// \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\u2500\n\nfor (const item of $input.all()) {\n // Safely extract raw industry\n let rawIndustry = '';\n\n if (sourceFieldPath.includes('.')) {\n const parts = sourceFieldPath.split('.');\n let current = item.json;\n for (const part of parts) {\n current = current?.[part];\n if (current === undefined) break;\n }\n rawIndustry = (current || '').toString();\n } else {\n rawIndustry = (item.json?.[sourceFieldPath] || '').toString();\n }\n\n const normalized = rawIndustry.trim().toLowerCase();\n\n // Find best match (first match wins - order in array matters!)\n let matched = { ...fallbackCategory, originalIndustry: rawIndustry };\n\n for (const cat of industryCategories) {\n for (const kw of cat.keywords) {\n if (normalized.includes(kw)) {\n matched = {\n category: cat.category,\n score: cat.score,\n originalIndustry: rawIndustry\n };\n break; // Stop at first match\n }\n }\n if (matched.category !== fallbackCategory.category) break;\n }\n\n newItems.push({\n json: {\n ...item.json,\n normalizedIndustry: matched.category,\n originalIndustry: matched.originalIndustry,\n industryScore: matched.score\n }\n });\n}\n\nreturn newItems;"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "ae55a0b9-6acb-42df-a1b5-e9773fda2e8d",
"name": "Algo Score",
"type": "n8n-nodes-base.code",
"notes": "Final Lead Scoring (Templatized)\n\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\nPurpose: Combines 3 sub-scores (each 0\u201310) \u2192 final 0\u2013100 score + rating label.\n\nCustomization (edit config object in code):\n\u2022 sources \u2192 update node/field names if upstream changes\n\u2022 weights \u2192 adjust relative importance (e.g. {headcount: 1.5, location: 1, industry: 2})\n\u2022 ratingTiers \u2192 change thresholds/labels (highest first)\n\u2022 defaultSubScore \u2192 fallback when data missing\n\nCurrent setup:\n- Equal weights (1:1:1)\n- Max per sub-score = 10\n- Ratings: \u226580 Excellent, \u226560 Good, \u226540 Moderate, else Poor\n\nOutput structure:\noutput.scores: {\n headcount, location, industry,\n totalRaw, finalScore (0\u2013100), rating\n}\n\nTips:\n- Test with known good/bad leads\n- To emphasize one factor \u2192 increase its weight\n- Add new sub-scores by extending config.sources & weights",
"position": [
-2800,
992
],
"parameters": {
"jsCode": "/**\n * Final Lead Scoring (Templatized)\n * Combines headcount, location, and industry scores into a 0\u2013100 final score + qualitative rating.\n * \n * Customization guide (edit in USER CONFIG section):\n * - Change weights for each component (they auto-normalize to 100)\n * - Adjust rating thresholds and labels\n * - Modify node names or field paths if upstream nodes change\n * - Add more sub-scores later if needed\n */\n\nconst newItems = [];\n\n// \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\u2500\n// USER CONFIGURATION SECTION \u2500\u2500 EDIT HERE\n// \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\u2500\n\nconst config = {\n // Source node names and field paths (update if renamed)\n sources: {\n headcount: { node: \"Score Staff Count\", field: \"headcountScore\" },\n location: { node: \"Score Country\", field: \"locationScore\" },\n industry: { node: \"Industry Scoring\", field: \"industryScore\" }\n },\n\n // Weights (higher = more important) \u2014 will be normalized to total 100\n weights: {\n headcount: 1, // e.g. give headcount more/less emphasis\n location: 1,\n industry: 1\n },\n\n // Rating tiers (thresholds from high \u2192 low)\n ratingTiers: [\n { min: 80, label: \"Excellent Fit\" },\n { min: 60, label: \"Good Fit\" },\n { min: 40, label: \"Moderate Fit\" },\n { min: 0, label: \"Poor Fit\" }\n ],\n\n // Fallback if a score is missing/invalid\n defaultSubScore: 0\n};\n\n// \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\u2500\n// END OF USER CONFIG \u2500\u2500 NO NEED TO EDIT BELOW\n// \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\u2500\n\nfor (const item of $input.all()) {\n // Helper to safely get a score from upstream\n const getScore = (source) => {\n try {\n return Number(item.json?.[source.field]) || config.defaultSubScore;\n } catch (e) {\n return config.defaultSubScore;\n }\n };\n\n // Extract individual scores (using incoming data, not node lookups)\n const headcountScore = getScore(config.sources.headcount);\n const locationScore = getScore(config.sources.location);\n const industryScore = getScore(config.sources.industry);\n\n // Weighted total (raw points)\n const totalRaw = \n headcountScore * config.weights.headcount +\n locationScore * config.weights.location +\n industryScore * config.weights.industry;\n\n // Normalize to max possible raw = sum of max scores \u00d7 weights\n const maxPossible = \n 10 * config.weights.headcount +\n 10 * config.weights.location +\n 10 * config.weights.industry;\n\n const finalScore = maxPossible > 0 ? Math.round((totalRaw / maxPossible) * 100) : 0;\n\n // Find matching rating\n let rating = config.ratingTiers[config.ratingTiers.length - 1].label; // fallback\n for (const tier of config.ratingTiers) {\n if (finalScore >= tier.min) {\n rating = tier.label;\n break;\n }\n }\n\n newItems.push({\n json: {\n output: {\n scores: {\n headcount: headcountScore,\n location: locationScore,\n industry: industryScore,\n totalRaw,\n finalScore,\n rating\n }\n }\n }\n });\n}\n\nreturn newItems;"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "143888a2-a992-4e54-bb87-a5152512bdfd",
"name": "Extract Name From Email",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-1648,
976
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-nano",
"cachedResultName": "GPT-4.1-NANO"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "You are an AI assistant that extracts names from email addresses. You receive an email address and should look at the part before the \u201c@\u201d to determine if it is a person\u2019s first name or first and last name.\n\t1.\tIf you are extremely confident it is a real first name (e.g., \u201cjohn\u201d), output the first name only (e.g., \u201cJohn\u201d).\n\t2.\tIf you are extremely confident it is a real first and last name (e.g., \u201cjohn.smith\u201d), output both (e.g., \u201cJohn Smith\u201d).\n\t3.\tIf there is any doubt\u2014meaning it looks like a role, department, organization, or anything that is not clearly a person\u2019s name (e.g., \u201csupport,\u201d \u201cadmin,\u201d \u201cinfo,\u201d etc.)\u2014output the word \"there\" as the Name and First Name without the quotes.\n\nCapitalize names properly. Output only the name or there, with no additional text or punctuation.\n\nOutput the following:\n- Name\n- First Name\n- Last Name"
},
{
"content": "=Email: {{ $item(\"0\").$node[\"Webhook\"].json[\"email\"] }}"
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "36a13dec-7a3e-49af-9e37-fc51ab70f921",
"name": "Wait",
"type": "n8n-nodes-base.wait",
"position": [
-1280,
976
],
"parameters": {
"unit": "minutes"
},
"typeVersion": 1.1
},
{
"id": "b33a8eb7-390f-4951-97bf-58667ea434df",
"name": "Extract LinkedIn Url",
"type": "n8n-nodes-base.code",
"notes": "This node will extract the LI profile URL",
"position": [
-5344,
1008
],
"parameters": {
"jsCode": "const data = $input.item.json;\n\n// Step 1: Recursively search for the first LinkedIn URL\nfunction findLinkedInUrl(obj, path = '') {\n if (!obj || typeof obj !== 'object') return null;\n\n if (Array.isArray(obj)) {\n for (const item of obj) {\n if (typeof item === 'string' && item.includes('linkedin.com')) {\n return item;\n }\n }\n for (let i = 0; i < obj.length; i++) {\n const result = findLinkedInUrl(obj[i], `${path}[${i}]`);\n if (result) return result;\n }\n } else {\n for (const [key, value] of Object.entries(obj)) {\n if (typeof value === 'string' && value.includes('linkedin.com')) {\n return value;\n }\n const result = findLinkedInUrl(value, path ? `${path}.${key}` : key);\n if (result) return result;\n }\n }\n\n return null;\n}\n\nconst rawLinkedInUrl = findLinkedInUrl(data);\n\n// Step 2: Normalize the LinkedIn URL (vanity or numeric company page)\nlet cleanLinkedInUrl = null;\nif (typeof rawLinkedInUrl === 'string') {\n // Match pattern like: https://www.linkedin.com/company/zeva-global-inc or /company/98872926\n const match = rawLinkedInUrl.match(/https?:\\/\\/(www\\.)?linkedin\\.com\\/company\\/[a-zA-Z0-9-]+/);\n if (match) {\n cleanLinkedInUrl = match[0].replace(/\\/+$/, '') + '/'; // Ensure single trailing slash\n }\n}\n\n// Step 3: Fallback in case no valid LinkedIn URL is found\nconst safeLinkedInUrl = cleanLinkedInUrl || null;\nconst hasLinkedIn = safeLinkedInUrl !== null;\n\nreturn {\n json: {\n domain: data.metadata?.sourceURL?.replace(/^https?:\\/\\//, '').split('/')[0] || '',\n originalUrl: data.metadata?.sourceURL || '',\n linkedInUrl: safeLinkedInUrl,\n hasLinkedIn,\n debugInfo: {\n rawLinkedInUrl,\n cleanLinkedInUrl,\n dataKeys: Object.keys(data),\n searchComplete: true\n }\n }\n};"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "99f68bfa-6dc8-4a55-a930-f7fa24fe78aa",
"name": "Check if Company or Personal LI Profile",
"type": "n8n-nodes-base.if",
"notes": "This node only passes through company LI profiles (and not personal LI profiles). If personal LI profiles still fits your ICP, route that via the 'False' branch.",
"position": [
-4896,
1008
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c60eb149-fdda-4d58-b9ef-1dbc21472c63",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ $json.debugInfo.cleanLinkedInUrl }}",
"rightValue": "linkedin.com/company"
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "f46b91bc-f5b5-43e6-8055-089bc7932dcc",
"name": "Check root URL",
"type": "n8n-nodes-base.if",
"notes": "This node checks the status code to ensure the domain is healthy and responsive (so it does not waste any credits/scraping efforts).",
"position": [
-5808,
1008
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "47b571c7-b3de-4b32-a11d-acc68379f8a6",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $json.statusCode }}",
"rightValue": 200
},
{
"id": "7b8e4747-cbc4-467a-a84a-7557b70dcc64",
"operator": {
"type": "number",
"operation": "lte"
},
"leftValue": "={{ $json.statusCode }}",
"rightValue": 399
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "6e209053-4c83-4106-b1c0-fae7e3734d0f",
"name": "Extract Name From Email2",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-1648,
1200
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-nano",
"cachedResultName": "GPT-4.1-NANO"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "You are an AI assistant that extracts names from email addresses. You receive an email address and should look at the part before the \u201c@\u201d to determine if it is a person\u2019s first name or first and last name.\n\t1.\tIf you are extremely confident it is a real first name (e.g., \u201cjohn\u201d), output the first name only (e.g., \u201cJohn\u201d).\n\t2.\tIf you are extremely confident it is a real first and last name (e.g., \u201cjohn.smith\u201d), output both (e.g., \u201cJohn Smith\u201d).\n\t3.\tIf there is any doubt\u2014meaning it looks like a role, department, organization, or anything that is not clearly a person\u2019s name (e.g., \u201csupport,\u201d \u201cadmin,\u201d \u201cinfo,\u201d etc.)\u2014output the word there as the Name and First Name\n\nCapitalize names properly. Output only the name or there, with no additional text or punctuation.\n\nOutput the following:\n- Name\n- First Name\n- Last Name"
},
{
"content": "=Email: {{ $item(\"0\").$node[\"Webhook\"].json[\"email\"] }}"
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "7f9f9d1c-1c04-4d46-afb4-403b5c7b8938",
"name": "Wait2",
"type": "n8n-nodes-base.wait",
"position": [
-1280,
1200
],
"parameters": {
"unit": "minutes"
},
"typeVersion": 1.1
},
{
"id": "fdcd7556-4857-4b4f-a4d4-715b2f79d41c",
"name": "High Value Trials (70-89)",
"type": "n8n-nodes-base.if",
"position": [
-2336,
1200
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c27a01a4-5ae8-4870-9024-5d427cb38b69",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $json.output.scores.finalScore }}",
"rightValue": 70
},
{
"id": "c56c2b84-b4c9-4710-aa3e-c3da6fe4cd39",
"operator": {
"type": "number",
"operation": "lte"
},
"leftValue": "={{ $json.output.scores.finalScore }}",
"rightValue": 89
}
]
}
},
"typeVersion": 2.2
},
{
"id": "5a0bde80-7bb5-4a13-a27c-2807500a1614",
"name": "Very High Value Trials (90-100)",
"type": "n8n-nodes-base.if",
"position": [
-2336,
992
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c27a01a4-5ae8-4870-9024-5d427cb38b69",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $json.output.scores.finalScore }}",
"rightValue": 90
}
]
}
},
"typeVersion": 2.2
},
{
"id": "ba99157b-07de-4114-b34c-c7f1a16186c5",
"name": "Extract Name From Email3",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-1648,
1408
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-nano",
"cachedResultName": "GPT-4.1-NANO"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "You are an AI assistant that extracts names from email addresses. You receive an email address and should look at the part before the \u201c@\u201d to determine if it is a person\u2019s first name or first and last name.\n\t1.\tIf you are extremely confident it is a real first name (e.g., \u201cjohn\u201d), output the first name only (e.g., \u201cJohn\u201d).\n\t2.\tIf you are extremely confident it is a real first and last name (e.g., \u201cjohn.smith\u201d), output both (e.g., \u201cJohn Smith\u201d).\n\t3.\tIf there is any doubt\u2014meaning it looks like a role, department, organization, or anything that is not clearly a person\u2019s name (e.g., \u201csupport,\u201d \u201cadmin,\u201d \u201cinfo,\u201d etc.)\u2014output the word there as the Name and First Name\n\nCapitalize names properly. Output only the name or there, with no additional text or punctuation.\n\nOutput the following:\n- Name\n- First Name\n- Last Name"
},
{
"content": "=Email: {{ $item(\"0\").$node[\"Webhook\"].json[\"email\"] }}"
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "5139ced2-e900-4503-b469-2f7a90cd82cf",
"name": "Wait3",
"type": "n8n-nodes-base.wait",
"position": [
-1280,
1408
],
"parameters": {
"unit": "minutes"
},
"typeVersion": 1.1
},
{
"id": "30002cb6-a5d2-42be-a59c-0f38a870b398",
"name": "Mid Value Trials (50-69)",
"type": "n8n-nodes-base.if",
"position": [
-2336,
1408
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c27a01a4-5ae8-4870-9024-5d427cb38b69",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $json.output.scores.finalScore }}",
"rightValue": 50
},
{
"id": "c56c2b84-b4c9-4710-aa3e-c3da6fe4cd39",
"operator": {
"type": "number",
"operation": "lte"
},
"leftValue": "={{ $json.output.scores.finalScore }}",
"rightValue": 69
}
]
}
},
"typeVersion": 2.2
},
{
"id": "3a0d54c5-9fa9-46b5-a4e0-36dd36669660",
"name": "Extract Name From Email4",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-1648,
1648
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-nano",
"cachedResultName": "GPT-4.1-NANO"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "You are an AI assistant that extracts names from email addresses. You receive an email address and should look at the part before the \u201c@\u201d to determine if it is a person\u2019s first name or first and last name.\n\t1.\tIf you are extremely confident it is a real first name (e.g., \u201cjohn\u201d), output the first name only (e.g., \u201cJohn\u201d).\n\t2.\tIf you are extremely confident it is a real first and last name (e.g., \u201cjohn.smith\u201d), output both (e.g., \u201cJohn Smith\u201d).\n\t3.\tIf there is any doubt\u2014meaning it looks like a role, department, organization, or anything that is not clearly a person\u2019s name (e.g., \u201csupport,\u201d \u201cadmin,\u201d \u201cinfo,\u201d etc.)\u2014output the word there as the Name and First Name\n\nCapitalize names properly. Output only the name or there, with no additional text or punctuation.\n\nOutput the following:\n- Name\n- First Name\n- Last Name"
},
{
"content": "=Email: {{ $item(\"0\").$node[\"Webhook\"].json[\"email\"] }}"
}
]
},
"jsonOutput": true
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "9c75d444-52e6-4362-a6c3-62f8c1025408",
"name": "Wait4",
"type": "n8n-nodes-base.wait",
"disabled": true,
"position": [
-1280,
1664
],
"parameters": {
"unit": "minutes",
"amount": 20
},
"typeVersion": 1.1
},
{
"id": "44b49fca-6879-48d8-b045-98532f2b6aad",
"name": "Low Value Trials (0-49)",
"type": "n8n-nodes-base.if",
"position": [
-2336,
1648
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c27a01a4-5ae8-4870-9024-5d427cb38b69",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $json.output.scores.finalScore }}",
"rightValue": 0
},
{
"id": "c56c2b84-b4c9-4710-aa3e-c3da6fe4cd39",
"operator": {
"type": "number",
"operation": "lte"
},
"leftValue": "={{ $json.output.scores.finalScore }}",
"rightValue": 49
}
]
}
},
"typeVersion": 2.2
},
{
"id": "7d39e660-97e4-4856-8a25-c83b0cecd437",
"name": "Blacklist Regex Domains",
"type": "n8n-nodes-base.if",
"notes": "This removes trash emails, personal emails, employee emails and only passes through good B2B emails.",
"position": [
-6432,
1008
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "445af0a5-1a1b-4afe-9730-12af901491ec",
"operator": {
"type": "string",
"operation": "notRegex"
},
"leftValue": "={{ $json.root_domain }}",
"rightValue": "=^(gmail\\.com|yahoo\\.com|ymail\\.com|rocketmail\\.com|hotmail\\.com|aol\\.com|outlook\\.com|icloud\\.com|protonmail\\.com|pm\\.me|proton\\.me|zoho\\.com|yandex\\.com|mail\\.com|gmx\\.com|me\\.com|mac\\.com|sbcglobal\\.net|verizon\\.net|bellsouth\\.net|comcast\\.net|cox\\.net|earthlink\\.net|charter\\.net|att\\.net|frontier\\.com|optonline\\.net|shaw\\.ca|sympatico\\.ca|rogers\\.com|fastmail\\.com|qq\\.com|guerrillamail\\.com|10minutemail\\.com|maildrop\\.cc|anonaddy\\.com|live\\.com|msn\\.com|tutanota\\.com|hushmail\\.com|aol\\.co\\.uk|btinternet\\.com|sky\\.com|temp-mail\\.org|yopmail\\.com|throwawaymail\\.com|sharklasers\\.com|getnada\\.com|dispostable\\.com|trashmail\\.com|mohmal\\.com|inbox\\.com|rediffmail\\.com|naver\\.com|163\\.com|126\\.com|sina\\.com|wp\\.pl|seznam\\.cz|libero\\.it|mail\\.ru|centurylink\\.net|windstream\\.net|suddenlink\\.net|spectrum\\.net|telus\\.net|videotron\\.ca|bigpond\\.com|virginmedia\\.com|talktalk\\.co\\.uk|telenet\\.be|tempmail\\.com|burnermail\\.io|emailondeck\\.com|fakeinbox\\.com|mailinator\\.com|byom\\.de|inbox\\.lv|protonmail\\.ch|grr\\.la|spambox\\.us|.*\\.edu(\\.co)?)$"
},
{
"id": "40046651-a329-464d-ace0-17dde2f4ade3",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "",
"rightValue": ""
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "7f567204-a733-4afd-9987-f023c250ff3b",
"name": "Check LinkedIn is not null",
"type": "n8n-nodes-base.if",
"notes": "Checks to make sure the LinkedIn profile is not empty/null.",
"position": [
-5120,
1008
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "19aa2caa-6e6c-4b99-a41d-1512446dba5f",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.debugInfo.cleanLinkedInUrl }}",
"rightValue": ""
},
{
"id": "fb63985f-9a93-44d9-817d-20575375bd90",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.debugInfo.cleanLinkedInUrl }}",
"rightValue": "null"
},
{
"id": "5846c30a-6a75-4e8a-96be-fbbbe634a8cd",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.debugInfo.cleanLinkedInUrl }}",
"rightValue": ""
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "f301371e-132f-4df0-be6b-c4496ccd8723",
"name": "Audit LI Results",
"type": "n8n-nodes-base.if",
"notes": "This node audits the LI output from the agent to ensure there is a quality LI company profile (to continue the workflow). \n\nThis implies that any company that has a complete LI profile is likely worth it whereas if they do not have a complete profile they are likely not a good fit. ",
"position": [
-4272,
992
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "6886a47a-cc73-465e-aae4-e7a808b1ae9f",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.output.company_name }}",
"rightValue": ""
},
{
"id": "94a016a0-a9a2-4991-8fb5-7986be143042",
"operator": {
"type": "number",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.output.followers }}",
"rightValue": ""
},
{
"id": "bce36df9-ea31-49e2-908b-5975cdef410f",
"operator": {
"type": "number",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.output.employee_count }}",
"rightValue": ""
},
{
"id": "65251c39-1576-4302-ae17-e6c07d8f8bfb",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.output.headquarters_location }}",
"rightValue": ""
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "64331374-3051-4f88-b124-a9341d3193e3",
"name": "Firecrawl Scrape",
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"notes": "This uses Firecrawl to scrape the website and extract LinkedIn URL. Typically websites will have their social profiles on their website (namely the footer) and this will extract the LinkedIn. Can also be used to extract other channels (such as YouTube, \ud835\udd4f, etc.). ",
"position": [
-5568,
992
],
"parameters": {
"url": "={{ $('Extract Email Root Domain').item.json.root_domain }}",
"operation": "scrape",
"scrapeOptions": {
"options": {
"formats": {
"format": [
{
"type": "lin
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.
firecrawlApihttpQueryAuthinstantlyApiopenAiApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automatically processes new free-trial / lead sign-ups in real time: Catches a webhook from any source (Webflow form, Intercom, custom agent, etc.) Filters out personal / disposable / .edu emails → only business emails continue Validates the company website is live…
Source: https://n8n.io/workflows/13644/ — 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.
This workflow turns raw product inputs into a complete, launch-ready AI-generated social media campaign package. It accepts product details via webhook, sanitizes messy fields, generates a strategic c
🧾 An intelligent automation system that turns Google Meet recordings into structured meeting notes — integrating Fireflies.ai, OpenAI GPT-4.1-mini, Notion, Slack, Google Drive, and Gmail via n8n.
This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La
Production-ready Reddit lead generation system with progressive data loading for optimal UX. This workflow integrates with a web frontend, sending results in real-time as they're processed instead of
🧠 Automate end-to-end SEO blog creation and WordPress publishing using a GPT-5 multi-agent workflow with real-time research, metadata generation, and optional featured images.