Skip to content

Airtable

Paid add-on. Every submission writes a row into an Airtable table, to feed an operational tracker or an internal portal.

This document describes what the module guarantees and what it refuses. The user guide gives the short version, under “Writing to an Airtable base”.

What sets this module apart from the others in the series

It does not write into a CRM or a sending platform, but into a base that people open and read. Three things follow: the values are the ones a human would read, the columns are designated by their id, and the module converts for itself instead of letting Airtable guess.

The three sections that follow are the module’s three decisions.

Columns go out by id, never by name

Airtable accepts both in a write. The name is what you read on screen; the id — fldXXXXXXXXXXXXXX — is what does not change.

Renaming a column is the most ordinary gesture in the world in Airtable. It is also what breaks a mapping by name: Airtable answers UNKNOWN_FIELD_NAME, a definitive refusal, and the submission is lost — for one word changed in an interface, by somebody who did not know a form was writing there.

The id is therefore what gets saved, and the readable name is kept only for display.

The same goes for the base and the table.

The module converts; Airtable does not guess

Airtable offers a typecast flag which makes any string acceptable for any column. For a single-select, it does not merely convert: it creates the missing option.

Put on a public form, that means a visitor typing whatever they like into a free field mapped to a single-select adds whatever they like to the options of the client’s base. That is no longer writing a row, it is modifying the schema — from outside, and without anybody having asked for it.

The flag therefore stays off. The conversion happens here, from the column’s type, recorded with the mapping at the moment of the setting:

Column typeWhat is sent
Text, long text, email, URL, phone, single selectthe value as it is
Number, currency, percent, rating, durationa number — the decimal comma and grouping spaces are read
Checkboxtrue, except non, no, false, off, 0
Multiple selectthe value re-split on commas
DateYYYY-MM-DD — the DD/MM/YYYY form is recognised before anything else
Date and timeISO 8601 in universal time

What does not convert is omitted, never guessed. Omitting costs an empty column; guessing costs a 422 refusal on the whole record, because Airtable does not keep the valid fields of a request one field of which is not. A misread date would therefore lose the complete submission rather than a column.

An empty value is not sent either: on an update, it would overwrite what the row already carried.

If every value is omitted, nothing is written: an empty row in the base informs nobody and is indistinguishable from an outage.

The merge key is what makes the write idempotent

With no key, every send creates. A request whose response is lost on the way is replayed, and leaves two rows in the base. The local unique key can do nothing about it: the first call did succeed at Airtable.

With a key, the write is a PATCH carrying performUpsert: Airtable looks for the row on that column, updates it if it exists, creates it otherwise.

The key is therefore not a comfort option, and the panel says so in plain words rather than letting the duplicates be discovered. It must be a mapped column — merging on a column you do not write would never find anything — and of a type Airtable allows: text, long text, email, URL, phone, number, single or multiple select, date. A checkbox or a computed column has the whole request refused, and is therefore not offered.

When the key is set but empty on a submission, the module creates instead of merging, and writes it to the log: merging on an empty column would find the first row whose column is empty, which is to say any of them.

The connection screen shows the number of records created rather than merged. That is the figure you come looking for when a base fills with duplicates.

A personal token rather than an application

Both are possible; the screen recommends the first, and not out of convenience.

A personal access token is created in a minute at airtable.com/create/tokens, does not expire, and carries the scopes you give it — here data.records:write and schema.bases:read. There is nothing to renew.

An OAuth token lasts an hour and renews itself, rotating the refresh token: Airtable invalidates the old pair as soon as it returns a new one. That is precisely the trap, and it is detailed below.

OAuth is still offered for the case where it is imposed: a token that must not belong to one particular person’s account, because that person will leave the company before the site does.

Renewal never happens in twos

Two tasks renewing at the same time produce this: the first obtains a fresh pair and saves it; the second presents the token it had read earlier, already dead, is refused — and would erase the valid pair the first had just saved.

The authorisation would be lost for good. No retry finds it again: a human has to come back and reauthorise. And it would happen precisely when the site is doing well, since it takes two closely spaced submissions to get there.

Renewal is therefore taken under a lock, and that lock is an insert settled by the unique key of the options table — never a read followed by a write, which would leave between the two the interval in which the other process reads the same thing. It is the procedure WordPress itself uses to lock its updates.

Whoever does not get the lock does not start waiting: it re-reads the connection, and if the other has finished, the token is fresh. Otherwise it gives up for this time, and the retry takes care of it. Putting a scheduled task to sleep would immobilise the process running all the others.

A personal token does not go through any of that: it has no rotation, no lock, and no possible failure.

PKCE, which Airtable makes compulsory

The other integrations in this series do without it. Airtable requires it: the authorisation request carries the SHA-256 fingerprint of a randomly drawn secret, and the exchange carries the secret itself. An intercepted authorisation code is therefore worth nothing without that secret, which never left this server.

The verifier is kept in the same per-user transient as the state, and destroyed on use.

The client secret, meanwhile, travels in the Authorization: Basic header — that is the form Airtable imposes; placed in the body, it is refused.

Five requests per second per base

That is the documented limit, and it is low. A send consumes only one — two at most with a token renewal. A 429 asks you to wait thirty seconds; the retry waits a minute, then five, then thirty. It is not an outage, it is a queue.

What is not attempted, and what is retried

Nothing is scheduled without a base, without a table, without a mapped column, without a connection, or if the form’s condition is not met. A submission marked as spam or sent to the trash does not go out.

Definitive refusals are not replayed: unknown column, missing select option, incompatible type, deleted table, token without the write scope. Replaying them would give the same no three times over.

429 and 5xx are, at 1, 5 then 30 minutes.

An Airtable outage never blocks the submission. It is recorded, confirmed to the visitor and notified by email before this module is called on.

Resending

The resend button replays the same write: with a key, it updates the row; without a key, it creates a second one. The screen says so before you press.

What the log does not contain

Neither the request nor the response. The first carries the submission’s values, and in the second Airtable returns the record as it wrote it. Recording them would make the log a second copy, which would go off into backups and exports — where it would read as a technical trace when it describes a person.

The address, on the other hand, is recorded in full: a base and a table are not personal data, and they are the first two things to check when nothing is arriving.

Schema

slf_airtable_records: one row per submission bound for Airtable, with the base, the table, the id of the record returned, whether it was created or merged, the state, the reason for the last problem and the number of attempts.

The connection lives in an option: the kind of connection, the client id and the account id in clear — they are not secrets — the tokens and the client secret encrypted.

Erasing a submission erases its tracking row and its log. The row already written in Airtable stays: it belongs to the client’s base, and deleting it from here would be deciding in their place — on a base this module does not even have the read scope for.

What V1 does not do

No reading of records, no deletion, no attachment, no link to another table, no computed column, no reverse synchronisation.

An attachment is described by a publicly downloadable address, and a link by the id of a record that would first have to be looked up: both ask for more than a form has to give. A form writes; it does not hold the base.