DEV Community

Cover image for My n8n lead agent looked fine until I found 3 failure points: Apollo 200s, HubSpot 423s, and bad JSON handoffs
Lars Winstand
Lars Winstand

Posted on Originally published at standardcompute.com

My n8n lead agent looked fine until I found 3 failure points: Apollo 200s, HubSpot 423s, and bad JSON handoffs

My n8n lead agent looked fine until I found 3 failure points: Apollo 200s, HubSpot 423s, and bad JSON handoffs

At 2:13 a.m. my n8n lead-enrichment flow marked the same prospect as enriched twice, Apollo returned HTTP 200 with nothing useful, HubSpot threw 423 Locked, and GPT-5 kept confidently retrying bad JSON.

The worst part: the workflow looked healthy.

Green checks in n8n. No dramatic stack trace. Just a slow leak of duplicate contacts, half-filled records, and retries that made the whole thing more expensive without making it more correct.

After debugging it, my take is pretty simple:

This usually is not an LLM intelligence problem. It is a contract-design problem.

A lot of n8n builders blame GPT-5 for bugs that actually come from:

  • Apollo edge cases
  • HubSpot write behavior
  • weak JSON validation
  • retry logic with no idempotency

My strong opinion: strict JSON contracts + idempotency keys beat “let the model fix it on retry” every time.

I originally thought the model was the weak link. That was the false diagnosis.

The real failures were happening in the handoffs between n8n, Apollo’s enrichment API, HubSpot’s contact write API, and the model’s JSON output.

The workflow looked reasonable on paper

The flow was straightforward:

  1. Pull a new lead into n8n
  2. Send the lead to Apollo for enrichment
  3. Ask GPT-5 to normalize the result into my target schema
  4. Write the contact into HubSpot
  5. Retry if anything failed

That sounds fine until:

  • Apollo returns something technically successful but operationally useless
  • GPT-5 turns partial input into confident-looking JSON
  • HubSpot locks the write for a short window
  • your retry path creates duplicates instead of recovering safely

So the first symptom was not “the agent crashed.”

It was worse.

The agent kept running.

Failure point #1: Apollo returned 200, but the lead was still unusable

This was the first trap.

Apollo can return HTTP 200 even when the enrichment result does not contain enough identity data to support the next step.

If your n8n branch only checks the status code, the workflow moves on as if the lead is enriched.

Then GPT-5 gets a payload with missing fields, tries to be helpful, and emits JSON that is structurally valid enough to pass a casual glance but semantically wrong for your HubSpot write.

That is where people say, “the model hallucinated.”

Sometimes it did.

But in my case, the bigger issue was that I asked the model to normalize a record that Apollo never really enriched in the first place.

Bad success check

if (apolloResponse.status === 200) {
  return { enriched: true, data: apolloResponse.data };
}
Enter fullscreen mode Exit fullscreen mode

That is not enough.

Better success check

function isUsableApolloRecord(data) {
  return Boolean(
    data &&
    data.person &&
    data.person.email &&
    data.person.first_name &&
    data.person.last_name &&
    data.company &&
    data.company.name
  );
}

if (apolloResponse.status === 200 && isUsableApolloRecord(apolloResponse.data)) {
  return { enriched: true, data: apolloResponse.data };
}

return {
  enriched: false,
  reason: "Apollo returned 200 but required fields were missing"
};
Enter fullscreen mode Exit fullscreen mode

My rule now is simple:

Apollo does not count as successful unless the payload contains the exact fields the next step requires.

HTTP 200 is not success.

A usable person or company record is success.

That one change removed a lot of fake progress from the workflow.

Failure point #2: HubSpot 423 Locked is not a normal retry case

This was the second trap.

HubSpot can return 423 Locked for a short window, often around a couple of seconds.

If your workflow treats that like a generic failure and immediately retries the same write, you can create the exact mess you were trying to avoid.

My broken version of the flow did three things wrong:

  • retried too fast
  • retried without a real idempotency key
  • let the fallback path create a new contact instead of proving whether the first write eventually landed

That is how you get duplicate contacts and weird audit trails.

Bad retry logic

for (let attempt = 0; attempt < 3; attempt++) {
  try {
    return await createHubSpotContact(payload);
  } catch (err) {
    // immediately retry everything
  }
}
Enter fullscreen mode Exit fullscreen mode

Better retry logic

async function upsertHubSpotContact(payload, idempotencyKey) {
  try {
    return await createHubSpotContact(payload, idempotencyKey);
  } catch (err) {
    if (err.statusCode === 423) {
      await sleep(3000);

      const existing = await findHubSpotContactByEmail(payload.email);
      if (existing) {
        return { recovered: true, contact: existing };
      }

      return await createHubSpotContact(payload, idempotencyKey);
    }

    throw err;
  }
}
Enter fullscreen mode Exit fullscreen mode

