This workflow follows the Error Trigger → 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 →
{
"updatedAt": "2025-12-25T01:31:56.743Z",
"createdAt": "2025-12-24T12:13:53.194Z",
"id": "iitZ5h0qrXbgxu2R",
"name": "03_ANALYST_Website_Scoring",
"active": false,
"isArchived": false,
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 1
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [
-2336,
64
],
"id": "2686e786-4c0b-48be-b5ee-ff3ca4076bf3",
"name": "Every 1 Minute",
"disabled": true
},
{
"parameters": {
"operation": "getAll",
"tableId": "leads",
"limit": 5,
"matchType": "allFilters",
"filters": {
"conditions": [
{
"keyName": "status",
"condition": "eq",
"keyValue": "raw"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
-2128,
64
],
"id": "20c755ec-bf8f-4edd-a89e-19ff206c211f",
"name": "Query Raw Leads",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "check-leads-exist",
"leftValue": "={{ $json.length }}",
"rightValue": "",
"operator": {
"type": "number",
"operation": "notEmpty"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-1936,
64
],
"id": "0cf44872-c9da-4ecf-adfd-e7917fe0ab47",
"name": "Leads Exist?"
},
{
"parameters": {
"operation": "update",
"tableId": "leads"
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
-1728,
-48
],
"id": "7453ddfc-6e5d-4c46-b3b0-1dd59b2cd516",
"name": "Mark as Enriching",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"url": "={{ 'https://' + $json.domain }}",
"options": {
"allowUnauthorizedCerts": true,
"redirect": {
"redirect": {
"maxRedirects": 3
}
},
"timeout": 10000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
-1536,
-48
],
"id": "1156d29a-47cb-41e3-8dd5-d6abd23dfea7",
"name": "Fetch Website"
},
{
"parameters": {
"jsCode": "// Get the HTML response and lead data\nconst html = $input.first().json || '';\nconst lead = $('Query Raw Leads').item.json;\n\n// Function to clean HTML\nfunction cleanHTML(html) {\n if (typeof html !== 'string') {\n return '';\n }\n \n // Remove script tags and content\n let cleaned = html.replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '');\n \n // Remove style tags and content\n cleaned = cleaned.replace(/<style\\b[^<]*(?:(?!<\\/style>)<[^<]*)*<\\/style>/gi, '');\n \n // Remove HTML tags\n cleaned = cleaned.replace(/<[^>]+>/g, ' ');\n \n // Decode common HTML entities\n cleaned = cleaned\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\");\n \n // Remove extra whitespace\n cleaned = cleaned.replace(/\\s+/g, ' ').trim();\n \n return cleaned;\n}\n\n// Function to extract metadata\nfunction extractMetadata(html) {\n const metadata = {\n title: '',\n description: '',\n keywords: []\n };\n \n if (typeof html !== 'string') {\n return metadata;\n }\n \n // Extract title\n const titleMatch = html.match(/<title[^>]*>([^<]+)<\\/title>/i);\n if (titleMatch) {\n metadata.title = titleMatch[1].trim();\n }\n \n // Extract meta description\n const descMatch = html.match(/<meta[^>]*name=[\"']description[\"'][^>]*content=[\"']([^\"']+)[\"']/i);\n if (descMatch) {\n metadata.description = descMatch[1].trim();\n }\n \n // Extract keywords\n const keywordsMatch = html.match(/<meta[^>]*name=[\"']keywords[\"'][^>]*content=[\"']([^\"']+)[\"']/i);\n if (keywordsMatch) {\n metadata.keywords = keywordsMatch[1].split(',').map(k => k.trim()).filter(Boolean);\n }\n \n return metadata;\n}\n\n// Process the HTML\nconst htmlContent = html.body || html.data || html.toString() || '';\nconst metadata = extractMetadata(htmlContent);\nconst cleanedText = cleanHTML(htmlContent);\n\n// Truncate content to avoid token limits (first 3000 chars)\nconst truncatedText = cleanedText.substring(0, 3000);\n\nconsole.log(`Processed ${lead.domain}: ${cleanedText.length} chars cleaned, ${truncatedText.length} used`);\n\nreturn [{\n json: {\n lead_id: lead.id,\n batch_id: lead.batch_id,\n user_id: lead.user_id,\n domain: lead.domain,\n name: lead.name || 'Unknown',\n \n // Metadata\n title: metadata.title || lead.name || 'No title',\n description: metadata.description || 'No description',\n keywords: metadata.keywords,\n \n // Content for analysis\n content: truncatedText,\n content_length: cleanedText.length,\n \n // Original lead data\n original_lead: lead\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1328,
-48
],
"id": "33a27e5d-6ee2-4e08-846b-b39685161507",
"name": "Extract & Clean Content"
},
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "anthropic-version",
"value": "2023-06-01"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"model\": \"claude-3-5-sonnet-20241022\",\n \"max_tokens\": 1024,\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"Analyze this business website and provide a fit score (0-100) based on the following criteria:\\n\\n**Business Information:**\\n- Name: {{ $json.name }}\\n- Domain: {{ $json.domain }}\\n- Website Title: {{ $json.title }}\\n- Description: {{ $json.description }}\\n\\n**Website Content (first 3000 chars):**\\n{{ $json.content }}\\n\\n**Scoring Criteria:**\\n1. Business Legitimacy (0-25): Is this a real, active business?\\n2. Website Quality (0-25): Professional design, updated content?\\n3. Service Relevance (0-25): Do they offer relevant services?\\n4. Contact Information (0-25): Easy to reach, multiple contact methods?\\n\\n**Response Format (JSON only, no markdown):**\\n{\\n \\\"fit_score\\\": <0-100>,\\n \\\"legitimacy_score\\\": <0-25>,\\n \\\"quality_score\\\": <0-25>,\\n \\\"relevance_score\\\": <0-25>,\\n \\\"contact_score\\\": <0-25>,\\n \\\"summary\\\": \\\"<2-3 sentence summary>\\\",\\n \\\"pros\\\": [\\\"<key strength 1>\\\", \\\"<key strength 2>\\\"],\\n \\\"cons\\\": [\\\"<potential issue 1>\\\", \\\"<potential issue 2>\\\"],\\n \\\"recommended_action\\\": \\\"contact|research_more|skip\\\"\\n}\"\n }\n ]\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
-768,
-32
],
"id": "a2afb20f-742c-4c04-8d71-68cb7151e85a",
"name": "Claude Analysis",
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"disabled": true
},
{
"parameters": {
"jsCode": "// Get the LLM response and lead data\nconst llmResponse = $input.first().json;\nconst leadData = $('Extract & Clean Content').item.json;\n\n// Extract content from Claude response\nlet analysisText = '';\nif (llmResponse.content && Array.isArray(llmResponse.content)) {\n analysisText = llmResponse.content\n .filter(block => block.type === 'text')\n .map(block => block.text)\n .join('\\n');\n} else if (typeof llmResponse === 'string') {\n analysisText = llmResponse;\n}\n\nconsole.log('LLM Response:', analysisText.substring(0, 200));\n\n// Parse JSON from response\nlet analysis;\ntry {\n // Remove markdown code blocks if present\n let cleanedText = analysisText.replace(/```json\\n?/g, '').replace(/```\\n?/g, '');\n \n // Find JSON object\n const jsonMatch = cleanedText.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n analysis = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('No JSON found in response');\n }\n} catch (e) {\n console.error('Failed to parse LLM response:', e.message);\n \n // Fallback: extract score from text\n const scoreMatch = analysisText.match(/fit[_\\s]*score[:\\s]*(\\d+)/i);\n const score = scoreMatch ? parseInt(scoreMatch[1]) : 50;\n \n analysis = {\n fit_score: score,\n summary: analysisText.substring(0, 500) || 'Analysis failed - using default score',\n pros: ['Unable to extract detailed analysis'],\n cons: ['LLM response parsing failed'],\n recommended_action: 'research_more',\n legitimacy_score: Math.round(score * 0.25),\n quality_score: Math.round(score * 0.25),\n relevance_score: Math.round(score * 0.25),\n contact_score: Math.round(score * 0.25)\n };\n}\n\n// Ensure score is valid (0-100)\nconst fitScore = Math.max(0, Math.min(100, parseInt(analysis.fit_score) || 50));\n\nconsole.log(`Scored ${leadData.domain}: ${fitScore}/100`);\n\nreturn [{\n json: {\n lead_id: leadData.lead_id,\n batch_id: leadData.batch_id,\n domain: leadData.domain,\n \n // Scoring\n fit_score: fitScore,\n legitimacy_score: analysis.legitimacy_score || null,\n quality_score: analysis.quality_score || null,\n relevance_score: analysis.relevance_score || null,\n contact_score: analysis.contact_score || null,\n \n // Analysis\n summary: (analysis.summary || 'No summary available').substring(0, 1000),\n pros: Array.isArray(analysis.pros) ? analysis.pros.slice(0, 5) : [],\n cons: Array.isArray(analysis.cons) ? analysis.cons.slice(0, 5) : [],\n recommended_action: analysis.recommended_action || 'research_more',\n \n // Metadata\n analyzed_at: new Date().toISOString(),\n llm_model: llmResponse.model || 'claude-3-5-sonnet',\n \n // Store full analysis\n full_analysis: analysis\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-560,
-32
],
"id": "18303080-9a11-466c-a41d-97b1c2bb94f1",
"name": "Parse Analysis"
},
{
"parameters": {
"operation": "update",
"tableId": "leads"
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
-368,
-32
],
"id": "c53a042d-b2e8-4b77-9484-611c1de814d1",
"name": "Update Lead (Scored)",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Get batch_id from processed lead\nconst batchId = $json.batch_id;\n\nif (!batchId) {\n console.log('No batch_id found, skipping batch check');\n return [];\n}\n\nconsole.log(`Checking completion for batch: ${batchId}`);\n\nreturn [{\n json: {\n batch_id: batchId\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-160,
-32
],
"id": "38d827ed-4be3-4d5d-863d-4386b92bc54e",
"name": "Check Batch"
},
{
"parameters": {
"operation": "getAll",
"tableId": "leads",
"returnAll": true,
"matchType": "allFilters",
"filters": {
"conditions": [
{
"keyName": "batch_id",
"keyValue": "={{ $json.batch_id }}"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
32,
-32
],
"id": "1f0a9838-63bc-4bed-a12c-2712fc2aad45",
"name": "Query Batch Leads",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Get all leads in batch\nconst allLeads = $input.all().map(item => item.json);\nconst batchId = $('Check Batch').item.json.batch_id;\n\nif (allLeads.length === 0) {\n console.log('No leads found in batch');\n return [{ json: { complete: false, skip: true } }];\n}\n\n// Count leads by status\nconst statusCounts = {\n raw: 0,\n enriching: 0,\n scored: 0,\n scraping_failed: 0\n};\n\nallLeads.forEach(lead => {\n const status = lead.status || 'unknown';\n if (statusCounts.hasOwnProperty(status)) {\n statusCounts[status]++;\n }\n});\n\n// Batch is complete if no raw or enriching leads remain\nconst isComplete = (statusCounts.raw + statusCounts.enriching) === 0;\n\n// Calculate stats\nconst totalLeads = allLeads.length;\nconst scoredLeads = statusCounts.scored;\n\n// Calculate average score (only for scored leads with valid scores)\nconst scoredLeadsWithScores = allLeads.filter(\n l => l.status === 'scored' && l.fit_score !== null && !isNaN(l.fit_score)\n);\n\nconst avgScore = scoredLeadsWithScores.length > 0\n ? Math.round(\n scoredLeadsWithScores.reduce((sum, l) => sum + parseFloat(l.fit_score), 0) / \n scoredLeadsWithScores.length\n )\n : 0;\n\nconsole.log(`Batch ${batchId}: ${isComplete ? 'COMPLETE' : 'PENDING'}`);\nconsole.log(`Status: Raw=${statusCounts.raw}, Enriching=${statusCounts.enriching}, Scored=${statusCounts.scored}, Failed=${statusCounts.scraping_failed}`);\nconsole.log(`Average score: ${avgScore}`);\n\nreturn [{\n json: {\n batch_id: batchId,\n complete: isComplete,\n total_leads: totalLeads,\n scored_leads: scoredLeads,\n failed_leads: statusCounts.scraping_failed,\n pending_leads: statusCounts.raw + statusCounts.enriching,\n average_score: avgScore,\n status_breakdown: statusCounts\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
-32
],
"id": "5384364f-e88e-4ea7-847c-64203923496a",
"name": "Calculate Completion"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "batch-complete-check",
"leftValue": "={{ $json.complete }}",
"rightValue": "true",
"operator": {
"type": "boolean",
"operation": "equals",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
448,
-32
],
"id": "fa695184-8ade-4f63-902e-a5c74b1dde54",
"name": "Batch Complete?"
},
{
"parameters": {
"operation": "update",
"tableId": "batches"
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
480,
-192
],
"id": "29cd57ff-b91b-49e4-a769-04c011735877",
"name": "Update Batch (Complete)",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.errorTrigger",
"typeVersion": 1,
"position": [
-2336,
304
],
"id": "8ff643ac-adb6-4f40-83c2-9b55a5004935",
"name": "Error Trigger"
},
{
"parameters": {
"jsCode": "// Handle scraping errors\nconst error = $input.first().json.error || {};\nconst errorNode = error.node || 'unknown';\nconst errorMessage = error.message || 'Unknown error';\n\n// Try to get lead_id from context\nlet leadId = null;\nlet batchId = null;\n\ntry {\n const queryResult = $('Query Raw Leads').item;\n if (queryResult) {\n leadId = queryResult.json.id;\n batchId = queryResult.json.batch_id;\n }\n} catch (e) {\n console.warn('Could not retrieve lead_id from context');\n}\n\nconsole.error('Workflow error:', {\n node: errorNode,\n message: errorMessage,\n lead_id: leadId\n});\n\nreturn [{\n json: {\n lead_id: leadId,\n batch_id: batchId,\n error_node: errorNode,\n error_message: errorMessage.substring(0, 500),\n error_stack: error.stack ? error.stack.substring(0, 1000) : null,\n timestamp: new Date().toISOString()\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-2128,
304
],
"id": "ac897795-ed5b-4b5f-944c-49e6db00de77",
"name": "Log Error"
},
{
"parameters": {
"operation": "update",
"tableId": "leads"
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
-1936,
304
],
"id": "02ab7e30-15a7-4dde-bdd4-cdd1d18949e2",
"name": "Mark Lead Failed",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"modelId": {
"__rl": true,
"value": "models/gemini-2.5-flash",
"mode": "list",
"cachedResultName": "models/gemini-2.5-flash"
},
"messages": {
"values": [
{
"content": "=Analyze this business website and provide a fit score (0-100) based on the following criteria:\\n\\n**Business Information:**\\n- Name: {{ $json.name }}\\n- Domain: {{ $json.domain }}\\n- Website Title: {{ $json.title }}\\n- Description: {{ $json.description }}\\n\\n**Website Content (first 3000 chars):**\\n{{ $json.content }}\\n\\n**Scoring Criteria:**\\n1. Business Legitimacy (0-25): Is this a real, active business?\\n2. Website Quality (0-25): Professional design, updated content?\\n3. Service Relevance (0-25): Do they offer relevant services?\\n4. Contact Information (0-25): Easy to reach, multiple contact methods?\\n\\n**Response Format (JSON only, no markdown):**\\n{\\n \\\"fit_score\\\": <0-100>,\\n \\\"legitimacy_score\\\": <0-25>,\\n \\\"quality_score\\\": <0-25>,\\n \\\"relevance_score\\\": <0-25>,\\n \\\"contact_score\\\": <0-25>,\\n \\\"summary\\\": \\\"<2-3 sentence summary>\\\",\\n \\\"pros\\\": [\\\"<key strength 1>\\\", \\\"<key strength 2>\\\"],\\n \\\"cons\\\": [\\\"<potential issue 1>\\\", \\\"<potential issue 2>\\\"],\\n \\\"recommended_action\\\": \\\"contact|research_more|skip\\\"\\n"
}
]
},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1,
"position": [
-1152,
-48
],
"id": "636f6354-deb8-4a08-b9ee-6d691cad5274",
"name": "Message a model",
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Every 1 Minute": {
"main": [
[
{
"node": "Query Raw Leads",
"type": "main",
"index": 0
}
]
]
},
"Query Raw Leads": {
"main": [
[
{
"node": "Leads Exist?",
"type": "main",
"index": 0
}
]
]
},
"Leads Exist?": {
"main": [
[
{
"node": "Mark as Enriching",
"type": "main",
"index": 0
}
]
]
},
"Mark as Enriching": {
"main": [
[
{
"node": "Fetch Website",
"type": "main",
"index": 0
}
]
]
},
"Fetch Website": {
"main": [
[
{
"node": "Extract & Clean Content",
"type": "main",
"index": 0
}
]
]
},
"Extract & Clean Content": {
"main": [
[
{
"node": "Message a model",
"type": "main",
"index": 0
}
]
]
},
"Claude Analysis": {
"main": [
[
{
"node": "Parse Analysis",
"type": "main",
"index": 0
}
]
]
},
"Parse Analysis": {
"main": [
[
{
"node": "Update Lead (Scored)",
"type": "main",
"index": 0
}
]
]
},
"Update Lead (Scored)": {
"main": [
[
{
"node": "Check Batch",
"type": "main",
"index": 0
}
]
]
},
"Check Batch": {
"main": [
[
{
"node": "Query Batch Leads",
"type": "main",
"index": 0
}
]
]
},
"Query Batch Leads": {
"main": [
[
{
"node": "Calculate Completion",
"type": "main",
"index": 0
}
]
]
},
"Calculate Completion": {
"main": [
[
{
"node": "Batch Complete?",
"type": "main",
"index": 0
}
]
]
},
"Batch Complete?": {
"main": [
[
{
"node": "Update Batch (Complete)",
"type": "main",
"index": 0
}
]
]
},
"Error Trigger": {
"main": [
[
{
"node": "Log Error",
"type": "main",
"index": 0
}
]
]
},
"Log Error": {
"main": [
[
{
"node": "Mark Lead Failed",
"type": "main",
"index": 0
}
]
]
},
"Message a model": {
"main": [
[
{
"node": "Claude Analysis",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "282f42ca-8ae3-4622-9f5c-b9b9eb8e55c5",
"activeVersionId": null,
"triggerCount": 0,
"shared": [
{
"updatedAt": "2025-12-24T12:13:53.216Z",
"createdAt": "2025-12-24T12:13:53.216Z",
"role": "workflow:owner",
"workflowId": "iitZ5h0qrXbgxu2R",
"projectId": "HHopAZ4lOFgjhBzT"
}
],
"activeVersion": null,
"tags": []
}
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.
googlePalmApihttpHeaderAuthsupabaseApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
03_ANALYST_Website_Scoring. Uses supabase, httpRequest, errorTrigger, googleGemini. Scheduled trigger; 18 nodes.
Source: https://github.com/abde0112/n8n_bkv2/blob/main/03_analyst_website_scoring-iitZ5h0qrXbgxu2R.json — 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 runs every minute to pull accounts from Salesforce, enrich each customer with order, payment, support, and marketing data via HTTP APIs, analyze the combined profile with Google Gemini,
Gemini Mockup Generator v1.1. Uses httpRequest, googleGemini, microsoftOutlook. Scheduled trigger; 98 nodes.
AI Institutional Stock Valuation Engine with Risk Scoring & Scenario Targets
Overview This is a production-grade, fully automated stock analysis system built entirely in n8n. It combines institutional-level financial analysis, dual AI model consensus, and a self-improving back
This workflow is a complete outbound automation system that discovers local businesses, extracts contact emails, generates personalized cold emails using AI, and runs a multi-step follow-up sequence —