Hi everyone, I’m Mario…
AI lead workflow succeeds, but qualified leads don’t reliably reach HubSpot — architecture advice?
Hi everyone, I’m Mario…
AI lead workflow succeeds, but qualified leads don’t reliably reach HubSpot — architecture advice?
Hey @MarioCovela, welcome to the HubSpot Community! ![]()
And thanks for bringing this great question to us!
Could you share a little more about your setup? How are the qualified leads being sent to HubSpot (API, integration, webhook, etc.), and are you seeing any errors or patterns when a lead doesn’t make it through?
Those details should help the Community narrow down where the issue might be happening.
In the meantime, here is an article that might help: " Sync lead ownership and activities ".
Also, let’s consult our Top Experts: Hi @danmoyle, @CHawkins and @josh do you have suggestions to help @MarioCovela, please?
Thanks so much and have a lovely day!
Bérangère
Welcome to the Community @MarioCovela! I’m with @Berangere here - if we could get some context without getting sensitive data, that would be helpful. Was this a workflow template you used or built yourself? What are the criteria you’ve set for “qualified?” And is this workflow supposed to go hunting for accounts/companies or is it supposed to update companies as they’re added to your HubSpot?
One piece of general advice for HubSpot today I have is: Use the Breeze Assistant in your portal. Asking it questions can often unlock help right on the page you’re viewing.
Moderator note: While this solution may not address the original poster’s specific situation, it could be helpful for other community members facing similar challenges.
One thing I’d add to the checks above: don’t use “workflow completed” as the success condition.
For each qualified lead, I’d persist a corelation key plus the intended HubSpot action, then only mark it synced after HubSpot returns a successful write and you can resolve the resulting record ID
That makes the missing cases much easier to classify: never attempted, API rejected, retry exhausted, or written successfully but not where you expected.
I’d also make retries idempotent so a timeout/unknown response can’t create a second contact or repeat the downstream action
If you can capture one qualifid lead that went missing and its HubSpot request/response, that should narrow this down pretty quickly
I’d separate “the AI workflow completed” from “HubSpot accepted and persisted the lead.”
For each qualified lead, I’d keep a small delivery record with a stable key (email or your own lead ID), the HubSpot request result, returned record ID, and sync status. Only mark it delivered after HubSpot confirms the write
The awkward case is a timeout or interrupted response: HubSpot may have created the contact even though your workflow never received confirmation. Blindly retrying there can create duplicate side effects
Before a retry, look up the contact by the same stable key. If it already exists, reconcile the existing record instead of creating again.
I’d also run a simple reconciliation job for qualified leads that have no confirmd HubSpot record after a few minutes. That catches the silent/ambiguous failures without depending on the AI workflow’s “success” state
Hi Mahdi and everyone,
Thank you — separating workflow completion from confirmed CRM delivery is very helpful. We are defining the acceptance checks before allowing automated writes, and I would appreciate your advice on a few practical points:
We have not yet established the root cause, so I am treating these as checks to validate, rather than assuming a particular API or connector behaviour. If we share one failed request/response example with personal data and credentials removed, what additional details would you need to diagnose it?
Thanks again,
Mario
Hi @MarioCovela and all,
Thanks for sharing this! With this additional context, I now understand that you’d like to know how to have automated CRM writes to HubSpot, preventing duplicates or lost updates when API/connector requests fail or have uncertain outcomes. You are trying to design and test an integration that automatically creates/updates HubSpot records before they go into production. Please let me know if that’s not the case.
Sharing a failed request/response example with any sensitive data removed would be a helpful next step. Could you also include the endpoint/action being used, the HTTP status code and error response, and whether the request is going directly to the HubSpot API or through a connector/integration?
I’d love to check with our Top Experts: Hi @Anton and @evaldas, do you have suggestions, please?
Thanks so much in advance for your help!
Bérangère
Hi Bérangère, Anton, Evaldas, Mahdi and everyone,
Thank you. We have now gone back through our internal incident log and
reconstructed a real case.
On October 1 we had several researched/high-fit companies that had
completed the research/qualification side of our workflow but were
still absent from HubSpot. Four examples recorded at the time were
CHEFS CULINAR, REMA 1000, Holland & Barrett and Bidfood.
At that point our architecture was:
AI research / qualification → human approval gate → HubSpot
The weakness was between qualification and CRM delivery. The approval
gate did not yet have a persistent delivery queue with an explicit
acceptance state. Therefore “research completed” could exist without
durable evidence that a CRM write had actually been accepted,
attempted or persisted.
Unfortunately, we cannot provide the original HTTP request/response
for that October 1 incident because we were not recording enough
write-level telemetry at that point. We do not want to invent an
endpoint, status code or API error that we did not capture.
We subsequently introduced a persistent queue and stronger delivery
evidence. By our third verified test run on October 4, the Lead
Factory recorded 12 accounts researched, 2 accepted, 2 persisted, and
2 subsequently verified by reading them back, with stable unique lead
IDs in the queue.
We rechecked HubSpot today. REMA 1000 and Holland & Barrett are now
present in the CRM. CHEFS CULINAR and Bidfood are still absent.
Our intended acceptance model is now:
qualified ≠ delivered
A record should only become DELIVERED after:
For an ambiguous outcome such as a timeout, our intended state is
UNKNOWN rather than FAILED. Before another create, the worker should
search/reconcile using the stable identity key.
We also found during this investigation that different HubSpot
operations in our integration have different permission/scope
requirements. We are therefore adding operation → credential →
required scope → approval class to our Tool & Permission Registry.
We would appreciate your advice on five remaining points:
Our goal is deliberately conservative: we want to prove persistence,
reconciliation and idempotency before allowing the agents to write
autonomously to HubSpot.
Thank you again. Your suggestions have already changed the way we
define a successful CRM handoff.
Mario
Thanks Mario — this is much closer to how I’d structure it.
For persistence, a successful write + returned HubSpot record ID + read-back from the intended portal is enough for me to move that delivery to DELIVERED. I’d keep the queue/ledger as the durable audit trail rather than add another synchronous acceptance step
For UNKNOWN, I’d reconcile before permitting another create: returned HubSpot ID if you have one → your stable external ID → trusted business identifiers. If that produces an ambiguous match, stop for review rather than create again
I’d also change the identity order sligtly. If Lead Factory already has a stable ID for the company, I’d store that in HubSpot as a unique property and use it as the integration key. Domain is still useful for matching existing records, but I wouldn’t use it as the idempotency key when domains can be missing or shared
The outcome classes you listed make sense. I’d keep UNKNOWN separate because it means “we don’t know whether the side effect happened,” which is different from a known retryable or permanent failure
For concurrency, I’d keep the claim/lock in the orchestration layer. I don’t know of a HubSpot-native distributed lock I’d rely on for two workers racing to create the same company. Where it fits your write path, I’d also prefer HubSpot’s upsert using the unique external-ID property over a search-then-create sequence, but I’d still keep the worker-level concurrency control
The acceptance test I’d definitly add is exactly that race: submit the same stable ID through two workers concurrently and verify you end with one company and one downstream side effect
And for CHEFS CULINAR and Bidfood, I’d treat them as reconciliation cases first. I wouldn’t recreate either until you can establish that the October 1 attempt didn’t persist under another identity
Hi Mahdi, Jaya, Bérangère and everyone,
A significant update from today’s investigation.
We reconciled our durable Lead Queue against the actual HubSpot Company database and found that the problem was larger than the two historical cases we had been watching.
Seventeen companies had already passed our research/qualification process with a GO decision and were durably stored in the Lead Queue, but they were absent from HubSpot.
We manually recovered those 17 companies today. HubSpot accepted all 17 Company creates and returned a record ID for every one. We then read the created records back from the intended HubSpot portal and updated the Lead Queue with the corresponding HubSpot IDs.
So we now have a much clearer boundary:
Research → Qualification → Durable Lead Queue: working
Durable Lead Queue → HubSpot Company: this is where qualified companies were being stranded.
This also confirms why “workflow completed” or even “queue persisted” cannot be our success condition. We are now using:
HubSpot write → returned Company ID → read-after-write → durable delivery record = DELIVERED
Anything less is not CRM_DELIVERED.
CHEFS CULINAR and Bidfood remain deliberately excluded from this manual recovery. Following Mahdi’s advice, we are keeping both as UNKNOWN / HUMAN REVIEW until the October 1 outcomes can be reconciled safely. We did not blindly recreate them.
The manual recovery proves that these companies themselves were not inherently rejected by HubSpot. What we still need to isolate is why the automated handoff from the durable queue did not execute/complete for them.
Mahdi’s recommendation about using our immutable Lead Factory ID as a unique HubSpot Company property now looks particularly important. We have audited the current Company configuration and do not see a dedicated unique external-ID property for this integration yet.
Our intended production path is therefore:
Qualified Lead
→ durable queue
→ worker claim/lock
→ reconcile by immutable external ID
→ HubSpot upsert
→ returned Company ID
→ read-after-write
→ durable DELIVERED state
The failure mode we want to eliminate is very concrete now: we never want a human to discover days later that commercially useful, already-qualified companies have been silently accumulating between the AI system and the CRM.
Given today’s evidence, we would particularly value your advice on two points:
Besides creating the unique external-ID Company property and using upsert, what HubSpot-side or integration-side telemetry would you instrument to prove whether the automated handoff was never attempted, was blocked before reaching HubSpot, or reached HubSpot but failed?
Would you implement a separate reconciliation watchdog that periodically queries all QUEUE_PERSISTED records lacking CRM_DELIVERED after a short SLA, even after the primary handoff is repaired? Our instinct now is yes, as an independent safety net.
Thank you again. Your previous replies directly influenced the controls that prevented us from blindly recreating the ambiguous historical records.
Mario
Hi Mario — this is a really useful finding. The fact that all 17 manual creates succeeded narrows the problem down quite a bit.
I’d instrument the handoff as a small delivery state machine rather than relying on workflow completion. For each company I’d keep the immutable Lead Factory ID, an attempt ID, attempt timestamp/count, whether the HubSpot request was actually sent, HTTP status, HubSpot corelation/error details when available, returned Company ID, and the result of the read-after-write check. That should let you distinguish “worker never picked it up” from “attempted but failed” and “HubSpot accepted it but our local state never advanced.”
And yes, I’d definitely keep the reconciliation watchdog even after fixing the primary path. I’d just make it a reconciler first and a retry mechanism second. If a QUEUE_PERSISTED record misses the delivery SLA, first look it up in HubSpot by the immutable external ID. If it exists, repair the local delivery state. If it doesn’t, retry the upsert. If the outcome is still ambiguous, move it to UNKNOWN / HUMAN_REVIEW rather than creating again
That separation is important because a timeout or lost response doesnt necessarily mean HubSpot never committed the write
Given what you found today, I’d also alert on the age and count of records stuck between QUEUE_PERSISTED and CRM_DELIVERED. That would have surfaced these 17 as an operational issue much earlier instead of waiting for someone to notice missing companies.
I think the architecture you outlined is heading in the right direction: make delivery provable, and make reconciliation independent of the happy path