The boring pattern that actually works

  • Use email as the primary unique key when possible
  • Store an idempotency key in n8n before the HubSpot write
  • On 423 Locked, wait longer than your instinct says
  • Re-check HubSpot before retrying a create path
  • Separate “retry the same write” from “attempt a new create”

This is where I stopped blaming GPT-5 entirely.

No model can rescue bad write semantics.

Failure point #3: GPT-5 was retrying bad JSON because I let it

My first fix was the classic one:

  • tighten the prompt
  • ask for cleaner JSON
  • add more examples
  • tell the model not to invent missing fields

That helped a little.

It did not solve the real problem.

Prompting harder is the wrong first move when Apollo is returning thin records and HubSpot is temporarily locking writes.

You are polishing the middle of the pipeline while the ends are lying to each other.

If I had to pick one loser pattern, it is this:

“let the model fix it on retry.”

That pattern burns time, burns tokens, and hides the actual bug.

The winner is strict validation before and after every named service call.

What changed in the n8n workflow

I wanted a clever lead-enrichment agent.

What actually worked was a more disciplined workflow.

The fixes were not glamorous:

  • validate Apollo payloads against required fields before any model call
  • use Structured Outputs instead of “please return valid JSON” whenever possible
  • reject partial JSON instead of trying to salvage it downstream
  • add idempotency keys before HubSpot writes
  • treat HubSpot 423 Locked as a timed retry case, not a generic error
  • log every payload transition between n8n, Apollo, GPT-5, and HubSpot

The most important change was deciding that partial success is failure.

Once I stopped letting weak Apollo results and half-valid model output sneak through, the workflow got much quieter.

The contract I wish I had written first

This is the kind of schema gate I should have added before the model step:

{
  "type": "object",
  "required": ["email", "first_name", "last_name", "company_name"],
  "properties": {
    "email": { "type": "string", "format": "email" },
    "first_name": { "type": "string", "minLength": 1 },
    "last_name": { "type": "string", "minLength": 1 },
    "company_name": { "type": "string", "minLength": 1 },
    "job_title": { "type": "string" },
    "linkedin_url": { "type": "string" }
  },
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

And this is the kind of validation step that saves you from downstream nonsense:

import Ajv from "ajv";

const ajv = new Ajv();
const validate = ajv.compile(schema);

const ok = validate(modelOutput);
if (!ok) {
  throw new Error(`Invalid model output: ${JSON.stringify(validate.errors)}`);
}
Enter fullscreen mode Exit fullscreen mode

If the payload is incomplete, fail early.

Do not pass garbage to HubSpot and hope retries make it cleaner.

A practical n8n checklist

If you are building lead enrichment in n8n, this is the checklist I would use now.

Step What to validate
Apollo response Required identity fields exist, not just 200 OK
Model input Missing fields are explicit, not implied
Model output JSON matches schema exactly
HubSpot write Idempotency key stored before request
HubSpot retry 423 Locked gets delay + re-check
Final state Contact exists once, with expected properties

Why this gets expensive fast with per-token pricing

This part matters if you are running agents in n8n, Make, Zapier, OpenClaw, or custom automations at real volume.

When a workflow bounces across Apollo, HubSpot, and multiple model calls, debugging is not one request.

It is a chain of:

  • retries
  • schema checks
  • reformats
  • replay runs
  • validation loops
  • post-failure cleanup

If you are paying per token, reliability work gets punished twice:

  1. during development
  2. again in production when edge cases trigger extra model calls

That pricing model changes behavior.

Teams get conservative about testing.
They avoid aggressive replay.
They hesitate to add validation loops because every safeguard has a visible marginal cost.

Flat-rate compute changes that.

If your API layer can route across models and absorb heavy retry and testing behavior without surprise bills, you can afford to build the safer version of the workflow instead of the cheapest-looking one.

That is a big reason I think services like Standard Compute are interesting for agent-heavy workflows.

If you are running lead agents all day, predictable cost is not just a finance benefit.

It changes how seriously you can treat reliability.

What I would tell anyone building this in n8n, Make, or Zapier

Stop doing these five things:

  • treating status-code success as workflow success
  • asking GPT-5 to paper over missing Apollo data
  • retrying HubSpot writes without idempotency
  • calling malformed or partial JSON “close enough”
  • assuming the expensive part of the workflow is the smart part

In my experience, the real damage happens in the boring places:

  • the API response you did not validate
  • the lock you retried too quickly
  • the duplicate write you did not make idempotent

That was the lesson for me.

My n8n lead agent did not need a smarter model nearly as much as it needed stricter contracts between Apollo, GPT-5, HubSpot, and the workflow itself.

If your lead-enrichment agent feels like “flaky AI,” check the handoffs first.

That is probably where the bug actually lives.

Top comments (0)