Upsert Records
airtable.upsert_records
ActionFiles & storageconnection required
Calls the Airtable API with the connection's credentials.
Update Airtable records that match on chosen columns and create the ones that do not, using Airtable's performUpsert.
Connection
- Required
- Yes — the node will not save without one
- Auth type
- Bearer token
- Providers
- Airtable
- Secret shape
- the Airtable access token
- Resolved by
- config.connection_id, else node.connection_id
Configuration
6 fields, 4 required. A dotted path is a nested object and name[] an array of objects — the editor renders the same tree.
| Field | Type | Required | Default | Constraints | Example |
|---|---|---|---|---|---|
base_id Airtable base id (app…), from the base's API docs URL or airtable.list_bases. | string | Yes | — | — | appAbCdEfGhIjKlMn |
table Table id (tbl…) or table name. An id survives a rename; a name does not. | string | Yes | — | — | Tasks |
records The records to write, as a JSON array of {"fields": {column: value}} objects. An entry may carry an 'id' to update that row directly. | array | Yes | — | array of object | [{"fields":{"Email":"ada@example.com","Status":"Open"}}] |
fields_to_merge_on The 1 to 3 columns Airtable matches an incoming record against to decide update-or-create, as a JSON array of field names or ids. | array | Yes | — | array of string | ["Email"] |
typecast Let Airtable convert strings into select options, dates and numbers. Off means a value must already match the column's type exactly. | boolean | No | true | — | — |
replace Send Airtable's destructive form (PUT): a matched row has every column the payload does not mention cleared. | boolean | No | false | — | — |
Example configuration
Every value below is the contract’s own example or default — nothing invented.
{
"base_id": "appAbCdEfGhIjKlMn",
"fields_to_merge_on": [
"Email"
],
"records": [
{
"fields": {
"Email": "ada@example.com",
"Status": "Open"
}
}
],
"replace": false,
"table": "Tasks",
"typecast": true
}Output
One shape.
result
| Field | Type | Example |
|---|---|---|
records[].id The Airtable record id (rec…). | string | — |
records[].fields The record's cell values, keyed by field name. | object | — |
records[].createdTime When the record was created (ISO 8601). | string | — |
count How many records this call returned. | number | — |
created_record_ids Ids of the records Airtable created. | array | — |
updated_record_ids Ids of the records Airtable updated. | array | — |
created_count How many records were created. | number | — |
updated_count How many records were updated. | number | — |
batches How many 10-record requests were sent to Airtable. | number | — |
{
"batches": 0,
"count": 0,
"created_count": 0,
"created_record_ids": [],
"records": [
{
"createdTime": "",
"fields": {},
"id": ""
}
],
"updated_count": 0,
"updated_record_ids": []
}Notes
- Airtable matches on at most 3 fields, and a record matching more than one existing row fails that request rather than guessing.
- Airtable caps every batch write at 10 records per request. The batch nodes chunk for you and report how many calls they made in 'batches'; the calls are sequential and not a transaction, so a failure part-way leaves the earlier chunks committed and says how many got through.
- typecast is what lets a plain string be written into a single-select, date or number column. It is on by default, matching airtable.create_record.
- Reads page on an opaque 'offset' token, not a page number: feed the offset a call returns back into the next call while 'has_more' is true.
- The base/table/field nodes use Airtable's metadata API, which needs the 'schema.bases:read' (or ':write') scope on the personal access token. A token minted only for 'data.records:*' gets a 403 from them.
See Upsert Records run in a workflow.
In a technical demo we add the Airtable connection, drop the node on the canvas and let Workflow Health pre-flight it before the first real run.