Airtable is where the work happens and the CRM is where the revenue is recorded, so sooner or later somebody asks for the two to talk to each other. The request sounds small. Six weeks later there are three "Acme Corp" companies in HubSpot, a deal that reopens itself every night, and an automation loop that burned through a month of API quota in a weekend.
None of that is inevitable. Two-way integration between Airtable and a CRM like HubSpot, Salesforce, or Pipedrive is a solved problem, but only if you make four decisions before you connect anything: what the system of record is for each field, what the shared identifier is, which direction each field flows, and how you stop echoes. This tutorial walks through all four and then builds a working sync.
Step 0: decide what you are actually integrating
"Sync Airtable and HubSpot" is not a specification. Write down the object map first, on one page:
| Airtable table | CRM object | Direction | Volume/month |
|---|---|---|---|
Companies | Company | CRM → Airtable | ~120 |
Contacts | Contact | CRM → Airtable | ~400 |
Projects | Deal | two-way (narrow) | ~60 |
Time entries | — | Airtable only | ~2,000 |
Most integrations that go wrong tried to make everything two-way. In practice the majority of objects have an obvious owner: marketing and sales data belongs to the CRM, delivery data belongs to Airtable. Genuinely two-way fields are usually a handful — deal stage, delivery status, a project link — and each one needs a rule for what happens when both sides change.
Step 1: pick the system of record, field by field
Make a table of the fields you intend to move and mark the owner of each:
Company name,Domain,Owner,Lifecycle stage— CRM owns. Airtable displays them read-only.Delivery status,Go-live date,Assigned consultant,Hours used— Airtable owns. The CRM receives them.Deal stage— CRM owns, but Airtable may push one specific transition (Delivered).
Then enforce it in the UI. In Airtable, fields the CRM owns should be visibly read-only: prefix them (CRM · Owner), group them in a field section, and hide them from the interface forms people actually edit. Nothing causes duplicate-truth bugs faster than a team happily editing a field that gets overwritten every fifteen minutes. Our data-modelling guide covers how to keep those imported fields from polluting your schema.
Step 2: establish a shared key
This is the step that prevents duplicates, and it is the one most integrations skip.
Every record you sync needs a stable identifier that both systems agree on. Names do not qualify — "Acme Corp", "Acme Corp.", and "ACME" are three different strings and one company. Use the CRM's own record ID.
Add to each synced Airtable table:
CRM ID— single line text, holds the HubSpot object ID or Salesforce 18-character ID.CRM last synced— date/time, written by the integration.Sync state— single select:Linked,Pending,Error,Do not sync.
And in the CRM, add a custom property Airtable record ID (a text field) on the corresponding object. Now each side can find its counterpart in one lookup instead of guessing by name.
The matching logic for any incoming record becomes:
- If the payload carries an
Airtable record ID, update that record. Done. - Otherwise, search Airtable for a record whose
CRM IDequals the incoming object ID. If found, update it and write the Airtable record ID back to the CRM. - Otherwise, try a normalised business key — lowercased email for contacts, root domain for companies. If exactly one match, link them and write both IDs.
- Otherwise, create a new record with
Sync state=Pendingfor a human to eyeball.
Step 3 is where duplicates are actually prevented, so normalise properly: strip www., lowercase, trim, drop +tags from emails. A formula field makes the key visible and searchable:
LOWER(
TRIM(
REGEX_REPLACE({Website}, "^https?://(www\\.)?|/.*$", "")
)
)
The de-duplication patterns in our data quality tutorial apply directly here; an integration is just a very fast way of creating duplicates you did not check for.
Step 3: choose the transport
Three realistic options, in increasing order of control.
Native CRM connectors. HubSpot and Salesforce both have marketplace or partner connectors for Airtable, and Airtable's own integrations cover some paths. Fastest to stand up, weakest on conditional logic and conflict handling. Fine for one-directional reference data (CRM companies into an Airtable lookup table).
An iPaaS — Make, Zapier, Workato, n8n. The default choice for most mid-size builds. You get retries, error notifications, and a visual map of the field mapping, which matters because the person maintaining this in a year is not you. Watch the per-operation pricing: a two-way sync on 400 contacts can be 20,000+ operations a month once you count polling. Our Zapier vs Make vs native comparison has the decision matrix.
Direct API, event-driven. Airtable webhooks on one side, CRM webhooks on the other, a small service in the middle. The most work, the least ongoing cost, and the only option that gives you full control over conflict resolution. Choose it when volume is high or the rules are genuinely bespoke.
Whichever you pick, the logic in steps 2 and 4 is the same. The transport is the least interesting decision.
Step 4: stop the echo
An echo is the loop where Airtable updates the CRM, the CRM's change event fires, the integration writes the value back to Airtable, Airtable's change event fires, and so on. At best you waste quota. At worst two automations fight over a field forever.
Three defences, use at least two:
Compare before writing. Never write a value that is already equal to the incoming one. This one line kills most loops, because an update that changes nothing generates no change event on either platform. In a Make or script step:
if (String(record.getCellValue('CRM · Owner') ?? '') !== String(incoming.owner ?? '')) {
await table.updateRecordAsync(record, { 'CRM · Owner': incoming.owner });
}
Mark the origin. Write a Last updated by field (Sync or Human) alongside every integration write, and have the outbound trigger ignore records whose last write came from the sync. In Airtable, a "when record updated" trigger watching specific fields plus a condition on Last updated by is Human is usually enough.
Watch narrow fields. Configure the trigger to fire only on the fields that genuinely need to propagate, not on "any field". Most accidental loops start with a trigger set to watch the whole table, which includes the timestamp the integration itself just wrote.
For true conflicts — both sides changed Deal stage inside the sync window — pick a rule and document it. Last-write-wins is defensible if the timestamps are trustworthy. System-of-record-wins is safer: the CRM value survives, and the rejected Airtable change is written to a Sync conflicts table for review. Silently discarding a user's edit with no trace is the option that destroys trust in the integration.
Step 5: respect the rate limits
Airtable's REST API allows 5 requests per second per base, and returns 429 when you exceed it; batch endpoints handle up to 10 records per request. HubSpot and Salesforce have their own daily and per-second caps, and Salesforce in particular counts API calls against an org-wide daily allowance that other tools are also consuming.
Practical consequences:
- Batch. Ten records per Airtable write call, not one call per record.
- Never loop a full-table resync on a schedule "just to be safe". Sync deltas — records modified since
CRM last synced. - Back off on 429 rather than retrying immediately, and cap the number of retries. The retry helper in our scripting guide transfers straight over.
- Do the initial backfill as a deliberate one-off, off-hours, in chunks, with matching (step 2) running in dry-run mode first so you can inspect what it would have merged.
Step 6: make it observable
An integration you cannot see is an integration you will find out about from a client. Build the same instrumentation you would give any critical automation:
- A
Sync logtable: object, direction, action (created/updated/skipped/error), record link, message, timestamp. - A view grouped by action, filtered to the last seven days, on an interface page.
- An alert when the error count in an hour exceeds a threshold, or — more importantly — when the success count for a given direction falls to zero for a day. A sync that has stopped looks exactly like a quiet week.
- A weekly count of records with
Sync state=Pending. If nobody resolves them, the queue is your duplicate backlog waiting to happen.
Step 7: test with a sandbox, not production
Duplicate the base, point the integration at a CRM sandbox (Salesforce) or a test portal/test records (HubSpot), and run the five cases that matter:
- New record in the CRM → appears once in Airtable.
- The same record updated twice → still one record, no loop.
- New record in Airtable → appears in the CRM with both IDs written back.
- Simultaneous edits on both sides of a two-way field → conflict rule applies, log written.
- CRM temporarily returns 500 → run fails loudly, retries, no half-written record.
Only then cut over, and keep the first week's sync log where you can see it. The change-management workflow we use for schema edits applies to integrations too: additive first, reversible, documented.
When not to integrate at all
If the CRM data is only ever read in Airtable, a nightly one-way export into a reference table is a tenth of the work and cannot create duplicates. If fewer than twenty records a month cross the boundary, a button in an interface that pushes one record on demand is honest and maintainable. Two-way sync earns its complexity when volume is real, both teams edit, and latency matters. Otherwise it is a maintenance liability with a dashboard.
BaseBrainers builds and maintains Airtable–CRM integrations for agencies and operations teams — see our work on CRM systems in Airtable and Airtable API integration. If you have a sync that is producing duplicates or quietly stopped weeks ago, our project rescue team untangles them; get in touch.