This workflow corresponds to n8n.io template #15390 — we link there as the canonical source.
This workflow follows the HTTP Request → Informationextractor 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 →
{
"nodes": [
{
"id": "61e7b101-ae69-4a2a-a073-9701141e6817",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
880,
-288
],
"parameters": {
"width": 480,
"height": 480,
"content": "### How it works\n\n1. The workflow triggers manually to initiate data processing.\n2. Environment variables are set for HTTP requests and data handling.\n3. Data is fetched from DataForSEO and parsed.\n4. SQL queries are executed to prepare, filter, and merge data.\n5. Information is extracted and validated using AI models.\n6. Results are formatted and finalized for output.\n\n### Setup steps\n\n- [ ] Ensure DataForSEO and database credentials are configured.\n- [ ] Set up OpenAI credentials for AI model usage.\n- [ ] Verify environment variables like target and language are correctly set.\n\n### Customization\n\nThe AI model prompts and data parsing logic can be customized based on specific SEO metrics and outputs."
},
"typeVersion": 1
},
{
"id": "c8fa3ba8-9e32-47c4-a365-653f8ea94729",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1392,
-112
],
"parameters": {
"color": 7,
"width": 400,
"height": 304,
"content": "## Manual trigger setup\n\nInitiates the workflow and sets environment variables."
},
"typeVersion": 1
},
{
"id": "4c38f6de-10a7-42fb-a43b-b597108615f4",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1824,
-96
],
"parameters": {
"color": 7,
"width": 864,
"height": 272,
"content": "## Fetch and prepare data\n\nHandles HTTP requests and prepares items for processing."
},
"typeVersion": 1
},
{
"id": "d37053c2-dd3b-4998-bd77-85447a6585e4",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2720,
-208
],
"parameters": {
"color": 7,
"width": 416,
"height": 496,
"content": "## Extract and merge data\n\nParses SERP data and merges with existing data through SQL."
},
"typeVersion": 1
},
{
"id": "aeae40db-bee5-48ae-ad0e-d9241a92b4a7",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
3168,
-128
],
"parameters": {
"color": 7,
"width": 528,
"height": 320,
"content": "## Check conditions and optimize\n\nEvaluates audit status and optimizes prompts for AI processing."
},
"typeVersion": 1
},
{
"id": "73838fdb-c329-43b7-a735-9c582ec20cca",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
3712,
-192
],
"parameters": {
"color": 7,
"width": 496,
"height": 560,
"content": "## AI-based information extraction\n\nUtilizes AI to extract and verify information."
},
"typeVersion": 1
},
{
"id": "edcdfbf8-c19d-41e6-9858-1f5e90e7d438",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
4256,
-112
],
"parameters": {
"color": 7,
"width": 768,
"height": 416,
"content": "## Conditional check and final output\n\nChecks results and formats for final output or further SQL processing."
},
"typeVersion": 1
},
{
"id": "6bd8029e-fbf6-4422-b139-33e9e90206a4",
"name": "Get SERP Data for SEO",
"type": "n8n-nodes-dataforseo.dataForSeo",
"position": [
2544,
16
],
"parameters": {
"os": "ios",
"device": "mobile",
"keyword": "={{ $json.keyword }}",
"resource": "serp",
"se_domain": "google.com",
"language_name": "={{ $json.market_language }}",
"location_name": "={{ $json.market_location }}"
},
"credentials": {
"dataForSeoApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "1600f493-f0bd-4250-85b3-9ba1c8fbcf23",
"name": "Manual Start Trigger",
"type": "n8n-nodes-base.manualTrigger",
"position": [
1424,
16
],
"parameters": {},
"typeVersion": 1
},
{
"id": "471a2bd6-5840-460a-918b-0991e7f0c0a1",
"name": "Parse Domain and Path",
"type": "n8n-nodes-base.code",
"position": [
2768,
112
],
"parameters": {
"jsCode": "// Flatten DataForSEO results \u2014 robust string parsing for n8n sandbox\n\nconst input = $input.first().json;\n\n// Support both:\n// 1) top-level object\n// 2) top-level array with first object\nconst root = Array.isArray(input) ? input[0] : input;\n\nconst items = root?.tasks?.[0]?.result?.[0]?.items || [];\n\n// \ud83d\udea8 THE FIX: Count the total items right here\nconst totalCount = items.length;\n\nreturn items.map(item => {\n const rawUrl = typeof item.url === 'string' ? item.url : '';\n\n let host = '';\n let domain = '';\n let path = '/';\n let query = '';\n let fragment = '';\n let parse_status = 'ok';\n\n try {\n if (rawUrl) {\n // Remove protocol\n let clean = rawUrl.replace(/^https?:\\/\\//i, '');\n\n // Extract fragment\n const hashIndex = clean.indexOf('#');\n if (hashIndex > -1) {\n fragment = clean.substring(hashIndex + 1);\n clean = clean.substring(0, hashIndex);\n }\n\n // Extract query\n const queryIndex = clean.indexOf('?');\n if (queryIndex > -1) {\n query = clean.substring(queryIndex + 1);\n clean = clean.substring(0, queryIndex);\n }\n\n // Split host/path\n const slashIndex = clean.indexOf('/');\n if (slashIndex > -1) {\n host = clean.substring(0, slashIndex);\n path = clean.substring(slashIndex) || '/';\n } else {\n host = clean;\n path = '/';\n }\n\n // Normalize domain\n domain = host.replace(/^www\\./i, '');\n } else {\n parse_status = 'empty-url';\n }\n } catch (e) {\n parse_status = 'parse-error';\n host = '';\n domain = '';\n path = '/';\n }\n\n return {\n json: {\n total_serp_results: totalCount, // \ud83d\udea8 Passed along for the LLM\n rank: item.rank_absolute || 0,\n rank_group: item.rank_group || 0,\n page: item.page || 0,\n serp_type: item.type || '',\n title: item.title || '',\n description: item.description || '',\n full_url: rawUrl,\n host,\n domain,\n path,\n query,\n fragment,\n parse_status\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "7c5c06cd-2f29-4910-b14b-a3b9d1f006e2",
"name": "SQL-Based Data Merge",
"type": "n8n-nodes-base.merge",
"position": [
2992,
16
],
"parameters": {
"mode": "combineBySql",
"query": "SELECT \n serp.rank, \n serp.title, \n serp.full_url, \n serp.domain,\n rules.rule_id, \n COALESCE(rules.goggle_id, (SELECT MAX(goggle_id) FROM input1)) AS goggle_id,\n rules.action AS existing_action, \n rules.strength AS existing_strength,\n CASE \n WHEN rules.rule_id IS NOT NULL THEN rules.action \n ELSE '\ud83d\udea8 NIEUW' \n END AS audit_status\nFROM input2 AS serp\nLEFT JOIN input1 AS rules\n ON serp.domain = rules.rule_target\nORDER BY serp.rank ASC;",
"options": {
"emptyQueryResult": "success"
}
},
"typeVersion": 3.2
},
{
"id": "599409ea-4c5e-4c19-8558-bc741314e01a",
"name": "Execute Postgres Query",
"type": "n8n-nodes-base.postgres",
"position": [
2768,
-80
],
"parameters": {
"query": "SELECT \n g.id AS goggle_id, \n r.id AS rule_id,\n r.action, \n r.strength, \n r.target AS rule_target, \n r.path_pattern \nFROM goggles g\nLEFT JOIN goggle_rules r ON g.id = r.goggle_id\n-- Pakt automatisch je actieve Goggle (voorkomt problemen als je er later meer toevoegt)\nWHERE g.name = 'StuccoOS Vakkennis';",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "c3d718a5-6531-4609-92b7-1ece5eeceb0e",
"name": "Use OpenAI GPT-4o",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
3840,
224
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "o4-mini",
"cachedResultName": "o4-mini"
},
"options": {
"reasoningEffort": "medium"
},
"responsesApiEnabled": false
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "308e6f50-cc17-44c8-9d35-19d21efb5ecf",
"name": "Post to DataForSEO API",
"type": "n8n-nodes-base.httpRequest",
"position": [
1872,
16
],
"parameters": {
"url": "https://api.dataforseo.com/v3/dataforseo_labs/google/domain_intersection/live",
"method": "POST",
"options": {},
"jsonBody": "=[\n {\n \"target1\": \"{{ $json.target1 }}\",\n \"target2\": \"{{ $json.target2 }}\",\n \"language_code\": \"{{ $json.language_code }}\",\n \"location_code\": {{ $json.location_code }},\n \"intersections\": {{ $json.intersections }},\n \"include_serp_info\": {{ $json.include_serp_info }},\n \"limit\": {{ $json.limit }},\n \"order_by\": {{ JSON.stringify($json.order_by) }}\n }\n]",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"nodeCredentialType": "dataForSeoApi"
},
"credentials": {
"dataForSeoApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "8284e61e-04ab-437c-8cea-3d0d23c7a039",
"name": "Process Intersection Results",
"type": "n8n-nodes-base.code",
"position": [
2096,
16
],
"parameters": {
"jsCode": "// 1. Fetch the items from the DataForSEO intersection results\nconst items = $input.first().json.tasks[0].result[0].items;\n\n// 2. Fetch the \"Dashboard\" settings from your env_ node\nconst env = $('Set Environment Variables').item.json;\n\n// 3. Map and Inject: Attach context to every keyword\nreturn items.map(item => {\n return {\n json: {\n keyword: item.keyword_data.keyword,\n search_volume: item.keyword_data.keyword_info.search_volume,\n intent: item.keyword_data.search_intent_info.main_intent,\n source: `Intersection: ${env.target1} vs ${env.target2}`,\n discovery_priority: item.keyword_data.keyword_info.search_volume > 1000 ? 'high' : 'medium',\n \n // Accessible env settings for downstream nodes (SERP & AI)\n market_location: env.Location,\n market_language: env.Language,\n market_language_code: env.language_code,\n // We pass this so the AI knows which industry it's auditing\n industry_context: env.industry_context || 'plastering, ceiling, and wall finishing'\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "d75960be-9826-4b54-915e-d96576012ad4",
"name": "Run SQL Data Fetch",
"type": "n8n-nodes-base.postgres",
"position": [
4800,
0
],
"parameters": {
"query": "INSERT INTO goggle_rules (\n goggle_id, \n action, \n strength, \n target, \n target_type, \n path_pattern, \n confidence, \n site_type, \n reasoning, \n source, \n updated_at\n)\nVALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()\n)\nON CONFLICT (goggle_id, target, target_type, path_pattern)\nDO UPDATE SET\n action = CASE \n WHEN EXCLUDED.confidence = 'high' OR goggle_rules.confidence != 'high' THEN EXCLUDED.action \n ELSE goggle_rules.action \n END,\n strength = CASE \n WHEN EXCLUDED.confidence = 'high' OR goggle_rules.confidence != 'high' THEN EXCLUDED.strength \n ELSE goggle_rules.strength \n END,\n confidence = CASE \n WHEN EXCLUDED.confidence = 'high' OR goggle_rules.confidence != 'high' THEN EXCLUDED.confidence \n ELSE goggle_rules.confidence \n END,\n reasoning = CASE \n WHEN EXCLUDED.confidence = 'high' OR goggle_rules.confidence != 'high' THEN EXCLUDED.reasoning \n ELSE goggle_rules.reasoning \n END,\n updated_at = NOW();",
"options": {
"queryBatching": "transaction",
"queryReplacement": "={{ [$json.goggle_id, $json.action, $json.strength, $json.target, $json.target_type, $json.path_pattern, $json.confidence, $json.site_type, $json.reasoning, $json.source] }}"
},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "c96d71ab-00f1-4bb8-884b-40285a3fa3bf",
"name": "Information Extraction Agent",
"type": "@n8n/n8n-nodes-langchain.informationExtractor",
"position": [
3776,
0
],
"parameters": {
"text": "=### CRITICAL TASK\nYou are provided with a batch of EXACTLY {{ $json.item_count }} domains.\nYour output 'rules' array MUST contain EXACTLY {{ $json.item_count }} entries. \n\n### DOMAIN DATA\n{{ $json.domain_list_string }}",
"options": {
"systemPromptTemplate": "You are a strict SEO Domain Auditor specializing in the Dutch plastering, ceiling, and wall finishing industry.\n\nYOUR MISSION:\nYou will receive a list of domains and their context. You MUST extract and classify EVERY SINGLE DOMAIN into the `rules` array. Do not skip any.\n\nCLASSIFICATION RULES:\n\n1. SITE TYPE:\n- `specialist`: Manufacturers (e.g., Knauf, Gyproc), dedicated plastering contractors, industry associations (NOA), technical blogs.\n- `retailer`: DIY hardware stores (e.g., Gamma, Hornbach, Praxis), general construction webshops.\n- `aggregator`: Lead-generation platforms (e.g., Werkspot, Trustoo), quotation aggregators, social media, forums, and legal/news sites.\n\n2. ACTION & STRENGTH (Strict Integers 1-5):\n- `boost` (Strength 2-5): For specialists and manufacturers. Top manufacturers get 5. Local specialists get 2-4.\n- `downrank` (Strength 2-4): For retailers and aggregators.\n- `discard` (Strength 1): For social media (YouTube, Facebook), marketplaces (Marktplaats), and entirely unrelated sites (e.g., agriculture, law).\n\n3. BATHROOM NUANCE (Badkamers):\n- Do not automatically discard bathroom sites. If they focus on \"Beton Cir\u00e9\", \"Microcement\", or waterproof plastering, classify them as `specialist` and `boost`."
},
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"rules\": {\n \"type\": \"array\",\n \"description\": \"An array containing the classification for EVERY domain provided in the input. You MUST NOT skip any domain.\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"domain\": {\n \"type\": \"string\",\n \"description\": \"The exact domain name from the input list (e.g., 'knauf.nl').\"\n },\n \"reasoning\": {\n \"type\": \"string\",\n \"description\": \"THINKING STEP: Write a concise 1-2 sentence analysis in Dutch. Explain WHY you chose the site_type and action. Look for specialist keywords versus generic retail or lead-gen signals.\"\n },\n \"site_type\": {\n \"type\": \"string\",\n \"enum\": [\"specialist\", \"retailer\", \"aggregator\"],\n \"description\": \"Categorize the domain. 'specialist' = plastering professionals, manufacturers, or technical knowledge bases. 'retailer' = DIY stores, general hardware webshops. 'aggregator' = lead-generation (Werkspot), social media, directories, forums.\"\n },\n \"action\": {\n \"type\": \"string\",\n \"enum\": [\"boost\", \"downrank\", \"discard\"],\n \"description\": \"'boost' = high quality/relevant specialists. 'downrank' = retailers or aggregators with mixed value. 'discard' = social media, unrelated noise, purely local promotion.\"\n },\n \"strength\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 5,\n \"description\": \"Assign a strict integer weight from 1 to 5. 5 = top authority/manufacturer. 2-4 = specialist/retailer. 1 = discard or very low value. DO NOT use decimals.\"\n }\n },\n \"required\": [\"domain\", \"reasoning\", \"site_type\", \"action\", \"strength\"]\n }\n }\n },\n \"required\": [\"rules\"]\n}"
},
"typeVersion": 1.2
},
{
"id": "d22e292b-953e-4991-93ad-8683deb9e658",
"name": "If Audit Status Pending",
"type": "n8n-nodes-base.if",
"position": [
3216,
16
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "f456723d-d00e-4756-b709-11a04fc80944",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.audit_status }}",
"rightValue": "=\ud83d\udea8 NIEUW"
}
]
},
"looseTypeValidation": true
},
"typeVersion": 2.3
},
{
"id": "34e46dee-c0e5-42bc-b21d-45f3e3e99d39",
"name": "Split Rules",
"type": "n8n-nodes-base.splitOut",
"position": [
4496,
0
],
"parameters": {
"options": {},
"fieldToSplitOut": "rules"
},
"typeVersion": 1
},
{
"id": "b1b78d85-0306-450c-9a55-57e8519ffcdb",
"name": "Format Data for SQL",
"type": "n8n-nodes-base.code",
"position": [
4640,
0
],
"parameters": {
"jsCode": "const allItems = $input.all();\n\nconst goggleId = $('Execute Postgres Query').first().json.goggle_id;\n\n// 2. Harde stop als de ID mist (Geen fallback ID's meer!)\nif (!goggleId) {\n throw new Error(\"FATAL: Geen Goggle ID gevonden. Controleer of 'Run SQL Query' een geldige ID uit de database heeft gehaald.\");\n}\n\nreturn allItems.map(item => {\n const rule = item.json;\n \n // Normalisatie\n const domain = rule.domain ? String(rule.domain).toLowerCase().trim() : 'geen-domein';\n const actionStr = rule.action ? String(rule.action).toLowerCase().trim() : 'discard';\n const finalAction = ['boost', 'downrank', 'discard'].includes(actionStr) ? actionStr : 'discard';\n const allowedTypes = ['specialist', 'retailer', 'aggregator'];\n const finalType = allowedTypes.includes(rule.site_type) ? rule.site_type : 'aggregator';\n\n return {\n json: {\n goggle_id: goggleId, // 100% dynamisch\n target: domain,\n action: finalAction,\n strength: parseInt(rule.strength) || 1,\n target_type: 'site',\n path_pattern: '',\n confidence: 'low',\n site_type: finalType,\n reasoning: rule.reasoning || 'SERP Quick Scan',\n source: 'serp_audit'\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "9062350f-b486-474d-ac71-16d083280bc4",
"name": "Optimize AI Prompts",
"type": "n8n-nodes-base.code",
"position": [
3552,
0
],
"parameters": {
"jsCode": "const items = $input.all();\n\n// 1. Safety Guard: If no new domains are found, stop the workflow here to save credits.\nif (items.length === 0) return [];\n\n// 2. Fetch goggle_id from the incoming data stream (SQL Merge passed it here)\nconst goggleId = items[0].json.goggle_id;\n\n// 3. Strip tokens: Only keep domain and title (the context)\nconst optimizedList = items.map(item => {\n return {\n domain: item.json.domain,\n context: item.json.title\n };\n});\n\n// 4. Create the Master AI Task\nreturn [{\n json: {\n goggle_id: goggleId, // Passed through for the final SORT node\n item_count: optimizedList.length,\n domain_list_string: JSON.stringify(optimizedList, null, 2)\n }\n}];"
},
"typeVersion": 2
},
{
"id": "2dc2ef4e-7788-40af-abd7-9cfc2b3a972c",
"name": "Verify AI Process",
"type": "n8n-nodes-base.code",
"position": [
4080,
0
],
"parameters": {
"jsCode": "// 1. Specifically look back at the Information Extractor node, ignoring the SQL output\nconst aiOutput = $('Information Extraction Agent').first().json.output.rules || [];\n\n// 2. The rest of your logic remains the same\nconst expectedCount = $('Optimize AI Prompts').first().json.item_count;\nlet currentRetries = $input.first().json.retry_count || 0;\n\nconst isComplete = (aiOutput.length === expectedCount) || (currentRetries >= 3);\n\nreturn [{\n json: {\n rules: aiOutput,\n is_complete: isComplete,\n retry_count: currentRetries + 1,\n actual_count: aiOutput.length,\n expected_count: expectedCount\n }\n}];"
},
"typeVersion": 2
},
{
"id": "636a9198-0b64-4f97-bcc3-f7ec52f5636b",
"name": "If Complete Status Is True",
"type": "n8n-nodes-base.if",
"position": [
4304,
112
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c1d2e3f4",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.is_complete }}",
"rightValue": "={{ true }}"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "7ecd5d7b-1efe-4950-b54f-9e1455c82f77",
"name": "Set Environment Variables",
"type": "n8n-nodes-base.set",
"position": [
1600,
16
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "target1-id",
"name": "target1",
"type": "string",
"value": "knauf.com"
},
{
"id": "target2-id",
"name": "target2",
"type": "string",
"value": "gyproc.nl"
},
{
"id": "246737d1-726b-4405-984a-591b886890ea",
"name": "Language",
"type": "string",
"value": "Dutch"
},
{
"id": "lang-id",
"name": "language_code",
"type": "string",
"value": "nl"
},
{
"id": "5c4b2b6c-b537-40a3-8c42-bb857a8ce1bd",
"name": "Location",
"type": "string",
"value": "Netherlands"
},
{
"id": "loc-id",
"name": "location_code",
"type": "number",
"value": 2528
},
{
"id": "inter-id",
"name": "intersections",
"type": "boolean",
"value": true
},
{
"id": "serp-id",
"name": "include_serp_info",
"type": "boolean",
"value": true
},
{
"id": "limit-id",
"name": "limit",
"type": "number",
"value": 10
},
{
"id": "order-id",
"name": "order_by",
"type": "array",
"value": "={{ [\"keyword_data.keyword_info.search_volume,desc\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "09c19744-c6c6-49be-8e63-e11c89d970ef",
"name": "Batch Process Items in Tens",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2304,
16
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "77aee493-aa2c-4856-87df-2d1e7889b050",
"name": "Fetch All Goggle Entries",
"type": "n8n-nodes-base.postgres",
"position": [
1888,
1072
],
"parameters": {
"query": "SELECT \n *, \n '{{ $json.mode }}' as run_mode \nFROM {{ $json.table }}\nWHERE {{ $json.setting_L }} = '{{ $json.setting_R }}';",
"options": {
"queryReplacement": ""
},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "8cc09057-b8fd-451e-9ac8-124b098de558",
"name": "Batch Process Goggles",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2544,
720
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "f5fcf5c2-5259-4633-a091-844a35a144b1",
"name": "Check Gist ID Presence",
"type": "n8n-nodes-base.if",
"position": [
2768,
640
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "has-gist-id",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.github_gist_id }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "8dcd8f20-d488-4c54-bc66-2b387a03d621",
"name": "Patch GitHub Gist",
"type": "n8n-nodes-base.httpRequest",
"position": [
3280,
544
],
"parameters": {
"url": "=https://api.github.com/gists/{{ $json.github_gist_id }}",
"method": "PATCH",
"options": {
"response": {
"response": {
"neverError": true
}
}
},
"jsonBody": "={{ JSON.stringify({\n \"description\": $json.goggle_name || 'stephan-koning',\n \"files\": {\n \"stucco-search.goggle\": {\n \"content\": $json.full_goggle_text\n }\n }\n}) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "githubApi"
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "d25c6416-4a7b-43e1-bd0b-f8dcb71d5d59",
"name": "Post New GitHub Gist",
"type": "n8n-nodes-base.httpRequest",
"position": [
2992,
736
],
"parameters": {
"url": "https://api.github.com/gists",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify({\n \"description\": $json.goggle_name || 'stephan-koning',\n \"files\": {\n \"stucco-search.goggle\": {\n \"content\": $json.full_goggle_text\n }\n }\n}) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "githubApi"
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "117d4ada-345a-4e63-b973-e79fd944770d",
"name": "Store Gist ID in Database",
"type": "n8n-nodes-base.postgres",
"position": [
3280,
736
],
"parameters": {
"table": {
"__rl": true,
"mode": "list",
"value": "goggles",
"cachedResultName": "goggles"
},
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"columns": {
"value": {
"id": "={{ $('Check Gist ID Presence').item.json.goggle_id }}",
"author": "StuccoOS",
"github_gist_id": "={{ $json.id }}"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": false,
"required": true,
"displayName": "id",
"defaultMatch": true,
"canBeUsedToMatch": true
},
{
"id": "name",
"type": "string",
"display": true,
"removed": false,
"required": true,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "description",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "author",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "author",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "public",
"type": "boolean",
"display": true,
"removed": false,
"required": false,
"displayName": "public",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "avatar",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "avatar",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "homepage",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "homepage",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "github_gist_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "github_gist_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "created_at",
"type": "dateTime",
"display": true,
"removed": false,
"required": false,
"displayName": "created_at",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "updated_at",
"type": "dateTime",
"display": true,
"removed": false,
"required": false,
"displayName": "updated_at",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"id"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "d786624d-9913-4e53-9247-bce5cb0d3558",
"name": "Wait 3 Seconds",
"type": "n8n-nodes-base.wait",
"position": [
3568,
736
],
"parameters": {
"amount": 3
},
"typeVersion": 1.1
},
{
"id": "d27fcdfb-5f97-4476-b6ea-3b5de03d089a",
"name": "Batch Process Domains",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2336,
1488
],
"parameters": {
"options": {
"reset": false
}
},
"typeVersion": 3
},
{
"id": "c503071b-4285-42de-b158-b0ea6f622f66",
"name": "Trigger Every 6 Hours",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
1440,
976
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"typeVersion": 1.2
},
{
"id": "0629e52a-8671-44a9-a474-3de3b81101b6",
"name": "Trigger Weekly on Monday",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
1440,
1168
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtDay": [
1
]
}
]
}
},
"typeVersion": 1.2
},
{
"id": "3bfcfd7e-7a05-4a1e-b1e5-13ff86b6b49c",
"name": "Set Update Mode Fields",
"type": "n8n-nodes-base.set",
"position": [
1664,
976
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "goggle-id",
"name": "goggle_id",
"type": "string",
"value": "a7b8c9d0-1234-4567-89ab-cdef01234567"
},
{
"id": "mode",
"name": "mode",
"type": "string",
"value": "update"
},
{
"id": "d25d9431-553d-44c5-837b-a1521bd3068a",
"name": "table",
"type": "string",
"value": "v_goggle_full_preview"
},
{
"id": "4871acf7-8e1f-4e98-b1bc-f47a0db2fc69",
"name": "setting_L",
"type": "string",
"value": "goggle_id"
},
{
"id": "42beceb3-7b7e-464f-a1c8-52a03c098a5e",
"name": "setting_R",
"type": "string",
"value": "a7b8c9d0-1234-4567-89ab-cdef01234567"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "5daf11e5-26a4-4902-be49-f27b956ced47",
"name": "Switch by Mode",
"type": "n8n-nodes-base.switch",
"position": [
2112,
1072
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "\ud83d\udd04 Update Gist Only",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "is-update",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.run_mode }}",
"rightValue": "update"
}
]
},
"renameOutput": true
},
{
"outputKey": "\ud83d\udd0d Run FireCrawl + AI",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "is-discovery",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.run_mode }}",
"rightValue": "discovery"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.4
},
{
"id": "d87a55d6-c648-4e40-a5b7-fc550f47262e",
"name": "OpenAI GPT-4o",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
3088,
1296
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o",
"cachedResultName": "gpt-4o"
},
"options": {
"temperature": 0.3
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "73c5bdd7-915d-40a3-a327-4cc619c457a8",
"name": "Map Firecrawl Data",
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"onError": "continueErrorOutput",
"position": [
2560,
1312
],
"parameters": {
"url": "=https://{{ $json.target }}",
"limit": 500,
"timeout": 30000,
"resource": "MapSearch",
"operation": "map",
"requestOptions": {}
},
"credentials": {
"firecrawlApi": {
"name": "<your credential>"
}
},
"executeOnce": false,
"retryOnFail": true,
"typeVersion": 1,
"alwaysOutputData": false,
"waitBetweenTries": 5000
},
{
"id": "b7be2714-ff5e-4aa2-a9ad-46bbe3f0bc03",
"name": "Set Discovery Mode Fields",
"type": "n8n-nodes-base.set",
"position": [
1664,
1168
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "goggle-id",
"name": "goggle_id",
"type": "string",
"value": "a7b8c9d0-1234-4567-89ab-cdef01234567"
},
{
"id": "mode",
"name": "mode",
"type": "string",
"value": "discovery"
},
{
"id": "d25d9431-553d-44c5-837b-a1521bd3068a",
"name": "table",
"type": "string",
"value": "goggle_rules"
},
{
"id": "4871acf7-8e1f-4e98-b1bc-f47a0db2fc69",
"name": "setting_L",
"type": "string",
"value": "confidence"
},
{
"id": "42beceb3-7b7e-464f-a1c8-52a03c098a5e",
"name": "setting_R",
"type": "string",
"value": "=low"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "15a1af19-e2b2-4189-8ce1-85f8b8720a03",
"name": "Parse Domain Structures",
"type": "n8n-nodes-base.code",
"position": [
2784,
1072
],
"parameters": {
"jsCode": "const items = $input.all();\n\n// Safety check for empty data\nif (!items || items.length === 0) {\n return [{ json: { domain: 'unknown', error: 'No data' } }];\n}\n\n// Helper function (buiten de loop gehouden voor snelheid)\nconst parseUrlManual = (urlStr) => {\n try {\n let clean = String(urlStr || '');\n if (clean.startsWith('http://')) clean = clean.substring(7);\n if (clean.startsWith('https://')) clean = clean.substring(8);\n if (clean.startsWith('www.')) clean = clean.substring(4);\n let parts = clean.split('/');\n let hostname = parts[0] || 'unknown';\n let pathname = '/' + parts.slice(1).join('/').split('?')[0].split('#')[0];\n if (pathname.length > 1 && pathname.endsWith('/')) pathname = pathname.slice(0, -1);\n if (!pathname) pathname = '/';\n return { hostname, pathname, original: urlStr };\n } catch (e) { \n return { hostname: 'error', pathname: '/', original: urlStr }; \n }\n};\n\nconst VAKKENNIS_KWS = ['expertise', 'trainingen', 'technisch', 'bestek', 'richtlijn', 'instructie', 'kennis', 'bim', 'stappenplan', 'projecten'];\nconst RETAIL_KWS = ['/product/', '/product-categorie/', '/shop/', '/merk/', 'winkelwagen', 'checkout', 'cart', 'bestellen'];\n\n// \ud83d\udea8 BATCH LOOP START: Verwerk ELK item in de batch apart\nreturn items.map((item) => {\n const mapData = item.json;\n const urlsToProcess = mapData.links || [];\n const totalScanned = urlsToProcess.length;\n\n // \ud83d\udea8 ROCKSOLID FIX: Haal goggle_id direct uit de input stroom of gebruik fallback. Geen storingsgevoelige $('Node') references meer!\n const goggleId = mapData.goggle_id || \"a7b8c9d0-1234-4567-89ab-cdef01234567\";\n \n const rawDomain = urlsToProcess[0]?.url || mapData.url || mapData.target || '';\n const domain = parseUrlManual(rawDomain).hostname;\n\n let pathCounts = {};\n let retailHits = 0;\n let cleanUrlsLog = [];\n\n urlsToProcess.forEach(urlItem => {\n const urlString = typeof urlItem === 'object' ? urlItem.url : urlItem;\n if (!urlString) return;\n \n const parsed = parseUrlManual(urlString);\n const pathLower = parsed.pathname.toLowerCase();\n \n const isJunk = pathLower.includes('sitemap') || pathLower.includes('.xml') || pathLower.includes('/tag/');\n if (isJunk) return;\n\n if (cleanUrlsLog.length < 100) cleanUrlsLog.push(parsed.original);\n \n if (RETAIL_KWS.some(kw => pathLower.includes(kw))) retailHits++;\n\n const segments = parsed.pathname.split('/').filter(s => s.length > 0);\n let currentPath = '';\n \n for (let i = 0; i < Math.min(segments.length, 3); i++) {\n currentPath += '/' + segments[i];\n if (!pathCounts[currentPath]) {\n pathCounts[currentPath] = { count: 0, vakkennisHits: 0, samples: [] };\n }\n pathCounts[currentPath].count++;\n \n VAKKENNIS_KWS.forEach(kw => {\n if (pathLower.includes(kw)) pathCounts[currentPath].vakkennisHits++;\n });\n \n if (pathCounts[currentPath].samples.length < 3) {\n pathCounts[currentPath].samples.push(parsed.original);\n }\n }\n });\n\n // Calculate ratios safely (prevent NaN if totalScanned is 0)\n const retailRatio = totalScanned > 0 ? (retailHits / totalScanned) : 0;\n const isRetailer = retailRatio > 0.2 || retailHits > 20;\n\n let powerFolders = Object.entries(pathCounts)\n .map(([path, data]) => {\n const isExpertise = data.vakkennisHits > 0 && (data.vakkennisHits / data.count >= 0.1);\n return { path, count: data.count, isExpertise, examples: data.samples };\n })\n .filter(f => f.count >= 3)\n .sort((a, b) => b.count - a.count);\n\n const finalFolders = powerFolders.filter((folder, index, self) => {\n const isRetailFolder = RETAIL_KWS.some(kw => folder.path.toLowerCase().includes(kw));\n const isRoot = folder.count > (totalScanned * 0.9) && folder.path.split('/').length <= 2 && !folder.isExpertise && !isRetailFolder;\n \n const hasSpecificChild = self.some(other => \n other.path !== folder.path && other.path.startsWith(folder.path + '/') && other.count >= (folder.count * 0.8)\n );\n return !isRoot && !hasSpecificChild;\n }).slice(0, 15);\n\n const retailRatioPercent = Math.round(retailRatio * 100);\n\n // auditText wordt nu netjes IN de loop gedeclareerd en toegewezen\n const auditText = [\n `## DOMAIN AUDIT REQUEST: ${domain}`,\n `Totaal URLs gescand: ${totalScanned}`,\n `Retail/Product URL ratio: ${retailRatioPercent}%`,\n `Harde Retailer Indicatie: ${isRetailer ? 'JA (Grote catalogus/shop gevonden)' : 'NEE'}`,\n ``,\n `### Power Folders (Inhoudsanalyse):`,\n ...finalFolders.map(f => `- ${f.path} (${f.count} pagina's) ${f.isExpertise ? '[VAKKENNIS GEDETECTEERD]' : ''}\\n Voorbeelden: ${f.examples.join(' | ')}`)\n ].join('\\n');\n\n // Stuur het resultaat voor dit specifieke domein terug\n return { \n json: { \n domain, \n goggle_id: goggleId, \n audit_text: auditText, \n debug_log: { \n scanned_total: totalScanned, \n retail_hits: retailHits, \n is_retailer: isRetailer, \n power_folders: finalFolders \n } \n } \n };\n});"
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "dd735944-5281-4e36-afe4-1b29b55da9ac",
"name": "Build Database Rules",
"type": "n8n-nodes-base.code",
"position": [
3328,
1072
],
"parameters": {
"jsCode": "// Pak ALLE items die van de AI-node komen\nconst allItems = $input.all();\n\nreturn allItems.map((item, index) => {\n const ai = item.json.output || {};\n \n // \ud83d\udea8 FIX: We gebruiken hier de EXACTE naam van de node in je workflow\n const upstream = $items('Parse Domain Structures')[index].json;\n\n \n return {\n json: {\n // We zorgen dat er altijd een ID is, anders pakt hij de default\n goggle_id: upstream.goggle_id || \"a7b8c9d0-1234-4567-89ab-cdef01234567\",\n action: ai.action || 'boost',\n strength: parseInt(ai.strength) || 3,\n target: upstream.domain, \n target_type: 'site',\n path_pattern: '',\n confidence: 'high',\n site_type: ai.site_type || 'unknown',\n reasoning: ai.reasoning || '',\n source: 'deep_audit'\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "0e838cf2-260b-4424-90d7-b1a3b8ef9e13",
"name": "Insert Rule into Database",
"type": "n8n-nodes-base.postgres",
"position": [
3520,
1072
],
"parameters": {
"query": "INSERT INTO goggle_rules (\n goggle_id,\n action,\n strength,\n target,\n target_type,\n path_pattern, -- Keep this!\n confidence,\n site_type,\n reasoning,\n source,\n updated_at\n)\nVALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()\n)\nON CONFLICT (goggle_id, target, target_type, path_pattern)\nDO UPDATE SET\n action = EXCLUDED.action,\n strength = EXCLUDED.strength,\n confidence = EXCLUDED.confidence,\n site_type = EXCLUDED.site_type,\n reasoning = EXCLUDED.reasoning,\n source = EXCLUDED.source,\n updated_at = NOW();",
"options": {
"queryBatching": "transaction",
"queryReplacement": "={{ [\n $json.goggle_id || \"\", \n $json.action || \"boost\", \n $json.strength || 3, \n $json.target || \"unknown\", \n $json.target_type || \"site\", \n $json.path_pattern || \"\", // The Fix: if empty, send empty string\n $json.confidence || \"low\", \n $json.site_type || \"unknown\", \n $json.reasoning || \"\", \n $json.source || \"audit\"\n] }}"
},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "d8c5a87e-31ee-4058-b4e1-5f782ffa73a2",
"name": "Information Extractor Agent",
"type": "@n8n/n8n-nodes-langchain.informationExtractor",
"position": [
3008,
1072
],
"parameters": {
"text": "={{ $json.audit_text }}",
"options": {
"systemPromptTemplate": "Je bent een Senior Domein-Auditor voor de Nederlandse stukadoors-, plafond-, wandafwerking- en afbouwsector.\n\nJe krijgt een domein-audit tekst met signalen zoals:\n- domeinnaam\n- type site\n- webshop-indicatoren\n- Power Folders\n- URL-voorbeelden\n- technische content\n- projectcases\n- vakkennis-signalen\n\nJOUW DOEL:\nClassificeer het domein streng, consistent en bruikbaar voor ranking-doeleinden binnen de afbouwsector.\n\n====================\nSTAP 1 \u2014 SITE TYPE\n====================\n\nKies exact \u00e9\u00e9n `site_type` uit:\n- manufacturer\n- specialist\n- retailer\n- aggregator\n- unknown\n\nDefinities:\n\n1. manufacturer\nEigen merk, fabrikant of systeemleverancier met technische documentatie, datasheets, bestekteksten, systeemopbouwen, certificaten, downloads, BIM/CAD, productbladen of duidelijke verwerkingsinstructies.\n\n2. specialist\nUitvoerend specialist of nichebedrijf met aantoonbare vakkennis in stucwerk, afbouw, plafonds, wandafwerking, decoratieve afwerking, buitengevelafwerking, akoestiek of verwante uitvoering. Signalen: projectcases, eigen expertise, niche-diensten, verwerkingsadvies, probleemoplossing.\n\n3. retailer\nWebshop, bouwmarkt of productcatalogus-site met shop-first signalen zoals prijzen, categoriepagina\u2019s, filters, winkelwagen, brede productverkoop of bestelgerichte architectuur.\n\n4. aggregator\nVergelijker, bedrijvengids, offerteplatform, lead-gen platform, directory of verzamelsite zonder eigen diepe vakkennis of eigen technische autoriteit.\n\n5. unknown\nOnvoldoende bewijs, te weinig inhoud, onduidelijk, mixed signals zonder duidelijke hoofdidentiteit, of een slechte/lege scrape.\n\n====================\nSTAP 2 \u2014 ACTION\n====================\n\nKies exact \u00e9\u00e9n `action`:\n- boost\n- downrank\n- discard\n\nRegels:\n- manufacturer => altijd boost\n- specialist => meestal boost\n- retailer => altijd downrank\n- aggregator => meestal downrank, soms discard\n- unknown => meestal discard\n\nGebruik `discard` als de site:\n- ruis is\n- nauwelijks relevant is voor afbouw/stukadoors/plafonds/wanden\n- geen eigen vakkennis heeft\n- social/forum/UGC-achtig is\n- of te weinig betrouwbare signalen bevat\n\n====================\nSTAP 3 \u2014 STRENGTH\n====================\n\nKies een integer 1 t/m 5.\n\nHarde regels:\n- manufacturer => alleen 4 of 5\n- specialist => alleen 2, 3 of 4\n- retailer => alleen 1, 2 of 3\n- aggregator => alleen 1 of 2\n- unknown => alleen 1\n- discard => altijd 1\n- boost => nooit 1\n- strength moet logisch passen bij site_type en action\n\nInterpretatie:\n- 5 = uitzonderlijk sterke fabrikant / top technische autoriteit\n- 4 = sterke fabrikant of sterke nichespecialist\n- 3 = degelijke specialist of relevante retailer met enkele nuttige kenniszones\n- 2 = beperkte specialistische waarde / zwakke maar nog relevante site\n- 1 = discard, ruis, unknown of zeer lage waarde\n\n====================\nSTAP 4 \u2014 CONFIDENCE\n====================\n\nKies exact \u00e9\u00e9n `confidence`:\n- high\n- medium\n- low\n\nRegels:\n- high = duidelijke en consistente signalen, weinig twijfel\n- medium = redelijk duidelijk maar niet volledig sluitend\n- low = zwakke scrape, gemengde signalen of onvoldoende bewijs\n\nGebruik high alleen als de input echt duidelijke bewijsstukken bevat.\n\n===========================\nSTAP 5 \u2014 PROMISING PATHS\n===========================\n\nVul `promising_paths` als simpele string in.\n\nRegels:\n- Neem alleen paden op die letterlijk in de input staan.\n- Neem alleen paden op die duidelijke vakkennis/expertise bevatten.\n- Gebruik vooral promising_paths wanneer het domein een downrank krijgt, maar sommige kennisfolders toch waardevol zijn.\n- Als de hele site al een boost krijgt, vul meestal \"geen\" in.\n- Als er geen duidelijke uitzonderingspaden zijn, vul \"geen\" in.\n- Gebruik komma-gescheiden paden, bijvoorbeeld: \"/projecten, /downloads\"\n- Geef NOOIT een array.\n- Verzin nooit paden die niet in de input staan.\n- Neem geen shop-, cart-, checkout-, contact-, login-, account- of pure productlisting-paden op tenzij de input expliciet aantoont dat ze technische expertise bevatten.\n- Geef alleen exacte foldernamen of paden terug, geen uitleg in dit veld.\n\n===========================\nSTAP 6 \u2014 EVIDENCE SIGNALS\n===========================\n\nVul `evidence_signals` als simpele string in.\n\nRegels:\n- Geef 2 tot 5 concrete signalen uit de input.\n- Gebruik komma-gescheiden termen of korte frases.\n- Alleen signalen die echt in de input ondersteund worden.\n- Niet te algemeen formuleren.\n- Voorbeelden:\n - \"datasheets, technische documentatie, systeemopbouw, geen webshop\"\n - \"winkelwagen, productcategorieen, prijzen, brede catalogus\"\n - \"projectcases, niche-diensten, eigen foto's, verwerkingsadvies\"\n\n====================\nSTAP 7 \u2014 REASONING\n====================\n\nGeef `reasoning` in maximaal 2 korte zinnen in het Nederlands.\nNoem:\n1. waarom dit site_type en action logisch zijn\n2. waarom promising_paths wel of niet gekozen zijn\n\n====================\nBESLISLOGICA SAMENGEVAT\n====================\n\nGebruik deze logica strikt:\n- manufacturer + duidelijke technische autoriteit => boost + 4/5 + meestal high confidence\n- specialist + echte niche/vakkennis => boost + 2/3/4\n- retailer + webshopstructuur => downrank + 1/2/3\n- aggregator zonder eigen expertise => downrank of discard + 1/2\n- unknown of slechte scrape => discard + 1 + low confidence\n\n====================\nHARDE OUTPUTREGELS\n====================\n\n1. Geef UITSLUITEND raw JSON terug.\n2. GEEN markdown.\n3. GEEN backticks.\n4. Begin direct met { en eindig met }.\n5. `promising_paths` moet altijd een simpele string zijn.\n6. `evidence_signals` moet altijd een simpele string zijn.\n7. Gebruik exact de toegestane labels.\n8. Geen extra velden toevoegen."
},
"attributes": {
"attributes": [
{
"name": "site_type",
"required": true,
"description": "Kies exact \u00e9\u00e9n: manufacturer, specialist, retailer, aggregator, unknown."
},
{
"name": "action",
"required": true,
"description": "Kies exact \u00e9\u00e9n: boost, downrank, discard."
},
{
"name": "strength",
"type": "number",
"required": true,
"description": "Integer 1-5. manufacturer alleen 4-5, specialist alleen 2-4, retailer alleen 1-3, aggregator alleen 1-2, unknown alleen 1. discard altijd 1. boost nooit 1."
},
{
"name": "confidence",
"required": true,
"description": "Kies exact \u00e9\u00e9n: high, medium, low."
},
{
"name": "promising_paths",
"required": true,
"description": "Simpele string met komma-gescheiden paden die letterlijk in de input staan en duidelijke vakkennis bevatten. Geen array. Gebruik 'geen' als er geen uitzonderingspaden zijn."
},
{
"name": "evidence_signals",
"required": true,
"description": "Korte string met 2-5 concrete signalen uit de input die de classificatie onderbouwen, gescheiden door komma's. Bijvoorbeeld: 'datasheets, technische documentatie, geen webshop'."
},
{
"name": "reasoning",
"required": true,
"description": "Max 2 korte zinnen in het Nederlands. Leg uit waarom dit site_type/action is gekozen en waarom promising_paths wel of niet zijn geselecteerd."
}
]
}
},
"typeVersion": 1.2
},
{
"id": "2010ce1b-4e4d-49e0-b354-6f201fb61115",
"name": "Run Secondary SQL Query",
"type": "n8n-nodes-base.postgres",
"position": [
3536,
1328
],
"parameters": {
"query": "INSERT INTO goggle_audit_trail (\n goggle_id, \n target, \n site_type, \n full_log\n)\nVALUES (\n $1, $2, $3, $4\n);",
"options": {
"queryReplacement": "={{ [\n $('Batch Process Domains').item.json.goggle_id, \n $('Batch Process Domains').item.json.target,\n 'unknown',\n $json \n] }}"
},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "9b1075bf-f107-4136-b9e7-2cebfdcdcab0",
"name": "Execute JavaScript Code",
"type": "n8n-nodes-base.code",
"position": [
3824,
1456
],
"parameters": {
"jsCode": "const items = $input.all();\n\nreturn items.map(item => {\n const errorObj = item.json;\n\n // 1. DYNAMIC DATA RETRIEVAL\n // We look for the data in the current item (if passed through)\n // or via the pairedItem (the data from 'Process Domains in Batches')\n const goggleId = item.json.goggle_id || item.pairedItem?.json?.goggle_id;\n const targetUrl = item.json.target || item.json.url || item.pairedItem?.json?.target || \"unknown-url\";\n\n // 2. CLEAN DOMAIN EXTRACTION\n const domain = targetUrl\n .replace('https://', '')\n .replace('http://', '')\n .split('/')[0];\n\n return {\n json: {\n // 3. NO HARDCODED UUIDs\n goggle_id: goggleId,\n target: domain,\n site_type: 'error_timeout', \n full_log: { \n error: true, \n message: errorObj.message || \"Execution Error\", \n details: errorObj.errorDetails || errorObj || \"No further details\"\n }\n }\n };\n});"
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "b72282fe-f8cd-4c11-b18e-90f5bd030adf",
"name": "Log Error Postgres",
"type": "n8n-nodes-base.postgres",
"position": [
4048,
1456
],
"parameters": {
"query": "INSERT INTO goggle_audit_trail (\n goggle_id, \n target, \n site_type, \n full_log\n)\nVALUES (\n $1, $2, $3, $4\n);",
"options": {
"queryReplacement": "={{ [\n $json.goggle_id, \n $json.target, \n $json.site_type, \n $json.full_log \n] }}"
},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "d372989c-7a78-43ef-8074-572d2463f9c8",
"name": "Wait 5 Seconds",
"type": "n8n-nodes-base.wait",
"position": [
4272,
1360
],
"parameters": {},
"typeVersion": 1.1
},
{
"id": "3ac4d480-c0b0-44b0-bcaa-a727fe1ef199",
"name": "Merge",
"type": "n8n-nodes-base.merge",
"position": [
3888,
1280
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineAll"
},
"typeVersion": 3.2
},
{
"id": "b9e000d3-7fca-4540-b7fb-97328070fb7f",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
880,
768
],
"parameters": {
"width": 480,
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.
dataForSeoApifirecrawlApigithubApiopenAiApipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Business owners, industry specialists, and AI developers building domain-specific search experiences. It generates custom search filters to boost technical authorities and block lead-gen aggregators, requiring no advanced SEO expertise. Pre-configured for the Dutch plastering…
Source: https://n8n.io/workflows/15390/ — 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.
Automate Sales Meeting Prep With Ai & Apify Sent To Whatsapp. Uses gmail, googleCalendar, lmChatOpenAi, informationExtractor. Event-driven trigger; 61 nodes.
This n8n template builds a meeting assistant that compiles timely reminders of upcoming meetings filled with email history and recent LinkedIn activity of other people on the invite. This is then disc
A customized n8n workflow inspired by the Lead Generation Agent template. It automates B2B lead scraping via Apify, extracts contact emails with AI, sends cold emails via Gmail, and logs every interac
Selenium Ultimate Scraper Workflow. Uses html, lmChatOpenAi, httpRequest, limit. Webhook trigger; 63 nodes.
Selenium Ultimate Scraper Workflow. Uses html, lmChatOpenAi, httpRequest, limit. Webhook trigger; 63 nodes.