From d75d008b22f8f175e05005dcf5ad7fc91ffee107 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 27 Mar 2026 22:53:57 +0000 Subject: [PATCH] =?UTF-8?q?agent:=20task-2=20=E2=80=94=20build=20Workflow?= =?UTF-8?q?=201=20Lead=20Enrich=20Core=20n8n=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 11-node workflow: Webhook → Input Mapper → Clean & Validate → Validation Router branches to Dead Letter (422) or Test Mode Check → Mock Enrichment / Apollo HTTP Request → Enrichment Mapper & Grader → Success Response (200). Both live and mock paths converge on Success Response. JSON validated parseable and structurally correct. Co-Authored-By: Claude Sonnet 4.6 --- .../workflows/lead-enrich-core.json | 484 ++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 modules/A1-lead-enrich-pipeline/workflows/lead-enrich-core.json diff --git a/modules/A1-lead-enrich-pipeline/workflows/lead-enrich-core.json b/modules/A1-lead-enrich-pipeline/workflows/lead-enrich-core.json new file mode 100644 index 0000000..a3537b9 --- /dev/null +++ b/modules/A1-lead-enrich-pipeline/workflows/lead-enrich-core.json @@ -0,0 +1,484 @@ +{ + "name": "Lead Enrich Core", + "nodes": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 2, + "position": [0, 0], + "parameters": { + "path": "lead-enrich", + "httpMethod": "POST", + "responseMode": "responseNode", + "options": {} + } + }, + { + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "name": "Input Mapper", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [300, 0], + "parameters": { + "mode": "manual", + "assignments": { + "assignments": [ + { + "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", + "name": "lead.email", + "value": "={{ $json.body?.email || $json.email || '' }}", + "type": "string" + }, + { + "id": "d4e5f6a7-b8c9-0123-defa-234567890123", + "name": "lead.first_name", + "value": "={{ $json.body?.first_name || $json.first_name || '' }}", + "type": "string" + }, + { + "id": "e5f6a7b8-c9d0-1234-efab-345678901234", + "name": "lead.last_name", + "value": "={{ $json.body?.last_name || $json.last_name || '' }}", + "type": "string" + }, + { + "id": "f6a7b8c9-d0e1-2345-fabc-456789012345", + "name": "lead.company_name", + "value": "={{ $json.body?.company_name || $json.company_name || '' }}", + "type": "string" + }, + { + "id": "a7b8c9d0-e1f2-3456-abcd-567890123456", + "name": "lead.source", + "value": "={{ $json.body?.source || $json.source || 'webhook' }}", + "type": "string" + }, + { + "id": "b8c9d0e1-f2a3-4567-bcde-678901234567", + "name": "lead.submitted_at", + "value": "={{ $now.toISO() }}", + "type": "string" + }, + { + "id": "c9d0e1f2-a3b4-5678-cdef-789012345678", + "name": "lead.raw_payload", + "value": "={{ $json }}", + "type": "object" + } + ] + }, + "options": {} + } + }, + { + "id": "d0e1f2a3-b4c5-6789-defa-890123456789", + "name": "Clean & Validate", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "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;" + } + }, + { + "id": "e1f2a3b4-c5d6-7890-efab-901234567890", + "name": "Validation Router", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [900, 0], + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "rightValue": "" + }, + "conditions": [ + { + "id": "f2a3b4c5-d6e7-8901-fabc-012345678901", + "leftValue": "={{ $json._validation_error }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + } + }, + { + "id": "a3b4c5d6-e7f8-9012-abcd-123456789012", + "name": "Dead Letter", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [1200, -200], + "parameters": { + "mode": "manual", + "assignments": { + "assignments": [ + { + "id": "b4c5d6e7-f8a9-0123-bcde-234567890123", + "name": "error", + "value": true, + "type": "boolean" + }, + { + "id": "c5d6e7f8-a9b0-1234-cdef-345678901234", + "name": "error_reason", + "value": "={{ $json._error_reason }}", + "type": "string" + }, + { + "id": "d6e7f8a9-b0c1-2345-defa-456789012345", + "name": "lead", + "value": "={{ $json.lead }}", + "type": "object" + }, + { + "id": "e7f8a9b0-c1d2-3456-efab-567890123456", + "name": "timestamp", + "value": "={{ $now.toISO() }}", + "type": "string" + } + ] + }, + "options": {} + } + }, + { + "id": "f8a9b0c1-d2e3-4567-fabc-678901234567", + "name": "Dead Letter Response", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [1500, -200], + "parameters": { + "respondWith": "json", + "responseBody": "={{ JSON.stringify({ error: true, error_reason: $json.error_reason, lead: $json.lead, timestamp: $json.timestamp }) }}", + "options": { + "responseCode": 422 + } + } + }, + { + "id": "a9b0c1d2-e3f4-5678-abcd-789012345678", + "name": "Test Mode Check", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [1200, 200], + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "rightValue": "" + }, + "conditions": [ + { + "id": "b0c1d2e3-f4a5-6789-bcde-890123456789", + "leftValue": "={{ $env.TEST_MODE }}", + "rightValue": "true", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + } + }, + { + "id": "c1d2e3f4-a5b6-7890-cdef-901234567890", + "name": "Mock Enrichment", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [1500, 100], + "parameters": { + "mode": "manual", + "assignments": { + "assignments": [ + { + "id": "d2e3f4a5-b6c7-8901-defa-012345678901", + "name": "lead", + "value": "={{ $json.lead }}", + "type": "object" + }, + { + "id": "e3f4a5b6-c7d8-9012-efab-123456789012", + "name": "enrichment.provider", + "value": "mock", + "type": "string" + }, + { + "id": "f4a5b6c7-d8e9-0123-fabc-234567890123", + "name": "enrichment.person.title", + "value": "Head of Marketing", + "type": "string" + }, + { + "id": "a5b6c7d8-e9f0-1234-abcd-345678901234", + "name": "enrichment.person.linkedin_url", + "value": "https://linkedin.com/in/test-person", + "type": "string" + }, + { + "id": "b6c7d8e9-f0a1-2345-bcde-456789012345", + "name": "enrichment.person.city", + "value": "London", + "type": "string" + }, + { + "id": "c7d8e9f0-a1b2-3456-cdef-567890123456", + "name": "enrichment.person.state", + "value": "England", + "type": "string" + }, + { + "id": "d8e9f0a1-b2c3-4567-defa-678901234567", + "name": "enrichment.person.country", + "value": "United Kingdom", + "type": "string" + }, + { + "id": "e9f0a1b2-c3d4-5678-efab-789012345678", + "name": "enrichment.person.seniority", + "value": "Director", + "type": "string" + }, + { + "id": "f0a1b2c3-d4e5-6789-fabc-890123456789", + "name": "enrichment.organisation.name", + "value": "Acme Corp", + "type": "string" + }, + { + "id": "a1b2c3d4-e5f6-7890-abcd-901234567890", + "name": "enrichment.organisation.domain", + "value": "acmecorp.com", + "type": "string" + }, + { + "id": "b2c3d4e5-f6a7-8901-bcde-012345678901", + "name": "enrichment.organisation.industry", + "value": "Software", + "type": "string" + }, + { + "id": "c3d4e5f6-a7b8-9012-cdef-123456789013", + "name": "enrichment.organisation.estimated_employees", + "value": 250, + "type": "number" + }, + { + "id": "d4e5f6a7-b8c9-0123-defa-234567890124", + "name": "enrichment.organisation.annual_revenue", + "value": 5000000, + "type": "number" + }, + { + "id": "e5f6a7b8-c9d0-1234-efab-345678901235", + "name": "enrichment.organisation.linkedin_url", + "value": "https://linkedin.com/company/acmecorp", + "type": "string" + }, + { + "id": "f6a7b8c9-d0e1-2345-fabc-456789012346", + "name": "enrichment.organisation.website_url", + "value": "https://acmecorp.com", + "type": "string" + }, + { + "id": "a7b8c9d0-e1f2-3456-abcd-567890123457", + "name": "enrichment.enrichment_error", + "value": false, + "type": "boolean" + }, + { + "id": "b8c9d0e1-f2a3-4567-bcde-678901234568", + "name": "enrichment.credits_consumed", + "value": 0, + "type": "number" + }, + { + "id": "c9d0e1f2-a3b4-5678-cdef-789012345679", + "name": "enrichment.rate_limit_remaining", + "value": null, + "type": "null" + }, + { + "id": "d0e1f2a3-b4c5-6789-defa-890123456780", + "name": "enrichment.credits_low", + "value": false, + "type": "boolean" + }, + { + "id": "e1f2a3b4-c5d6-7890-efab-901234567891", + "name": "grading.profile_grade", + "value": "Full Profile", + "type": "string" + }, + { + "id": "f2a3b4c5-d6e7-8901-fabc-012345678902", + "name": "grading.fields_populated", + "value": 13, + "type": "number" + }, + { + "id": "a3b4c5d6-e7f8-9012-abcd-123456789013", + "name": "grading.graded_at", + "value": "={{ $now.toISO() }}", + "type": "string" + } + ] + }, + "options": {} + } + }, + { + "id": "b4c5d6e7-f8a9-0123-bcde-234567890124", + "name": "Apollo HTTP Request", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1500, 350], + "parameters": { + "method": "POST", + "url": "https://api.apollo.io/api/v1/people/match", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "x-api-key", + "value": "={{ $env.APOLLO_API_KEY }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ email: $json.lead.email, first_name: $json.lead.first_name, last_name: $json.lead.last_name, organization_name: $json.lead.company_name }) }}", + "options": { + "response": { + "response": { + "fullResponse": true + } + }, + "retry": { + "maxTries": 3, + "waitBetweenTries": 5000 + }, + "timeout": 30000 + }, + "onError": "continueRegularOutput" + } + }, + { + "id": "c5d6e7f8-a9b0-1234-cdef-345678901235", + "name": "Enrichment Mapper & Grader", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "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;" + } + }, + { + "id": "d6e7f8a9-b0c1-2345-defa-456789012346", + "name": "Success Response", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [2100, 200], + "parameters": { + "respondWith": "json", + "responseBody": "={{ JSON.stringify($json) }}", + "options": { + "responseCode": 200 + } + } + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { "node": "Input Mapper", "type": "main", "index": 0 } + ] + ] + }, + "Input Mapper": { + "main": [ + [ + { "node": "Clean & Validate", "type": "main", "index": 0 } + ] + ] + }, + "Clean & Validate": { + "main": [ + [ + { "node": "Validation Router", "type": "main", "index": 0 } + ] + ] + }, + "Validation Router": { + "main": [ + [ + { "node": "Dead Letter", "type": "main", "index": 0 } + ], + [ + { "node": "Test Mode Check", "type": "main", "index": 0 } + ] + ] + }, + "Dead Letter": { + "main": [ + [ + { "node": "Dead Letter Response", "type": "main", "index": 0 } + ] + ] + }, + "Test Mode Check": { + "main": [ + [ + { "node": "Mock Enrichment", "type": "main", "index": 0 } + ], + [ + { "node": "Apollo HTTP Request", "type": "main", "index": 0 } + ] + ] + }, + "Mock Enrichment": { + "main": [ + [ + { "node": "Success Response", "type": "main", "index": 0 } + ] + ] + }, + "Apollo HTTP Request": { + "main": [ + [ + { "node": "Enrichment Mapper & Grader", "type": "main", "index": 0 } + ] + ] + }, + "Enrichment Mapper & Grader": { + "main": [ + [ + { "node": "Success Response", "type": "main", "index": 0 } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "pinData": {} +}