Install
$ agentstack add skill-furkangonel-cowrangler-airtable ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Airtable REST API SOP
Read, create, update, and delete Airtable records. Covers auth, filtering, pagination, batch operations, and field type reference.
When to Use
- User wants to read records from an Airtable base
- User wants to create or update Airtable records
- User wants to filter or sort records by field values
- User wants to automate data entry or sync into Airtable
- User wants to delete or patch records in bulk
Part 1 — Auth Setup
1. Get a Personal Access Token
- Go to https://airtable.com/create/tokens
- Click Create new token
- Name it, add scopes:
data.records:read,data.records:write,schema.bases:read - Add bases or grant access to all bases
- Copy the token (shown only once)
export AIRTABLE_TOKEN="patXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
Or store in ~/.cowrangler/credentials.env:
AIRTABLE_TOKEN=patXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2. Find Your Base and Table IDs
Base ID is in the URL: https://airtable.com/appXXXXXXXXXXXXXX/... → appXXXXXXXXXXXXXX
Table ID or name — use the table name directly in the URL (URL-encoded) or get the ID from the metadata API:
AIRTABLE_TOKEN="${AIRTABLE_TOKEN:-$(grep '^AIRTABLE_TOKEN=' ~/.cowrangler/credentials.env 2>/dev/null | cut -d= -f2 | tr -d '\n\r')}"
BASE_ID="appXXXXXXXXXXXXXX"
# List all tables in a base
curl -s "https://api.airtable.com/v0/meta/bases/$BASE_ID/tables" \
-H "Authorization: Bearer $AIRTABLE_TOKEN" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
for t in data['tables']:
print(f\"{t['id']} {t['name']}\")
"
Shell Helper
airtable_get() {
# Usage: airtable_get "appXXX/TableName?filterByFormula=..."
curl -s "https://api.airtable.com/v0/$1" \
-H "Authorization: Bearer $AIRTABLE_TOKEN"
}
airtable_post() {
# Usage: airtable_post "appXXX/TableName" '{"records":[...]}'
curl -s -X POST "https://api.airtable.com/v0/$1" \
-H "Authorization: Bearer $AIRTABLE_TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
}
airtable_patch() {
# Usage: airtable_patch "appXXX/TableName" '{"records":[...]}'
curl -s -X PATCH "https://api.airtable.com/v0/$1" \
-H "Authorization: Bearer $AIRTABLE_TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
}
Part 2 — Reading Records
List Records (basic)
import os, json, urllib.request, urllib.parse
TOKEN = os.environ["AIRTABLE_TOKEN"]
BASE_ID = "appXXXXXXXXXXXXXX"
TABLE = "Tasks" # table name or ID
def airtable_request(method, path, body=None):
url = f"https://api.airtable.com/v0/{BASE_ID}/{urllib.parse.quote(path)}"
headers = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
data = json.dumps(body).encode() if body else None
req = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(req) as r:
return json.load(r)
result = airtable_request("GET", TABLE)
for rec in result["records"]:
print(rec["id"], rec["fields"])
Filter Records with Formula
import urllib.parse
# Airtable formula syntax (similar to Excel)
formula = "{Status} = 'In Progress'"
params = urllib.parse.urlencode({
"filterByFormula": formula,
"fields[]": ["Name", "Status", "Due Date", "Assignee"], # only these fields
"sort[0][field]": "Due Date",
"sort[0][direction]": "asc",
"maxRecords": 100,
})
url = f"https://api.airtable.com/v0/{BASE_ID}/{urllib.parse.quote(TABLE)}?{params}"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {TOKEN}"})
with urllib.request.urlopen(req) as r:
result = json.load(r)
for rec in result["records"]:
f = rec["fields"]
print(f"{rec['id']} {f.get('Name', ''):30s} {f.get('Status', ''):15s} {f.get('Due Date', '')}")
Common Formula Examples
# Filter by single-line text
{Name} = 'Alice'
# Filter by checkbox
{Done} = TRUE()
# Filter by date (before today)
IS_BEFORE({Due Date}, TODAY())
# Filter records where a field is not empty
{Assignee} != ''
# Multiple conditions
AND({Status} = 'In Progress', {Priority} = 'High')
# OR condition
OR({Status} = 'To Do', {Status} = 'In Progress')
# Search in text
FIND('keyword', {Description}) > 0
Paginate Through All Records
def list_all_records(table, formula=None, fields=None):
records = []
offset = None
while True:
params = {"pageSize": 100}
if formula:
params["filterByFormula"] = formula
if fields:
for i, f in enumerate(fields):
params[f"fields[{i}]"] = f
if offset:
params["offset"] = offset
query = urllib.parse.urlencode(params)
url = f"https://api.airtable.com/v0/{BASE_ID}/{urllib.parse.quote(table)}?{query}"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {TOKEN}"})
with urllib.request.urlopen(req) as r:
data = json.load(r)
records.extend(data["records"])
offset = data.get("offset")
if not offset:
break
return records
all_tasks = list_all_records("Tasks", formula="{Status} != 'Done'")
print(f"Total active tasks: {len(all_tasks)}")
Part 3 — Creating Records
Create a Single Record
result = airtable_request("POST", TABLE, {
"records": [
{
"fields": {
"Name": "Set up monitoring dashboard",
"Status": "To Do",
"Priority": "High",
"Due Date": "2026-06-01",
"Notes": "Use Grafana. See ticket #342.",
}
}
]
})
created = result["records"][0]
print(f"Created: {created['id']} {created['fields']['Name']}")
Batch Create Records (up to 10 per request)
items = [
{"Name": "Write unit tests", "Status": "To Do", "Priority": "High"},
{"Name": "Update documentation", "Status": "To Do", "Priority": "Medium"},
{"Name": "Deploy to staging", "Status": "To Do", "Priority": "High"},
{"Name": "Review pull requests", "Status": "In Progress", "Priority": "Medium"},
]
# Airtable accepts max 10 records per request
BATCH_SIZE = 10
for i in range(0, len(items), BATCH_SIZE):
batch = items[i:i + BATCH_SIZE]
result = airtable_request("POST", TABLE, {
"records": [{"fields": f} for f in batch]
})
for rec in result["records"]:
print(f" Created {rec['id']}: {rec['fields']['Name']}")
Part 4 — Updating Records
Update a Single Record (PATCH — only specified fields)
RECORD_ID = "recXXXXXXXXXXXXXX"
result = airtable_request("PATCH", TABLE, {
"records": [
{
"id": RECORD_ID,
"fields": {
"Status": "Done",
"Completed Date": "2026-05-18",
}
}
]
})
Batch Update Records (up to 10)
updates = [
{"id": "recAAAAAAAAAAAAAAAA", "fields": {"Status": "Done"}},
{"id": "recBBBBBBBBBBBBBBBB", "fields": {"Status": "In Progress", "Assignee": ["recUSER1"]}},
]
for i in range(0, len(updates), 10):
batch = updates[i:i + 10]
airtable_request("PATCH", TABLE, {"records": batch})
Upsert Records (create or update based on a key field)
# Available in Airtable API with performUpsert
result = airtable_request("PATCH", TABLE, {
"performUpsert": {"fieldsToMergeOn": ["Email"]},
"records": [
{"fields": {"Email": "alice@example.com", "Name": "Alice", "Status": "Active"}},
{"fields": {"Email": "bob@example.com", "Name": "Bob", "Status": "Inactive"}},
]
})
print(f"Created: {len(result['createdRecords'])} Updated: {len(result['updatedRecords'])}")
Part 5 — Deleting Records
import urllib.parse
# Delete up to 10 records per request
record_ids = ["recAAAAAAAAAAAAAAAA", "recBBBBBBBBBBBBBBBB"]
params = "&".join(f"records[]={rid}" for rid in record_ids)
url = f"https://api.airtable.com/v0/{BASE_ID}/{urllib.parse.quote(TABLE)}?{params}"
req = urllib.request.Request(
url,
headers={"Authorization": f"Bearer {TOKEN}"},
method="DELETE"
)
with urllib.request.urlopen(req) as r:
result = json.load(r)
print(f"Deleted: {[r['id'] for r in result['records']]}")
Field Type Reference
| Airtable type | Write value format | |---------------|--------------------| | Single line text | "value" | | Long text / Notes | "multi\nline\ntext" | | Number | 42 or 3.14 | | Currency | 99.99 | | Checkbox | true or false | | Single select | "Option Name" | | Multiple select | ["Option A", "Option B"] | | Date | "2026-05-18" | | Date+time | "2026-05-18T14:00:00.000Z" | | Email | "user@example.com" | | URL | "https://example.com" | | Phone | "+1 555-000-0000" | | Attachment | [{"url": "https://..."}] | | Linked record | ["recXXXXXXXXXXXXXX"] (array of record IDs) | | User | [{"id": "usrXXXXXXXXXXXXXX"}] | | Formula / Rollup | Read-only — cannot write | | Auto-number | Read-only — cannot write |
Checklist
- [ ]
AIRTABLE_TOKENset in environment (Personal Access Token, not legacy API key) - [ ] Token scopes include
data.records:readanddata.records:write - [ ] Base ID extracted from URL (
appXXXXXXXXXXXXXX) - [ ] Table name URL-encoded when used in requests
- [ ] Batch operations limited to 10 records per request
- [ ] Pagination implemented for tables with > 100 records (use
offset) - [ ] Formula fields and auto-number fields not written to
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: furkangonel
- Source: furkangonel/cowrangler
- License: MIT
- Homepage: https://cowrangler.com
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.