Airtable REST API via curl: Records CRUD, Filters, Upserts for Hermes Agent
Airtable REST API via curl. Records CRUD, filters, upserts.
Written by Neura Market from the official Hermes Agent documentation for Airtable. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationThis skill lets Hermes Agent read and write Airtable records through the REST API using plain curl commands. You get full CRUD, filtered queries, batch operations, and upserts without any SDK, MCP server, or OAuth flow. Reach for it when you need an autonomous agent to sync data, update task statuses, or run scheduled lookups against Airtable bases.
What it does
Hermes Agent uses the terminal tool to send HTTP requests to Airtable's API. The skill handles authentication through a personal access token stored in the environment, so every curl call is pre-authorized. You can list bases and tables, inspect schemas, read records with filters and sorting, create single or batch records, update or upsert by a merge field, and delete records. The skill also includes a pagination loop for fetching more than 100 records and a workflow for safely resolving record IDs before mutations.
Before you start
- Create a Personal Access Token (PAT) at https://airtable.com/create/tokens. Tokens start with
pat.... - Grant these minimum scopes:
data.records:readdata.records:writeschema.bases:read
- In the same token UI, add each base you want to access to the token's Access list. PATs are scoped per-base. A valid token on the wrong base returns
403. - Store the token in
${HERMES_HOME:-~/.hermes}/.env(or viahermes setup):
AIRTABLE_API_KEY=pat_your_token_here
Note: legacy
key...API keys were deprecated Feb 2024. Only PATs and OAuth tokens work now.
The skill is bundled with Hermes Agent (installed by default) and works on Linux, macOS, and Windows. The AIRTABLE_API_KEY environment variable flows from the .env file into subprocesses automatically when the skill loads. You do not need to re-export it before each curl call.
API Basics
- Endpoint:
https://api.airtable.com/v0 - Auth header:
Authorization: Bearer $AIRTABLE_API_KEY - All requests use JSON (
Content-Type: application/jsonfor any POST/PATCH/PUT body). - Object IDs: bases
app..., tablestbl..., recordsrec..., fieldsfld.... IDs never change; names can. Prefer IDs in automations. - Rate limit: 5 requests/sec/base.
429means back off. Burst on a single base will be throttled.
Base curl pattern:
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?maxRecords=5" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
-s suppresses curl's progress bar. Keep it set for every call so the tool output stays clean for Hermes. Pipe through python3 -m json.tool (always present) or jq (if installed) for readable JSON.
Field Types (request body shapes)
| Field type | Write shape |
|---|---|
| Single line text | "Name": "hello" |
| Long text | "Notes": "multi\nline" |
| Number | "Score": 42 |
| Checkbox | "Done": true |
| Single select | "Status": "Todo" (name must already exist unless typecast: true) |
| Multi-select | "Tags": ["urgent", "bug"] |
| Date | "Due": "2026-04-01" |
| DateTime (UTC) | "At": "2026-04-01T14:30:00.000Z" |
| URL / Email / Phone | "Link": "https://…" |
| Attachment | "Files": [{"url": "https://…"}] (Airtable fetches + rehosts) |
| Linked record | "Owner": ["recXXXXXXXXXXXXXX"] (array of record IDs) |
| User | "AssignedTo": {"id": "usrXXXXXXXXXXXXXX"} |
Pass "typecast": true at the top level of a create/update body to let Airtable auto-coerce values (e.g. create a new select option on the fly, convert "42" to 42).
Common Queries
List bases the token can see
curl -s "https://api.airtable.com/v0/meta/bases" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
List tables + schema for a base
curl -s "https://api.airtable.com/v0/meta/bases/$BASE_ID/tables" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
Use this BEFORE mutating. It confirms exact field names and IDs, surfaces options.choices for select fields, and shows primary-field names.
List records (first 10)
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?maxRecords=10" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
Get a single record
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE/$RECORD_ID" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
Filter records (filterByFormula)
Airtable formulas must be URL-encoded. Let Python stdlib do it. Never hand-encode:
FORMULA="{Status}='Todo'"
ENC=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$FORMULA")
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?filterByFormula=$ENC&maxRecords=20" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
Useful formula patterns:
- Exact match:
{Email}='user@example.com' - Contains:
FIND('bug', LOWER({Title})) - Multiple conditions:
AND({Status}='Todo', {Priority}='High') - Or:
OR({Owner}='alice', {Owner}='bob') - Not empty:
NOT({Assignee}='') - Date comparison:
IS_AFTER({Due}, TODAY())
Sort + select specific fields
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?sort%5B0%5D%5Bfield%5D=Priority&sort%5B0%5D%5Bdirection%5D=asc&fields%5B%5D=Name&fields%5B%5D=Status" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
Square brackets in query params MUST be URL-encoded (%5B / %5D).
Use a named view
curl -s "https://api.airtable.com/v0/$BASE_ID/$TABLE?view=Grid%20view&maxRecords=50" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
Views apply their saved filter + sort server-side.
Common Mutations
Create a record
curl -s -X POST "https://api.airtable.com/v0/$BASE_ID/$TABLE" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fields":{"Name":"New task","Status":"Todo","Priority":"High"}}' | python3 -m json.tool
Create up to 10 records in one call
curl -s -X POST "https://api.airtable.com/v0/$BASE_ID/$TABLE" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"typecast": true,
"records": [
{"fields": {"Name": "Task A", "Status": "Todo"}},
{"fields": {"Name": "Task B", "Status": "In progress"}}
]
}' | python3 -m json.tool
Batch endpoints are capped at 10 records per request. For larger inserts, loop in batches of 10 with a short sleep to respect 5 req/sec/base.
Update a record (PATCH merges, preserves unchanged fields)
curl -s -X PATCH "https://api.airtable.com/v0/$BASE_ID/$TABLE/$RECORD_ID" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fields":{"Status":"Done"}}' | python3 -m json.tool
Upsert by a merge field (no ID needed)
curl -s -X PATCH "https://api.airtable.com/v0/$BASE_ID/$TABLE" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"performUpsert": {"fieldsToMergeOn": ["Email"]},
"records": [
{"fields": {"Email": "user@example.com", "Status": "Active"}}
]
}' | python3 -m json.tool
performUpsert creates records whose merge-field values are new, patches records whose merge-field values already exist. Great for idempotent syncs.
Delete a record
curl -s -X DELETE "https://api.airtable.com/v0/$BASE_ID/$TABLE/$RECORD_ID" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
Delete up to 10 records in one call
curl -s -X DELETE "https://api.airtable.com/v0/$BASE_ID/$TABLE?records%5B%5D=rec1&records%5B%5D=rec2" \
-H "Authorization: Bearer $AIRTABLE_API_KEY" | python3 -m json.tool
Pagination
List endpoints return at most 100 records per page. If the response includes "offset": "...", pass it back on the next call. Loop until the field is absent:
OFFSET=""
while :; do
URL="https://api.airtable.com/v0/$BASE_ID/$TABLE?pageSize=100"
[ -n "$OFFSET" ] && URL="$URL&offset=$OFFSET"
RESP=$(curl -s "$URL" -H "Authorization: Bearer $AIRTABLE_API_KEY")
echo "$RESP" | python3 -c 'import json,sys; d=json.load(sys.stdin); [print(r["id"], r["fields"].get("Name","")) for r in d["records"]]'
OFFSET=$(echo "$RESP" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("offset",""))')
[ -z "$OFFSET" ] && break
done
Typical Hermes Workflow
- Confirm auth.
curl -s -o /dev/null -w "%{http_code}\n" https://api.airtable.com/v0/meta/bases -H "Authorization: Bearer $AIRTABLE_API_KEY", expect200. - Find the base. List bases (step above) OR ask the user for the
app...ID directly if the token lacksschema.bases:read. - Inspect the schema.
GET /v0/meta/bases/$BASE_ID/tables, cache the exact field names and primary-field name locally in the session before mutating anything. - Read before you write. For "update X where Y",
filterByFormulafirst to resolve therec...ID, thenPATCH /v0/$BASE_ID/$TABLE/$RECORD_ID. Never guess record IDs. - Batch writes. Combine related creates into one 10-record POST to stay under the 5 req/sec budget.
- Destructive ops. Deletions can't be undone via API. If the user says "delete all Xs", echo back the filter + record count and confirm before firing.
Pitfalls
filterByFormulaMUST be URL-encoded. Field names with spaces or non-ASCII also need encoding ({My Field}becomes%7BMy%20Field%7D). Use Python stdlib (pattern above). Never hand-escape.- Empty fields are omitted from responses. A missing
"Assignee"key does not mean the field does not exist. It means this record's value is empty. Check the schema (step 3) before concluding a field is missing. - PATCH vs PUT.
PATCHmerges supplied fields into the record.PUTreplaces the record entirely and clears any field you did not include. Default toPATCH. - Single-select options must exist. Writing
"Status": "Shipping"whenShippingis not in the field's option list errors withINVALID_MULTIPLE_CHOICE_OPTIONSunless you pass"typecast": true(which auto-creates the option). - Per-base token scoping. A
403on one base while another works means the token's Access list does not include that base. It is not a scope or auth issue. Send the user to https://airtable.com/create/tokens to grant it. - Rate limits are per base, not per token. 5 req/sec on
baseAand 5 req/sec onbaseBis fine. 6 req/sec onbaseAalone will throttle. Monitor theRetry-Afterheader on429.
Important Notes for Hermes
- Always use the
terminaltool withcurl. Do NOT useweb_extract(it cannot send auth headers) orbrowser_navigate(needs UI auth and is slow). AIRTABLE_API_KEYflows from${HERMES_HOME:-~/.hermes}/.envinto the subprocess automatically when this skill is loaded. No need to re-export it before eachcurlcall.- Escape curly braces in formulas carefully. In a heredoc body,
{Status}is literal. In a shell argument,{Status}is safe outside{...}brace-expansion context. But pass dynamic strings throughpython3 urllib.parse.quotebefore splicing into a URL. - Pretty-print with
python3 -m json.tool(always present) rather thanjq(optional). Only reach forjqwhen you need filtering or projection. - Pagination is per-page, not global. Airtable's 100-record cap is a hard limit. There is no way to bump it. Loop with
offsetuntil the field is absent. - Read the
errorsarray on non-2xx responses. Airtable returns structured error codes likeAUTHENTICATION_REQUIRED,INVALID_PERMISSIONS,MODEL_ID_NOT_FOUND,INVALID_MULTIPLE_CHOICE_OPTIONSthat tell you exactly what is wrong.
When not to use it
If you need real-time sync or event-driven updates, the REST API is not suitable. Airtable does not expose webhooks through this API. For complex relational queries or large data exports, consider Airtable's official SDK or a dedicated ETL tool. This skill is for direct, agent-driven CRUD operations.
Limits and gotchas
- Rate limit: 5 requests per second per base. Burst on a single base will be throttled.
- Batch endpoints: capped at 10 records per request.
- Pagination: 100 records per page maximum.
- Token scoping: per-base, not per-token. A
403on one base while another works means the token's Access list does not include that base. - Empty fields are omitted from responses. A missing key does not mean the field does not exist.
- Single-select options must exist unless
typecast: trueis passed. - Deletions cannot be undone via API.
What pairs with this
This skill is part of the Productivity category in Hermes Agent. It pairs naturally with other bundled skills for file operations, web scraping, and data transformation. For more complex automation, combine it with Hermes Agent's built-in scheduling and workflow capabilities.