n8nenrichmenterror-handling

Enrichment Error Handling in n8n

n8n enrichment workflows fail silently when APIs return errors. Error handling patterns: retry with backoff, fallback chains, dead letter queues, alert nodes.

8 min

The single biggest cause of n8n workflow failures is not API downtime or rate limits. It is enrichment APIs returning 200 OK with unexpected JSON shapes that silently break downstream nodes. A normalizer sub-workflow that standardizes every enrichment response into a consistent schema prevents this class of failure entirely.

The 200-With-Error Problem

Most enrichment APIs return HTTP 200 even when they cannot find data for a given input. Apollo returns an empty contacts array. Clearbit returns null fields. Prospeo returns a status field buried in the response body. Each API's "no data found" pattern is different, and n8n's error handling only triggers on non-2xx status codes by default.

The result: your workflow processes an empty or malformed response as if it were valid data, passes null values downstream, and either produces garbage output or crashes three nodes later with an unhelpful "Cannot read property of undefined" error.

The Normalizer Pattern

Build a sub-workflow that sits between every enrichment API call and your downstream logic. It does three things: validates the response has the fields you expect, maps vendor-specific field names to your internal schema, and flags missing data explicitly instead of passing nulls.

JavaScript
// n8n Function node: Enrichment Response Normalizer
// Place this after every HTTP Request node that calls an enrichment API

const input = $input.first().json;
const source = $node["HTTP Request"].params.url;

const normalized = {
  _source: source,
  _enriched_at: new Date().toISOString(),
  _has_data: false,
  company_name: null,
  domain: null,
  industry: null,
  employee_count: null,
  email: null,
  phone: null,
  linkedin_url: null,
  error: null,
};

try {
  if (source.includes("apollo")) {
    const contact = input.contacts?.[0] || input.person || {};
    normalized.company_name = contact.organization?.name || null;
    normalized.domain = contact.organization?.website_url || null;
    normalized.email = contact.email || null;
    normalized._has_data = !!normalized.email;
  } else if (source.includes("scavio")) {
    const results = input.organic_results || input.organic || [];
    const top = results[0] || {};
    normalized.company_name = top.title || null;
    normalized.domain = top.link || null;
    normalized._has_data = results.length > 0;
  } else if (source.includes("prospeo")) {
    normalized.email = input.email || null;
    normalized.company_name = input.company || null;
    normalized._has_data = input.status === "valid";
  }
} catch (e) {
  normalized.error = e.message;
}

return [{ json: normalized }];

Downstream Benefits

Every node after the normalizer can rely on the same field names regardless of which enrichment API produced the data. This means you can swap enrichment providers without touching downstream logic. When Apollo's API changes field names (which happens more often than their changelog suggests), you update one Function node instead of every workflow that depends on Apollo data.

The _has_data boolean is the key improvement. Instead of checking for null fields scattered across different response shapes, downstream IF nodes check one field. Routes split cleanly into "enriched" and "needs fallback" paths.

Multi-Provider Waterfall

Chain enrichment providers with the normalizer between each step. First try Scavio for SERP-based company signals ($0.005/credit), then fall back to Prospeo for email verification, then Apollo for contact details. Each normalizer outputs the same schema, so you can merge results cleanly.

JavaScript
// n8n Function node: Merge enrichment results
// Receives normalized outputs from multiple providers

const results = $input.all().map(i => i.json);
const merged = { ...results[0] };

for (const r of results.slice(1)) {
  for (const [key, value] of Object.entries(r)) {
    if (key.startsWith("_")) continue;
    if (value && !merged[key]) {
      merged[key] = value;
      merged._source = r._source;
    }
  }
}

merged._providers_tried = results.length;
merged._has_data = results.some(r => r._has_data);

return [{ json: merged }];

Testing the Normalizer

Create a test sub-workflow that sends known-bad responses through your normalizer: empty arrays, null bodies, HTML error pages disguised as 200 responses, timeout responses. If the normalizer handles all of these gracefully (returns the schema with _has_data: false and a descriptive error field), it will survive production.

The investment is 30 minutes to build the normalizer once. The payoff is never debugging a "workflow stopped working at 3am because the API changed a field name" incident again.