Audit report · No-code automation · Make + Airtable + HubSpot
I audited a six-scenario Make and Airtable lead pipeline as a production system: how it deduplicates, what happens under concurrent runs, how it retries, what it logs, and where its error handling quietly redefines what "success" means.
A working lead-routing system built in Make: a public webhook takes an inbound lead, an Airtable base (Leads, Clients, Orders, Dispatch Queue, Logs) holds all state, and a chain of scenarios routes, deduplicates, retries, and finally upserts each lead into HubSpot. This is a self-audit of a system I built and iterated on over several months, using the same process I'd apply to a client's automation. The findings below are what a real automation accumulates after months of iteration, not a staged demo.
The Worker and the Finalizer both read the same Dispatch Queue record independently — one marks it done, the other does the actual delivery — with no field connecting the two outcomes. That gap is finding F-01, below.
Same checklist I'd run on anyone's automation, not specific to Make:
The worker claims a queue job, sets it to processing, then — with no delivery step in between — sets it to done. The real work (check for an existing client, upsert into HubSpot) happens entirely inside the finalizer, a separate scenario triggered instantly on the same record. The worker only polls every 15 minutes, so the finalizer almost always wins the race, and done has reliably looked like "confirmed in HubSpot." Nothing enforces that order.
Built four days apart, never wired together.
v1 ("Диспетчер") polls every 15 minutes for leads with Send status = "новый" and creates a job for whatever it finds — no check for a job already in flight. Two overlapping runs, or a retry after a timeout, can create two dispatch jobs for the same lead.
The finalizer does search for an existing client first — but the create-record step also has an error-handler path wired up as a second, implicit branch: on failure, it updates and upserts as if the client already existed. Any create failure takes that branch, not only a genuine duplicate.
Exhibit A — raw blueprint excerpt, F-03's error-handler branch
"onerror": [
{ "module": "airtable:ActionUpdateRecords",
"mapper": { "table": "dispatch_queue",
"record": { "last_error": "{{12.error.message}}" } } },
{ "module": "airtable:ActionUpdateRecords",
"mapper": { "table": "clients",
"record": { "phone": "{{12.`Phone normalized`}}",
"email": "{{12.email}}" } } },
{ "module": "hubspotcrm:upsertAContact",
"mapper": { "email": "{{12.email}}", "phone": "{{12.`Phone normalized`}}" } },
{ "module": "builtin:Resume" }
]
Field IDs replaced with their real names for readability; structure and module order are unedited from the live blueprint. Module 12 is the Create-record step this whole branch only runs when it fails.
v2 fixed F-02 with a proper dedup search before creating a job. v1 wasn't deleted — just deactivated, sitting in the same workspace as "Диспетчер," a name with no version marker distinguishing it from its replacement.
The worker's attempts counter governs retries of its own status write — processing → done — not the HubSpot delivery, which happens in the finalizer, a different scenario entirely. The one call actually worth retrying has no retry logic at all: if the finalizer's upsert fails, that scenario just stops, and nothing in Airtable reflects it. What retry logic does exist has no backoff either — under 5 attempts, try again; 5 or more, mark permanently failed, paced only by the worker's fixed 15-minute poll.
A dedicated logs table models exactly what you'd want for debugging — stage, action, status, error_code, run_id, details_json. The intake scenario creates a record there on every call, but with no fields mapped. Live data confirms it: every row has only an autonumber and a timestamp.
| Finding | What actually happens to the business |
|---|---|
| F-01 · P0 | A lead can silently never reach the CRM while every internal signal says "done" — no alert fires, because nothing is checking. |
| F-02 · P1 | The same lead gets worked, or contacted, twice — under load or a retry storm, not rarely. |
| F-03 · P1 | A rate limit or a network blip gets treated as "already a customer" — a genuinely new lead can be dropped instead of created. |
| F-04 · P1 | Re-enabling the wrong scenario during an incident brings back a race condition that was already fixed once. |
| F-05 · P2 | Failing calls retry at a fixed pace regardless of cause — no protection against repeatedly hitting an already-struggling API. |
| F-06 · P2 | When something does break, there's no trail. Every incident starts debugging from zero. |
01Quick wins — days
02Hardening — structural
done means "confirmed in HubSpot," not "the worker didn't error."03Rebuild option
Once the bottleneck is coordination between scenarios rather than any single scenario's logic, the fix stops being a Make fix. Moving the queue, retry, and delivery-confirmation logic into one owned service removes the cross-scenario handoff instead of patching around it — the same shift behind this portfolio's own Lead Pipeline Reliability Platform.
A one-page summary like this, handed over alongside the full findings above:
Ship the quick wins and hardening above. Good fit while one team owns the base and the scenario count stays small enough to reason about by hand.
Once coordination between scenarios is itself the risk, see what that looks like built as one system: Lead Pipeline Reliability Platform →
This is the same checklist — dedup, concurrency, retries, observability, error handling, dead paths — I'd run on any existing automation before deciding which path fits.