agent: fix-workflow-json — apply 9 n8n spec fixes to lead-enrich-core.json
Set nodes: typeVersion 3.4 → 3.3, add duplicateItem:false. Mock Enrichment: add includeOtherFields:true to preserve upstream lead data. IF nodes: remove spurious rightValue from options block. Apollo HTTP Request: move retry config (retryOnFail, maxTries, waitBetweenTries) to node root, add continueOnFail:true, remove from parameters.options. Success Response: JSON.stringify($json) → $json. Dead Letter Response: JSON.stringify object literal → plain expression object. Settings executionOrder:v1 and webhookId were already correct. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,15 @@
|
|||||||
"name": "Webhook",
|
"name": "Webhook",
|
||||||
"type": "n8n-nodes-base.webhook",
|
"type": "n8n-nodes-base.webhook",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [0, 0],
|
"position": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"webhookId": "lead-enrich",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"path": "lead-enrich",
|
"path": "lead-enrich",
|
||||||
"httpMethod": "POST",
|
"httpMethod": "POST",
|
||||||
"responseMode": "responseNode",
|
"responseMode": "lastNode",
|
||||||
"options": {}
|
"options": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -18,10 +22,14 @@
|
|||||||
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
|
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
|
||||||
"name": "Input Mapper",
|
"name": "Input Mapper",
|
||||||
"type": "n8n-nodes-base.set",
|
"type": "n8n-nodes-base.set",
|
||||||
"typeVersion": 3.4,
|
"typeVersion": 3.3,
|
||||||
"position": [300, 0],
|
"position": [
|
||||||
|
300,
|
||||||
|
0
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "manual",
|
"mode": "manual",
|
||||||
|
"duplicateItem": false,
|
||||||
"assignments": {
|
"assignments": {
|
||||||
"assignments": [
|
"assignments": [
|
||||||
{
|
{
|
||||||
@@ -76,7 +84,10 @@
|
|||||||
"name": "Clean & Validate",
|
"name": "Clean & Validate",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [600, 0],
|
"position": [
|
||||||
|
600,
|
||||||
|
0
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForAllItems",
|
"mode": "runOnceForAllItems",
|
||||||
"jsCode": "const items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const lead = item.json.lead;\n\n // Trim all string fields\n for (const key of Object.keys(lead)) {\n if (typeof lead[key] === 'string') {\n lead[key] = lead[key].trim();\n }\n }\n\n // Lowercase email\n lead.email = (lead.email || '').toLowerCase();\n\n // Validate email format\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!lead.email || !emailRegex.test(lead.email)) {\n item.json._validation_error = true;\n item.json._error_reason = !lead.email\n ? 'Email is missing or empty'\n : `Invalid email format: ${lead.email}`;\n } else {\n item.json._validation_error = false;\n }\n\n // Ensure submitted_at exists\n if (!lead.submitted_at) {\n lead.submitted_at = new Date().toISOString();\n }\n\n results.push(item);\n}\n\nreturn results;"
|
"jsCode": "const items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const lead = item.json.lead;\n\n // Trim all string fields\n for (const key of Object.keys(lead)) {\n if (typeof lead[key] === 'string') {\n lead[key] = lead[key].trim();\n }\n }\n\n // Lowercase email\n lead.email = (lead.email || '').toLowerCase();\n\n // Validate email format\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!lead.email || !emailRegex.test(lead.email)) {\n item.json._validation_error = true;\n item.json._error_reason = !lead.email\n ? 'Email is missing or empty'\n : `Invalid email format: ${lead.email}`;\n } else {\n item.json._validation_error = false;\n }\n\n // Ensure submitted_at exists\n if (!lead.submitted_at) {\n lead.submitted_at = new Date().toISOString();\n }\n\n results.push(item);\n}\n\nreturn results;"
|
||||||
@@ -87,13 +98,15 @@
|
|||||||
"name": "Validation Router",
|
"name": "Validation Router",
|
||||||
"type": "n8n-nodes-base.if",
|
"type": "n8n-nodes-base.if",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [900, 0],
|
"position": [
|
||||||
|
900,
|
||||||
|
0
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"options": {
|
"options": {
|
||||||
"caseSensitive": true,
|
"caseSensitive": true,
|
||||||
"leftValue": "",
|
"leftValue": ""
|
||||||
"rightValue": ""
|
|
||||||
},
|
},
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
@@ -115,10 +128,14 @@
|
|||||||
"id": "a3b4c5d6-e7f8-9012-abcd-123456789012",
|
"id": "a3b4c5d6-e7f8-9012-abcd-123456789012",
|
||||||
"name": "Dead Letter",
|
"name": "Dead Letter",
|
||||||
"type": "n8n-nodes-base.set",
|
"type": "n8n-nodes-base.set",
|
||||||
"typeVersion": 3.4,
|
"typeVersion": 3.3,
|
||||||
"position": [1200, -200],
|
"position": [
|
||||||
|
1200,
|
||||||
|
-200
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "manual",
|
"mode": "manual",
|
||||||
|
"duplicateItem": false,
|
||||||
"assignments": {
|
"assignments": {
|
||||||
"assignments": [
|
"assignments": [
|
||||||
{
|
{
|
||||||
@@ -155,10 +172,13 @@
|
|||||||
"name": "Dead Letter Response",
|
"name": "Dead Letter Response",
|
||||||
"type": "n8n-nodes-base.respondToWebhook",
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
"typeVersion": 1.1,
|
"typeVersion": 1.1,
|
||||||
"position": [1500, -200],
|
"position": [
|
||||||
|
1500,
|
||||||
|
-200
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"respondWith": "json",
|
"respondWith": "json",
|
||||||
"responseBody": "={{ JSON.stringify({ error: true, error_reason: $json.error_reason, lead: $json.lead, timestamp: $json.timestamp }) }}",
|
"responseBody": "={{ { error: true, error_reason: $json.error_reason, lead: $json.lead, timestamp: $json.timestamp } }}",
|
||||||
"options": {
|
"options": {
|
||||||
"responseCode": 422
|
"responseCode": 422
|
||||||
}
|
}
|
||||||
@@ -169,13 +189,15 @@
|
|||||||
"name": "Test Mode Check",
|
"name": "Test Mode Check",
|
||||||
"type": "n8n-nodes-base.if",
|
"type": "n8n-nodes-base.if",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [1200, 200],
|
"position": [
|
||||||
|
1200,
|
||||||
|
200
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"conditions": {
|
"conditions": {
|
||||||
"options": {
|
"options": {
|
||||||
"caseSensitive": true,
|
"caseSensitive": true,
|
||||||
"leftValue": "",
|
"leftValue": ""
|
||||||
"rightValue": ""
|
|
||||||
},
|
},
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
@@ -197,10 +219,14 @@
|
|||||||
"id": "c1d2e3f4-a5b6-7890-cdef-901234567890",
|
"id": "c1d2e3f4-a5b6-7890-cdef-901234567890",
|
||||||
"name": "Mock Enrichment",
|
"name": "Mock Enrichment",
|
||||||
"type": "n8n-nodes-base.set",
|
"type": "n8n-nodes-base.set",
|
||||||
"typeVersion": 3.4,
|
"typeVersion": 3.3,
|
||||||
"position": [1500, 100],
|
"position": [
|
||||||
|
1500,
|
||||||
|
100
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "manual",
|
"mode": "manual",
|
||||||
|
"duplicateItem": false,
|
||||||
"assignments": {
|
"assignments": {
|
||||||
"assignments": [
|
"assignments": [
|
||||||
{
|
{
|
||||||
@@ -337,7 +363,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"options": {}
|
"options": {
|
||||||
|
"includeOtherFields": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -345,7 +373,14 @@
|
|||||||
"name": "Apollo HTTP Request",
|
"name": "Apollo HTTP Request",
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
"typeVersion": 4.2,
|
"typeVersion": 4.2,
|
||||||
"position": [1500, 350],
|
"position": [
|
||||||
|
1500,
|
||||||
|
350
|
||||||
|
],
|
||||||
|
"retryOnFail": true,
|
||||||
|
"maxTries": 3,
|
||||||
|
"waitBetweenTries": 5000,
|
||||||
|
"continueOnFail": true,
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"url": "https://api.apollo.io/api/v1/people/match",
|
"url": "https://api.apollo.io/api/v1/people/match",
|
||||||
@@ -371,13 +406,8 @@
|
|||||||
"fullResponse": true
|
"fullResponse": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"retry": {
|
|
||||||
"maxTries": 3,
|
|
||||||
"waitBetweenTries": 5000
|
|
||||||
},
|
|
||||||
"timeout": 30000
|
"timeout": 30000
|
||||||
},
|
}
|
||||||
"onError": "continueRegularOutput"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -385,7 +415,10 @@
|
|||||||
"name": "Enrichment Mapper & Grader",
|
"name": "Enrichment Mapper & Grader",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [1800, 350],
|
"position": [
|
||||||
|
1800,
|
||||||
|
350
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mode": "runOnceForAllItems",
|
"mode": "runOnceForAllItems",
|
||||||
"jsCode": "const items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const response = item.json;\n const lead = response.lead || item.json.lead;\n\n // Initialise enrichment structure\n const enrichment = {\n provider: 'apollo',\n person: {\n title: null,\n linkedin_url: null,\n city: null,\n state: null,\n country: null,\n seniority: null\n },\n organisation: {\n name: null,\n domain: null,\n industry: null,\n estimated_employees: null,\n annual_revenue: null,\n linkedin_url: null,\n website_url: null\n },\n enrichment_error: false,\n credits_consumed: 0,\n rate_limit_remaining: null,\n credits_low: false\n };\n\n // Check for HTTP errors via the response metadata\n const statusCode = response.statusCode || response.$response?.statusCode || 200;\n\n // Extract rate limit headers if available\n const headers = response.headers || response.$response?.headers || {};\n const rateRemaining = headers['x-minute-requests-left'];\n if (rateRemaining !== undefined) {\n enrichment.rate_limit_remaining = parseInt(rateRemaining, 10);\n enrichment.credits_low = enrichment.rate_limit_remaining < 50;\n }\n\n // Handle response based on status\n if (statusCode === 429) {\n enrichment.enrichment_error = true;\n } else if (statusCode >= 400) {\n enrichment.enrichment_error = true;\n } else {\n // Look for person data in the response body\n const body = response.body || response;\n const person = body.person || null;\n\n if (person && person !== null) {\n const org = person.organization || {};\n\n enrichment.person.title = person.title || null;\n enrichment.person.linkedin_url = person.linkedin_url || null;\n enrichment.person.city = person.city || null;\n enrichment.person.state = person.state || null;\n enrichment.person.country = person.country || null;\n enrichment.person.seniority = person.seniority || null;\n\n enrichment.organisation.name = org.name || null;\n enrichment.organisation.domain = org.primary_domain || null;\n enrichment.organisation.industry = org.industry || null;\n enrichment.organisation.estimated_employees = org.estimated_num_employees || null;\n enrichment.organisation.annual_revenue = org.annual_revenue || null;\n enrichment.organisation.linkedin_url = org.linkedin_url || null;\n enrichment.organisation.website_url = org.website_url || null;\n\n enrichment.credits_consumed = body.credits_consumed || 1;\n }\n // else: person is null — no match, all fields stay null, no error, no credits\n }\n\n // Grade the profile\n const hasCompany = enrichment.organisation.name !== null;\n const hasTitle = enrichment.person.title !== null;\n const hasEmployees = enrichment.organisation.estimated_employees !== null;\n\n let profileGrade;\n if (enrichment.enrichment_error || !hasCompany) {\n profileGrade = 'Limited';\n } else if (hasCompany && hasTitle && hasEmployees) {\n profileGrade = 'Full Profile';\n } else {\n profileGrade = 'Partial Profile';\n }\n\n // Count populated fields\n const personFields = Object.values(enrichment.person).filter(v => v !== null).length;\n const orgFields = Object.values(enrichment.organisation).filter(v => v !== null).length;\n\n const grading = {\n profile_grade: profileGrade,\n fields_populated: personFields + orgFields,\n graded_at: new Date().toISOString()\n };\n\n results.push({\n json: {\n lead,\n enrichment,\n grading\n }\n });\n}\n\nreturn results;"
|
"jsCode": "const items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const response = item.json;\n const lead = response.lead || item.json.lead;\n\n // Initialise enrichment structure\n const enrichment = {\n provider: 'apollo',\n person: {\n title: null,\n linkedin_url: null,\n city: null,\n state: null,\n country: null,\n seniority: null\n },\n organisation: {\n name: null,\n domain: null,\n industry: null,\n estimated_employees: null,\n annual_revenue: null,\n linkedin_url: null,\n website_url: null\n },\n enrichment_error: false,\n credits_consumed: 0,\n rate_limit_remaining: null,\n credits_low: false\n };\n\n // Check for HTTP errors via the response metadata\n const statusCode = response.statusCode || response.$response?.statusCode || 200;\n\n // Extract rate limit headers if available\n const headers = response.headers || response.$response?.headers || {};\n const rateRemaining = headers['x-minute-requests-left'];\n if (rateRemaining !== undefined) {\n enrichment.rate_limit_remaining = parseInt(rateRemaining, 10);\n enrichment.credits_low = enrichment.rate_limit_remaining < 50;\n }\n\n // Handle response based on status\n if (statusCode === 429) {\n enrichment.enrichment_error = true;\n } else if (statusCode >= 400) {\n enrichment.enrichment_error = true;\n } else {\n // Look for person data in the response body\n const body = response.body || response;\n const person = body.person || null;\n\n if (person && person !== null) {\n const org = person.organization || {};\n\n enrichment.person.title = person.title || null;\n enrichment.person.linkedin_url = person.linkedin_url || null;\n enrichment.person.city = person.city || null;\n enrichment.person.state = person.state || null;\n enrichment.person.country = person.country || null;\n enrichment.person.seniority = person.seniority || null;\n\n enrichment.organisation.name = org.name || null;\n enrichment.organisation.domain = org.primary_domain || null;\n enrichment.organisation.industry = org.industry || null;\n enrichment.organisation.estimated_employees = org.estimated_num_employees || null;\n enrichment.organisation.annual_revenue = org.annual_revenue || null;\n enrichment.organisation.linkedin_url = org.linkedin_url || null;\n enrichment.organisation.website_url = org.website_url || null;\n\n enrichment.credits_consumed = body.credits_consumed || 1;\n }\n // else: person is null — no match, all fields stay null, no error, no credits\n }\n\n // Grade the profile\n const hasCompany = enrichment.organisation.name !== null;\n const hasTitle = enrichment.person.title !== null;\n const hasEmployees = enrichment.organisation.estimated_employees !== null;\n\n let profileGrade;\n if (enrichment.enrichment_error || !hasCompany) {\n profileGrade = 'Limited';\n } else if (hasCompany && hasTitle && hasEmployees) {\n profileGrade = 'Full Profile';\n } else {\n profileGrade = 'Partial Profile';\n }\n\n // Count populated fields\n const personFields = Object.values(enrichment.person).filter(v => v !== null).length;\n const orgFields = Object.values(enrichment.organisation).filter(v => v !== null).length;\n\n const grading = {\n profile_grade: profileGrade,\n fields_populated: personFields + orgFields,\n graded_at: new Date().toISOString()\n };\n\n results.push({\n json: {\n lead,\n enrichment,\n grading\n }\n });\n}\n\nreturn results;"
|
||||||
@@ -396,10 +429,13 @@
|
|||||||
"name": "Success Response",
|
"name": "Success Response",
|
||||||
"type": "n8n-nodes-base.respondToWebhook",
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
"typeVersion": 1.1,
|
"typeVersion": 1.1,
|
||||||
"position": [2100, 200],
|
"position": [
|
||||||
|
2100,
|
||||||
|
200
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"respondWith": "json",
|
"respondWith": "json",
|
||||||
"responseBody": "={{ JSON.stringify($json) }}",
|
"responseBody": "={{ $json }}",
|
||||||
"options": {
|
"options": {
|
||||||
"responseCode": 200
|
"responseCode": 200
|
||||||
}
|
}
|
||||||
@@ -410,69 +446,113 @@
|
|||||||
"Webhook": {
|
"Webhook": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{ "node": "Input Mapper", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Input Mapper",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Input Mapper": {
|
"Input Mapper": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{ "node": "Clean & Validate", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Clean & Validate",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Clean & Validate": {
|
"Clean & Validate": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{ "node": "Validation Router", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Validation Router",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Validation Router": {
|
"Validation Router": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{ "node": "Dead Letter", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Dead Letter",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ "node": "Test Mode Check", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Test Mode Check",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Dead Letter": {
|
"Dead Letter": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{ "node": "Dead Letter Response", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Dead Letter Response",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Test Mode Check": {
|
"Test Mode Check": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{ "node": "Mock Enrichment", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Mock Enrichment",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ "node": "Apollo HTTP Request", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Apollo HTTP Request",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Mock Enrichment": {
|
"Mock Enrichment": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{ "node": "Success Response", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Success Response",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Apollo HTTP Request": {
|
"Apollo HTTP Request": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{ "node": "Enrichment Mapper & Grader", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Enrichment Mapper & Grader",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Enrichment Mapper & Grader": {
|
"Enrichment Mapper & Grader": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
{ "node": "Success Response", "type": "main", "index": 0 }
|
{
|
||||||
|
"node": "Success Response",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user