← back

Audit report · No-code automation · Make + Airtable + HubSpot

Make Automation Reliability Audit

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.

6 scenarios 5 linked Airtable tables 1 webhook intake 1 CRM target · HubSpot
01

What was handed over

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.

02

Data flow & responsibility map

Webhook intakegateway:CustomWebHook6
Leads tableAirtable
Router v1 — Диспетчер (retired)poll, no dedup guard2
Router v2 — Предобработкаevent-driven, dedup search4
Dispatch QueueAirtable
Workerstatus only, no delivery1
Finalizerdedup + HubSpot upsert3
HubSpot CRMdelivery endpoint
P0 — business-critical P1 — real operational risk P2 — quality / debuggability

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.

03

How the audit was conducted

Same checklist I'd run on anyone's automation, not specific to Make:

01Deduplication — how "seen this before" is actually decided
02Concurrent runs — two triggers, one record, at the same time
03Retries — ceiling, backoff, what actually gets re-attempted
04Observability — what's captured on failure, and where
05Error handling — does it recover, or redefine "success"?
06Dead & orphaned paths — writes and scenarios doing nothing
04

Findings

F-01P0

"Done" worked by coincidence, not by design

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.

worker: search(status=pending) → update(processing) → update(done) [no CRM call] finalizer: trigger(new record) → search → upsertAContact [never writes to Status]

Built four days apart, never wired together.

F-02P1

Original router permits duplicate processing under concurrent runs

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.

formula: {Send status} = "новый" — no existence check before ActionCreateRecord
F-03P1

Deduplication implemented via API error handling, not an explicit check

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.

onerror(ActionCreateRecord) → Update + Update + hubspotcrm:upsertAContact + Resume

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.

F-04P1

Superseded scenario left in the workspace and easy to reactivate

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.

Диспетчер — created 2026-02-14, isActive: false, no dedup guard Предобработка лидов и диспатчер — created 2026-02-21, isActive: false, dedup search added
F-05P2

The retry loop protects the wrong operation

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.

worker retries: its own update(done) call, attempts < 5 finalizer's upsertAContact: no onerror handler at all
F-06P2

Logging table schema exists but is never populated

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.

ActionCreateRecord(table=logs, record={}) — 3/3 live rows: log_id + created_at only
05

Risk, in plain terms

FindingWhat actually happens to the business
F-01 · P0A lead can silently never reach the CRM while every internal signal says "done" — no alert fires, because nothing is checking.
F-02 · P1The same lead gets worked, or contacted, twice — under load or a retry storm, not rarely.
F-03 · P1A rate limit or a network blip gets treated as "already a customer" — a genuinely new lead can be dropped instead of created.
F-04 · P1Re-enabling the wrong scenario during an incident brings back a race condition that was already fixed once.
F-05 · P2Failing calls retry at a fixed pace regardless of cause — no protection against repeatedly hitting an already-struggling API.
F-06 · P2When something does break, there's no trail. Every incident starts debugging from zero.
06

Fix plan

01Quick wins — days

  • Map the four log fields (stage, action, status, error_code) into the write that already runs — the schema already exists.
  • Delete v1 (Диспетчер), not just deactivate it, once v2 is confirmed as the only router.

02Hardening — structural

  • Replace the finalizer's error-driven dedup with an explicit existence check — the same pattern v2 already uses for routing.
  • Give the finalizer's HubSpot call its own retry with backoff and jitter — right now the operation actually worth retrying has none, while the worker retries a status write that doesn't need it.
  • Have the finalizer write its real outcome back onto the job it processed, so 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.

07

What a client receives

A one-page summary like this, handed over alongside the full findings above:

Audit Summary — Lead Pipeline (Make)

6 findings · 1 P0 · 3 P1 · 2 P2
P0 F-01  "Done" doesn't mean deliveredfix: hardening
P1 F-02  Duplicate processing under concurrencyfix: quick win
P1 F-03  Dedup via API error handlingfix: hardening
P1 F-04  Orphaned v1 scenario, reactivatablefix: quick win
P2 F-05  Retries with no backofffix: hardening
P2 F-06  Logging schema unusedfix: quick win
08

Two paths from here

Harden in place

Keep it in Make

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.

Rebuild in code

Move to an owned service

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.