Phase 3 audit of Lead Enrich Core + Lead Actions Example: - Renamed 'Sheets Audit Log' to 'Audit Log Formatter' (was dead-end Set node) - Documented formatter stub in README - Deleted duplicate inactive workflow from instance - Fixed Execute Workflow Trigger missing inputSource parameter - Scorecard: PASS (7/7 eval scenarios, p95 361ms, 0 silent failures) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lead Enrich Pipeline — Module A1
Webhook-triggered lead enrichment pipeline. Accepts a lead via POST, enriches it through Apollo (or mock data in test mode), grades the profile, and passes the result to a configurable downstream actions workflow.
Architecture
POST /webhook/lead-enrich
│
▼
Input Mapper ─── normalise fields
│
▼
Clean & Validate ─── trim, lowercase, email regex
│
├── INVALID ──► Dead Letter Response (422)
│
▼
Test Mode Check
│
├── TEST_MODE=true ──► Mock Enrichment (static data)
│
├── TEST_MODE=false ──► Apollo HTTP Request
│ │
│ ▼
│ Enrichment Mapper & Grader
│
▼
Success Response (200) + Call Actions Workflow
Two workflows:
lead-enrich-core.json— the pipeline above (this is the product)lead-actions-example.json— reference recipe showing downstream routing by grade (CRM sync, Slack notification, welcome email, audit log formatter)
Quick Start
- Import
lead-enrich-core.jsoninto n8n (Settings → Import Workflow or via API) - Set
TEST_MODE=truein your n8n environment variables - Activate the workflow
- Test:
curl -X POST https://your-n8n-instance/webhook/lead-enrich \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "first_name": "Test", "last_name": "User", "company_name": "Acme", "source": "manual"}'
You should get a 200 response with mock enrichment data and a "Full Profile" grade.
Input Schema
POST JSON body:
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | Yes | Validated with regex. Lowercased automatically. |
first_name |
string | No | Passed to Apollo for matching accuracy |
last_name |
string | No | Passed to Apollo for matching accuracy |
company_name |
string | No | Passed to Apollo as organization_name |
source |
string | No | Defaults to "webhook". Use for tracking origin. |
Accepts fields at root level ($json.email) or nested in body ($json.body.email) — the Input Mapper handles both.
Output Schema
200 response:
{
"lead": {
"email": "...",
"first_name": "...",
"last_name": "...",
"company_name": "...",
"source": "...",
"submitted_at": "ISO timestamp",
"raw_payload": { ... }
},
"enrichment": {
"provider": "apollo|mock",
"person": {
"title": "...",
"linkedin_url": "...",
"city": "...",
"state": "...",
"country": "...",
"seniority": "..."
},
"organisation": {
"name": "...",
"domain": "...",
"industry": "...",
"estimated_employees": 250,
"annual_revenue": 5000000,
"linkedin_url": "...",
"website_url": "..."
},
"enrichment_error": false,
"credits_consumed": 1,
"rate_limit_remaining": 95,
"credits_low": false
},
"grading": {
"profile_grade": "Full Profile|Partial Profile|Limited",
"fields_populated": 13,
"graded_at": "ISO timestamp"
}
}
422 response (validation failure):
{
"error": true,
"error_reason": "Invalid email format: ...",
"lead": { ... },
"timestamp": "ISO timestamp"
}
Profile Grading
| Grade | Criteria |
|---|---|
| Full Profile | Company name + job title + employee count all present |
| Partial Profile | Company name present but missing title or employee count |
| Limited | No company data, enrichment error, or no Apollo match |
Environment Variables
Set these on your n8n instance (Docker env vars, not n8n Variables which require a paid plan):
| Variable | Required | Description |
|---|---|---|
TEST_MODE |
Yes | "true" for mock data, "false" for live Apollo |
APOLLO_API_KEY |
For live mode | Apollo People Enrichment API key. Requires a paid Apollo plan — the free tier does not include the people/match endpoint. |
See .env.example for the full list including downstream action credentials (HubSpot, Slack, SMTP).
Apollo API Notes
- Endpoint:
POST https://api.apollo.io/api/v1/people/match - Free tier limitation: The
people/matchendpoint is not available on the free plan. You need at least a Basic plan ($49/month) or equivalent. - Rate limits: 100 requests/minute on paid plans. The workflow reads
x-minute-requests-leftfrom response headers and setscredits_low: truewhen below 50. - No-match behaviour: Apollo returns a successful response with
person: null. No credits are consumed. The grader assigns "Limited" grade. - Retry logic: The HTTP Request node retries up to 3 times with 5-second backoff on failure.
Audit Log Formatter
The "Audit Log Formatter" node in the actions workflow formats lead data into a flat structure suitable for logging (timestamp, email, name, company, grade, source, provider, credits). It is a formatter stub — it prepares the data shape but has no downstream sink connected. Buyers should connect their own destination node (Google Sheets Append, Supabase Insert, Airtable, etc.) after this node.
Swap Points (for Client Deployment)
These are the nodes you'd modify when deploying for a client:
- Webhook path — change
lead-enrichto a client-specific path - Apollo API key — client provides their own key via env var
- Grading thresholds — adjust in the Enrichment Mapper & Grader Code node
- Actions workflow — replace
lead-actions-examplewith client-specific routing
Apollo Simulator (Test Mode)
When TEST_MODE=true, the workflow routes through an Apollo Simulator Code node instead of the real API. The simulator generates Apollo-shaped responses with varying data quality based on the email prefix:
| Email prefix | Simulated response | Expected grade |
|---|---|---|
full@ (or any other) |
All person + org fields populated | Full Profile |
partial@ |
Company name present, title and employee count null | Partial Profile |
limited@ |
Person data but empty organisation object | Limited |
nomatch@ or empty@ |
person: null, zero credits consumed |
Limited |
error@ |
HTTP 429 rate limit, zero remaining | Limited (error) |
The simulator output goes through the same Enrichment Mapper & Grader as real Apollo responses. This means test mode exercises the actual parsing logic, not a bypass.
Test Results (2026/03/28)
Validation Tests
| Test | Result | Notes |
|---|---|---|
| Invalid email (bad format) | PASS — 422 | Correct error message returned |
| Missing email | PASS — 422 | "Email is missing or empty" |
Grading Path Tests (via Apollo Simulator)
| Test | Result | Notes |
|---|---|---|
Full Profile (full@test.com) |
PASS — 200 | Grade: Full Profile, 13 fields, provider: apollo |
Partial Profile (partial@test.com) |
PASS — 200 | Grade: Partial Profile, 8 fields, title: null, employees: null |
Limited - no org (limited@test.com) |
PASS — 200 | Grade: Limited, 6 fields, company: null |
No match (nomatch@test.com) |
PASS — 200 | Grade: Limited, 0 fields, credits: 0, no error |
Rate limit error (error@test.com) |
PASS — 200 | Grade: Limited, error: true, rate_remaining: 0, credits_low: true |
| Lead data in response | PASS | Lead object present in all paths |
Throughput Tests
| Concurrent | All 200s | Wall time | Throughput | Notes |
|---|---|---|---|---|
| 10 | Yes | 543ms | ~18 req/sec | Avg 345ms per request |
| 25 | Yes | 989ms | ~25 req/sec | Avg 575ms per request |
| 50 | Yes | 3.95s | ~12 req/sec | Tail latency 3.3s (n8n queueing). Acceptable for lead volumes. |
Files
A1-lead-enrich-pipeline/
├── README.md ← this file
├── .env.example ← required credentials
└── workflows/
├── lead-enrich-core.json ← the product (Workflow 1)
└── lead-actions-example.json ← reference recipe (Workflow 2)
Pricing (Template Marketplace)
| Tier | Price | What's included |
|---|---|---|
| Core template | £39–49 | Raw workflows + this documentation. Buyer wires own adapters. |
| Ready-made bundle | £79–99 | Pre-configured for Tally → Apollo → HubSpot + Sheets + Slack. |
| Done-for-you | £350–600 | Jake deploys, configures, tests, hands over. 7-day support window. |