From 86c32e3f9c7173023af8867d5cb7bc48f9604716 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 27 Mar 2026 23:09:36 +0000 Subject: [PATCH] =?UTF-8?q?agent:=20fix-workflow-json=20=E2=80=94=20apply?= =?UTF-8?q?=209=20n8n=20spec=20fixes=20to=20lead-enrich-core.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../workflows/lead-enrich-core.json | 158 +++++++++++++----- 1 file changed, 119 insertions(+), 39 deletions(-) diff --git a/modules/A1-lead-enrich-pipeline/workflows/lead-enrich-core.json b/modules/A1-lead-enrich-pipeline/workflows/lead-enrich-core.json index a3537b9..fdaf2bd 100644 --- a/modules/A1-lead-enrich-pipeline/workflows/lead-enrich-core.json +++ b/modules/A1-lead-enrich-pipeline/workflows/lead-enrich-core.json @@ -6,11 +6,15 @@ "name": "Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, - "position": [0, 0], + "position": [ + 0, + 0 + ], + "webhookId": "lead-enrich", "parameters": { "path": "lead-enrich", "httpMethod": "POST", - "responseMode": "responseNode", + "responseMode": "lastNode", "options": {} } }, @@ -18,10 +22,14 @@ "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Input Mapper", "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [300, 0], + "typeVersion": 3.3, + "position": [ + 300, + 0 + ], "parameters": { "mode": "manual", + "duplicateItem": false, "assignments": { "assignments": [ { @@ -76,7 +84,10 @@ "name": "Clean & Validate", "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [600, 0], + "position": [ + 600, + 0 + ], "parameters": { "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;" @@ -87,13 +98,15 @@ "name": "Validation Router", "type": "n8n-nodes-base.if", "typeVersion": 2, - "position": [900, 0], + "position": [ + 900, + 0 + ], "parameters": { "conditions": { "options": { "caseSensitive": true, - "leftValue": "", - "rightValue": "" + "leftValue": "" }, "conditions": [ { @@ -115,10 +128,14 @@ "id": "a3b4c5d6-e7f8-9012-abcd-123456789012", "name": "Dead Letter", "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [1200, -200], + "typeVersion": 3.3, + "position": [ + 1200, + -200 + ], "parameters": { "mode": "manual", + "duplicateItem": false, "assignments": { "assignments": [ { @@ -155,10 +172,13 @@ "name": "Dead Letter Response", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, - "position": [1500, -200], + "position": [ + 1500, + -200 + ], "parameters": { "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": { "responseCode": 422 } @@ -169,13 +189,15 @@ "name": "Test Mode Check", "type": "n8n-nodes-base.if", "typeVersion": 2, - "position": [1200, 200], + "position": [ + 1200, + 200 + ], "parameters": { "conditions": { "options": { "caseSensitive": true, - "leftValue": "", - "rightValue": "" + "leftValue": "" }, "conditions": [ { @@ -197,10 +219,14 @@ "id": "c1d2e3f4-a5b6-7890-cdef-901234567890", "name": "Mock Enrichment", "type": "n8n-nodes-base.set", - "typeVersion": 3.4, - "position": [1500, 100], + "typeVersion": 3.3, + "position": [ + 1500, + 100 + ], "parameters": { "mode": "manual", + "duplicateItem": false, "assignments": { "assignments": [ { @@ -337,7 +363,9 @@ } ] }, - "options": {} + "options": { + "includeOtherFields": true + } } }, { @@ -345,7 +373,14 @@ "name": "Apollo HTTP Request", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [1500, 350], + "position": [ + 1500, + 350 + ], + "retryOnFail": true, + "maxTries": 3, + "waitBetweenTries": 5000, + "continueOnFail": true, "parameters": { "method": "POST", "url": "https://api.apollo.io/api/v1/people/match", @@ -371,13 +406,8 @@ "fullResponse": true } }, - "retry": { - "maxTries": 3, - "waitBetweenTries": 5000 - }, "timeout": 30000 - }, - "onError": "continueRegularOutput" + } } }, { @@ -385,7 +415,10 @@ "name": "Enrichment Mapper & Grader", "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [1800, 350], + "position": [ + 1800, + 350 + ], "parameters": { "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;" @@ -396,10 +429,13 @@ "name": "Success Response", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, - "position": [2100, 200], + "position": [ + 2100, + 200 + ], "parameters": { "respondWith": "json", - "responseBody": "={{ JSON.stringify($json) }}", + "responseBody": "={{ $json }}", "options": { "responseCode": 200 } @@ -410,69 +446,113 @@ "Webhook": { "main": [ [ - { "node": "Input Mapper", "type": "main", "index": 0 } + { + "node": "Input Mapper", + "type": "main", + "index": 0 + } ] ] }, "Input Mapper": { "main": [ [ - { "node": "Clean & Validate", "type": "main", "index": 0 } + { + "node": "Clean & Validate", + "type": "main", + "index": 0 + } ] ] }, "Clean & Validate": { "main": [ [ - { "node": "Validation Router", "type": "main", "index": 0 } + { + "node": "Validation Router", + "type": "main", + "index": 0 + } ] ] }, "Validation Router": { "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": { "main": [ [ - { "node": "Dead Letter Response", "type": "main", "index": 0 } + { + "node": "Dead Letter Response", + "type": "main", + "index": 0 + } ] ] }, "Test Mode Check": { "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": { "main": [ [ - { "node": "Success Response", "type": "main", "index": 0 } + { + "node": "Success Response", + "type": "main", + "index": 0 + } ] ] }, "Apollo HTTP Request": { "main": [ [ - { "node": "Enrichment Mapper & Grader", "type": "main", "index": 0 } + { + "node": "Enrichment Mapper & Grader", + "type": "main", + "index": 0 + } ] ] }, "Enrichment Mapper & Grader": { "main": [ [ - { "node": "Success Response", "type": "main", "index": 0 } + { + "node": "Success Response", + "type": "main", + "index": 0 + } ] ] }