[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71QDTHFSFNS60VJDYP1N4YA":3,"skill-same:01M71QDTHFSFNS60VJDYP1N4YA":79},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":27,"publishedAt":41,"linkOnly":42,"markdownContent":43,"markdownBody":44,"frontMatter":45,"refName":59,"commitSha":60,"skillDirectory":61,"forksCount":13,"repositoryHtmlUrl":62,"repositoryHomepageUrl":6,"ownerHtmlUrl":63,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":64,"install":65,"resources":78},"01M71QDTHFSFNS60VJDYP1N4YA","airtable",null,"Airtable REST API via curl. Records CRUD, filters, upserts.","gjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN","gjbvsjjvgm-sketch","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F278511911?v=4","01M71F3JXKZQ70H2KCHHTXKZDA","01M71PENEVFN19DBZFNHJPCHQ7",0,"MIT","https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fproductivity\u002Fairtable\u002FSKILL.md",[17,22],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":23,"categoryName":24,"taxonomyType":25,"taxonomyTypeDesc":26},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[5,28,29,30,31,32,33,34,35,36,37,38,39,40],"web","database","https","productivity","REST","api","crud","curl","filters","community","rest api","via","records","2026-11-10 13:02:02",false,"---\nname: airtable\ndescription: Airtable REST API via curl. Records CRUD, filters, upserts.\nversion: 1.1.0\nauthor: community\nlicense: MIT\nprerequisites:\n  env_vars: [AIRTABLE_API_KEY]\n  commands: [curl]\nmetadata:\n  hermes:\n    tags: [Airtable, Productivity, Database, API]\n    homepage: https:\u002F\u002Fairtable.com\u002Fdevelopers\u002Fweb\u002Fapi\u002Fintroduction\n---\n\n# Airtable — Bases, Tables & Records\n\nWork with Airtable's REST API directly via `curl` using the `terminal` tool. No MCP server, no OAuth flow, no Python SDK — just `curl` and a personal access token.\n\n## Prerequisites\n\n1. Create a **Personal Access Token (PAT)** at https:\u002F\u002Fairtable.com\u002Fcreate\u002Ftokens (tokens start with `pat...`).\n2. Grant these scopes (minimum):\n   - `data.records:read` — read rows\n   - `data.records:write` — create \u002F update \u002F delete rows\n   - `schema.bases:read` — list bases and tables\n3. **Important:** 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`.\n4. Store the token in `~\u002F.hermes\u002F.env` (or via `hermes setup`):\n   ```\n   AIRTABLE_API_KEY=pat_your_token_here\n   ```\n\n> Note: legacy `key...` API keys were deprecated Feb 2024. Only PATs and OAuth tokens work now.\n\n## API Basics\n\n- **Endpoint:** `https:\u002F\u002Fapi.airtable.com\u002Fv0`\n- **Auth header:** `Authorization: Bearer $AIRTABLE_API_KEY`\n- **All requests** use JSON (`Content-Type: application\u002Fjson` for any POST\u002FPATCH\u002FPUT body).\n- **Object IDs:** bases `app...`, tables `tbl...`, records `rec...`, fields `fld...`. IDs never change; names can. Prefer IDs in automations.\n- **Rate limit:** 5 requests\u002Fsec\u002Fbase. `429` → back off. Burst on a single base will be throttled.\n\nBase curl pattern:\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?maxRecords=5\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n`-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.\n\n## Field Types (request body shapes)\n\n| Field type | Write shape |\n|---|---|\n| Single line text | `\"Name\": \"hello\"` |\n| Long text | `\"Notes\": \"multi\\nline\"` |\n| Number | `\"Score\": 42` |\n| Checkbox | `\"Done\": true` |\n| Single select | `\"Status\": \"Todo\"` (name must already exist unless `typecast: true`) |\n| Multi-select | `\"Tags\": [\"urgent\", \"bug\"]` |\n| Date | `\"Due\": \"2026-04-01\"` |\n| DateTime (UTC) | `\"At\": \"2026-04-01T14:30:00.000Z\"` |\n| URL \u002F Email \u002F Phone | `\"Link\": \"https:\u002F\u002F…\"` |\n| Attachment | `\"Files\": [{\"url\": \"https:\u002F\u002F…\"}]` (Airtable fetches + rehosts) |\n| Linked record | `\"Owner\": [\"recXXXXXXXXXXXXXX\"]` (array of record IDs) |\n| User | `\"AssignedTo\": {\"id\": \"usrXXXXXXXXXXXXXX\"}` |\n\nPass `\"typecast\": true` at the top level of a create\u002Fupdate body to let Airtable auto-coerce values (e.g. create a new select option on the fly, convert `\"42\"` → `42`).\n\n## Common Queries\n\n### List bases the token can see\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002Fmeta\u002Fbases\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n### List tables + schema for a base\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002Fmeta\u002Fbases\u002F$BASE_ID\u002Ftables\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\nUse this BEFORE mutating — confirms exact field names and IDs, surfaces `options.choices` for select fields, and shows primary-field names.\n\n### List records (first 10)\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?maxRecords=10\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n### Get a single record\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\u002F$RECORD_ID\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n### Filter records (filterByFormula)\nAirtable formulas must be URL-encoded. Let Python stdlib do it — never hand-encode:\n```bash\nFORMULA=\"{Status}='Todo'\"\nENC=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$FORMULA\")\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?filterByFormula=$ENC&maxRecords=20\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\nUseful formula patterns:\n- Exact match: `{Email}='user@example.com'`\n- Contains: `FIND('bug', LOWER({Title}))`\n- Multiple conditions: `AND({Status}='Todo', {Priority}='High')`\n- Or: `OR({Owner}='alice', {Owner}='bob')`\n- Not empty: `NOT({Assignee}='')`\n- Date comparison: `IS_AFTER({Due}, TODAY())`\n\n### Sort + select specific fields\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?sort%5B0%5D%5Bfield%5D=Priority&sort%5B0%5D%5Bdirection%5D=asc&fields%5B%5D=Name&fields%5B%5D=Status\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\nSquare brackets in query params MUST be URL-encoded (`%5B` \u002F `%5D`).\n\n### Use a named view\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?view=Grid%20view&maxRecords=50\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\nViews apply their saved filter + sort server-side.\n\n## Common Mutations\n\n### Create a record\n```bash\ncurl -s -X POST \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"fields\":{\"Name\":\"New task\",\"Status\":\"Todo\",\"Priority\":\"High\"}}' | python3 -m json.tool\n```\n\n### Create up to 10 records in one call\n```bash\ncurl -s -X POST \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\n    \"typecast\": true,\n    \"records\": [\n      {\"fields\": {\"Name\": \"Task A\", \"Status\": \"Todo\"}},\n      {\"fields\": {\"Name\": \"Task B\", \"Status\": \"In progress\"}}\n    ]\n  }' | python3 -m json.tool\n```\nBatch endpoints are capped at **10 records per request**. For larger inserts, loop in batches of 10 with a short sleep to respect 5 req\u002Fsec\u002Fbase.\n\n### Update a record (PATCH — merges, preserves unchanged fields)\n```bash\ncurl -s -X PATCH \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\u002F$RECORD_ID\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"fields\":{\"Status\":\"Done\"}}' | python3 -m json.tool\n```\n\n### Upsert by a merge field (no ID needed)\n```bash\ncurl -s -X PATCH \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\n    \"performUpsert\": {\"fieldsToMergeOn\": [\"Email\"]},\n    \"records\": [\n      {\"fields\": {\"Email\": \"user@example.com\", \"Status\": \"Active\"}}\n    ]\n  }' | python3 -m json.tool\n```\n`performUpsert` creates records whose merge-field values are new, patches records whose merge-field values already exist. Great for idempotent syncs.\n\n### Delete a record\n```bash\ncurl -s -X DELETE \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\u002F$RECORD_ID\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n### Delete up to 10 records in one call\n```bash\ncurl -s -X DELETE \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?records%5B%5D=rec1&records%5B%5D=rec2\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n## Pagination\n\nList 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:\n\n```bash\nOFFSET=\"\"\nwhile :; do\n  URL=\"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?pageSize=100\"\n  [ -n \"$OFFSET\" ] && URL=\"$URL&offset=$OFFSET\"\n  RESP=$(curl -s \"$URL\" -H \"Authorization: Bearer $AIRTABLE_API_KEY\")\n  echo \"$RESP\" | python3 -c 'import json,sys; d=json.load(sys.stdin); [print(r[\"id\"], r[\"fields\"].get(\"Name\",\"\")) for r in d[\"records\"]]'\n  OFFSET=$(echo \"$RESP\" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get(\"offset\",\"\"))')\n  [ -z \"$OFFSET\" ] && break\ndone\n```\n\n## Typical Hermes Workflow\n\n1. **Confirm auth.** `curl -s -o \u002Fdev\u002Fnull -w \"%{http_code}\\n\" https:\u002F\u002Fapi.airtable.com\u002Fv0\u002Fmeta\u002Fbases -H \"Authorization: Bearer $AIRTABLE_API_KEY\"` — expect `200`.\n2. **Find the base.** List bases (step above) OR ask the user for the `app...` ID directly if the token lacks `schema.bases:read`.\n3. **Inspect the schema.** `GET \u002Fv0\u002Fmeta\u002Fbases\u002F$BASE_ID\u002Ftables` — cache the exact field names and primary-field name locally in the session before mutating anything.\n4. **Read before you write.** For \"update X where Y\", `filterByFormula` first to resolve the `rec...` ID, then `PATCH \u002Fv0\u002F$BASE_ID\u002F$TABLE\u002F$RECORD_ID`. Never guess record IDs.\n5. **Batch writes.** Combine related creates into one 10-record POST to stay under the 5 req\u002Fsec budget.\n6. **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.\n\n## Pitfalls\n\n- **`filterByFormula` MUST be URL-encoded.** Field names with spaces or non-ASCII also need encoding (`{My Field}` → `%7BMy%20Field%7D`). Use Python stdlib (pattern above) — never hand-escape.\n- **Empty fields are omitted from responses.** A missing `\"Assignee\"` key doesn't mean the field doesn't exist — it means this record's value is empty. Check the schema (step 3) before concluding a field is missing.\n- **PATCH vs PUT.** `PATCH` merges supplied fields into the record. `PUT` replaces the record entirely and clears any field you didn't include. Default to `PATCH`.\n- **Single-select options must exist.** Writing `\"Status\": \"Shipping\"` when `Shipping` isn't in the field's option list errors with `INVALID_MULTIPLE_CHOICE_OPTIONS` unless you pass `\"typecast\": true` (which auto-creates the option).\n- **Per-base token scoping.** A `403` on one base while another works means the token's Access list doesn't include that base — not a scope or auth issue. Send the user to https:\u002F\u002Fairtable.com\u002Fcreate\u002Ftokens to grant it.\n- **Rate limits are per base, not per token.** 5 req\u002Fsec on `baseA` and 5 req\u002Fsec on `baseB` is fine; 6 req\u002Fsec on `baseA` alone will throttle. Monitor the `Retry-After` header on `429`.\n\n## Important Notes for Hermes\n\n- **Always use the `terminal` tool with `curl`.** Do NOT use `web_extract` (it can't send auth headers) or `browser_navigate` (needs UI auth and is slow).\n- **`AIRTABLE_API_KEY` flows from `~\u002F.hermes\u002F.env` into the subprocess automatically** when this skill is loaded — no need to re-export it before each `curl` call.\n- **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 through `python3 urllib.parse.quote` before splicing into a URL.\n- **Pretty-print with `python3 -m json.tool`** (always present) rather than `jq` (optional). Only reach for `jq` when you need filtering\u002Fprojection.\n- **Pagination is per-page, not global.** Airtable's 100-record cap is a hard limit; there is no way to bump it. Loop with `offset` until the field is absent.\n- **Read the `errors` array** on non-2xx responses — Airtable returns structured error codes like `AUTHENTICATION_REQUIRED`, `INVALID_PERMISSIONS`, `MODEL_ID_NOT_FOUND`, `INVALID_MULTIPLE_CHOICE_OPTIONS` that tell you exactly what's wrong.\n","# Airtable — Bases, Tables & Records\n\nWork with Airtable's REST API directly via `curl` using the `terminal` tool. No MCP server, no OAuth flow, no Python SDK — just `curl` and a personal access token.\n\n## Prerequisites\n\n1. Create a **Personal Access Token (PAT)** at https:\u002F\u002Fairtable.com\u002Fcreate\u002Ftokens (tokens start with `pat...`).\n2. Grant these scopes (minimum):\n   - `data.records:read` — read rows\n   - `data.records:write` — create \u002F update \u002F delete rows\n   - `schema.bases:read` — list bases and tables\n3. **Important:** 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`.\n4. Store the token in `~\u002F.hermes\u002F.env` (or via `hermes setup`):\n   ```\n   AIRTABLE_API_KEY=pat_your_token_here\n   ```\n\n> Note: legacy `key...` API keys were deprecated Feb 2024. Only PATs and OAuth tokens work now.\n\n## API Basics\n\n- **Endpoint:** `https:\u002F\u002Fapi.airtable.com\u002Fv0`\n- **Auth header:** `Authorization: Bearer $AIRTABLE_API_KEY`\n- **All requests** use JSON (`Content-Type: application\u002Fjson` for any POST\u002FPATCH\u002FPUT body).\n- **Object IDs:** bases `app...`, tables `tbl...`, records `rec...`, fields `fld...`. IDs never change; names can. Prefer IDs in automations.\n- **Rate limit:** 5 requests\u002Fsec\u002Fbase. `429` → back off. Burst on a single base will be throttled.\n\nBase curl pattern:\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?maxRecords=5\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n`-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.\n\n## Field Types (request body shapes)\n\n| Field type | Write shape |\n|---|---|\n| Single line text | `\"Name\": \"hello\"` |\n| Long text | `\"Notes\": \"multi\\nline\"` |\n| Number | `\"Score\": 42` |\n| Checkbox | `\"Done\": true` |\n| Single select | `\"Status\": \"Todo\"` (name must already exist unless `typecast: true`) |\n| Multi-select | `\"Tags\": [\"urgent\", \"bug\"]` |\n| Date | `\"Due\": \"2026-04-01\"` |\n| DateTime (UTC) | `\"At\": \"2026-04-01T14:30:00.000Z\"` |\n| URL \u002F Email \u002F Phone | `\"Link\": \"https:\u002F\u002F…\"` |\n| Attachment | `\"Files\": [{\"url\": \"https:\u002F\u002F…\"}]` (Airtable fetches + rehosts) |\n| Linked record | `\"Owner\": [\"recXXXXXXXXXXXXXX\"]` (array of record IDs) |\n| User | `\"AssignedTo\": {\"id\": \"usrXXXXXXXXXXXXXX\"}` |\n\nPass `\"typecast\": true` at the top level of a create\u002Fupdate body to let Airtable auto-coerce values (e.g. create a new select option on the fly, convert `\"42\"` → `42`).\n\n## Common Queries\n\n### List bases the token can see\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002Fmeta\u002Fbases\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n### List tables + schema for a base\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002Fmeta\u002Fbases\u002F$BASE_ID\u002Ftables\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\nUse this BEFORE mutating — confirms exact field names and IDs, surfaces `options.choices` for select fields, and shows primary-field names.\n\n### List records (first 10)\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?maxRecords=10\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n### Get a single record\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\u002F$RECORD_ID\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n### Filter records (filterByFormula)\nAirtable formulas must be URL-encoded. Let Python stdlib do it — never hand-encode:\n```bash\nFORMULA=\"{Status}='Todo'\"\nENC=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$FORMULA\")\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?filterByFormula=$ENC&maxRecords=20\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\nUseful formula patterns:\n- Exact match: `{Email}='user@example.com'`\n- Contains: `FIND('bug', LOWER({Title}))`\n- Multiple conditions: `AND({Status}='Todo', {Priority}='High')`\n- Or: `OR({Owner}='alice', {Owner}='bob')`\n- Not empty: `NOT({Assignee}='')`\n- Date comparison: `IS_AFTER({Due}, TODAY())`\n\n### Sort + select specific fields\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?sort%5B0%5D%5Bfield%5D=Priority&sort%5B0%5D%5Bdirection%5D=asc&fields%5B%5D=Name&fields%5B%5D=Status\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\nSquare brackets in query params MUST be URL-encoded (`%5B` \u002F `%5D`).\n\n### Use a named view\n```bash\ncurl -s \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?view=Grid%20view&maxRecords=50\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\nViews apply their saved filter + sort server-side.\n\n## Common Mutations\n\n### Create a record\n```bash\ncurl -s -X POST \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"fields\":{\"Name\":\"New task\",\"Status\":\"Todo\",\"Priority\":\"High\"}}' | python3 -m json.tool\n```\n\n### Create up to 10 records in one call\n```bash\ncurl -s -X POST \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\n    \"typecast\": true,\n    \"records\": [\n      {\"fields\": {\"Name\": \"Task A\", \"Status\": \"Todo\"}},\n      {\"fields\": {\"Name\": \"Task B\", \"Status\": \"In progress\"}}\n    ]\n  }' | python3 -m json.tool\n```\nBatch endpoints are capped at **10 records per request**. For larger inserts, loop in batches of 10 with a short sleep to respect 5 req\u002Fsec\u002Fbase.\n\n### Update a record (PATCH — merges, preserves unchanged fields)\n```bash\ncurl -s -X PATCH \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\u002F$RECORD_ID\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"fields\":{\"Status\":\"Done\"}}' | python3 -m json.tool\n```\n\n### Upsert by a merge field (no ID needed)\n```bash\ncurl -s -X PATCH \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\n    \"performUpsert\": {\"fieldsToMergeOn\": [\"Email\"]},\n    \"records\": [\n      {\"fields\": {\"Email\": \"user@example.com\", \"Status\": \"Active\"}}\n    ]\n  }' | python3 -m json.tool\n```\n`performUpsert` creates records whose merge-field values are new, patches records whose merge-field values already exist. Great for idempotent syncs.\n\n### Delete a record\n```bash\ncurl -s -X DELETE \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE\u002F$RECORD_ID\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n### Delete up to 10 records in one call\n```bash\ncurl -s -X DELETE \"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?records%5B%5D=rec1&records%5B%5D=rec2\" \\\n  -H \"Authorization: Bearer $AIRTABLE_API_KEY\" | python3 -m json.tool\n```\n\n## Pagination\n\nList 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:\n\n```bash\nOFFSET=\"\"\nwhile :; do\n  URL=\"https:\u002F\u002Fapi.airtable.com\u002Fv0\u002F$BASE_ID\u002F$TABLE?pageSize=100\"\n  [ -n \"$OFFSET\" ] && URL=\"$URL&offset=$OFFSET\"\n  RESP=$(curl -s \"$URL\" -H \"Authorization: Bearer $AIRTABLE_API_KEY\")\n  echo \"$RESP\" | python3 -c 'import json,sys; d=json.load(sys.stdin); [print(r[\"id\"], r[\"fields\"].get(\"Name\",\"\")) for r in d[\"records\"]]'\n  OFFSET=$(echo \"$RESP\" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get(\"offset\",\"\"))')\n  [ -z \"$OFFSET\" ] && break\ndone\n```\n\n## Typical Hermes Workflow\n\n1. **Confirm auth.** `curl -s -o \u002Fdev\u002Fnull -w \"%{http_code}\\n\" https:\u002F\u002Fapi.airtable.com\u002Fv0\u002Fmeta\u002Fbases -H \"Authorization: Bearer $AIRTABLE_API_KEY\"` — expect `200`.\n2. **Find the base.** List bases (step above) OR ask the user for the `app...` ID directly if the token lacks `schema.bases:read`.\n3. **Inspect the schema.** `GET \u002Fv0\u002Fmeta\u002Fbases\u002F$BASE_ID\u002Ftables` — cache the exact field names and primary-field name locally in the session before mutating anything.\n4. **Read before you write.** For \"update X where Y\", `filterByFormula` first to resolve the `rec...` ID, then `PATCH \u002Fv0\u002F$BASE_ID\u002F$TABLE\u002F$RECORD_ID`. Never guess record IDs.\n5. **Batch writes.** Combine related creates into one 10-record POST to stay under the 5 req\u002Fsec budget.\n6. **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.\n\n## Pitfalls\n\n- **`filterByFormula` MUST be URL-encoded.** Field names with spaces or non-ASCII also need encoding (`{My Field}` → `%7BMy%20Field%7D`). Use Python stdlib (pattern above) — never hand-escape.\n- **Empty fields are omitted from responses.** A missing `\"Assignee\"` key doesn't mean the field doesn't exist — it means this record's value is empty. Check the schema (step 3) before concluding a field is missing.\n- **PATCH vs PUT.** `PATCH` merges supplied fields into the record. `PUT` replaces the record entirely and clears any field you didn't include. Default to `PATCH`.\n- **Single-select options must exist.** Writing `\"Status\": \"Shipping\"` when `Shipping` isn't in the field's option list errors with `INVALID_MULTIPLE_CHOICE_OPTIONS` unless you pass `\"typecast\": true` (which auto-creates the option).\n- **Per-base token scoping.** A `403` on one base while another works means the token's Access list doesn't include that base — not a scope or auth issue. Send the user to https:\u002F\u002Fairtable.com\u002Fcreate\u002Ftokens to grant it.\n- **Rate limits are per base, not per token.** 5 req\u002Fsec on `baseA` and 5 req\u002Fsec on `baseB` is fine; 6 req\u002Fsec on `baseA` alone will throttle. Monitor the `Retry-After` header on `429`.\n\n## Important Notes for Hermes\n\n- **Always use the `terminal` tool with `curl`.** Do NOT use `web_extract` (it can't send auth headers) or `browser_navigate` (needs UI auth and is slow).\n- **`AIRTABLE_API_KEY` flows from `~\u002F.hermes\u002F.env` into the subprocess automatically** when this skill is loaded — no need to re-export it before each `curl` call.\n- **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 through `python3 urllib.parse.quote` before splicing into a URL.\n- **Pretty-print with `python3 -m json.tool`** (always present) rather than `jq` (optional). Only reach for `jq` when you need filtering\u002Fprojection.\n- **Pagination is per-page, not global.** Airtable's 100-record cap is a hard limit; there is no way to bump it. Loop with `offset` until the field is absent.\n- **Read the `errors` array** on non-2xx responses — Airtable returns structured error codes like `AUTHENTICATION_REQUIRED`, `INVALID_PERMISSIONS`, `MODEL_ID_NOT_FOUND`, `INVALID_MULTIPLE_CHOICE_OPTIONS` that tell you exactly what's wrong.\n",{"name":5,"author":37,"license":14,"version":46,"metadata":47,"description":7,"prerequisites":55},"1.1.0",{"hermes":48},{"tags":49,"homepage":54},[50,51,52,53],"Airtable","Productivity","Database","API","https:\u002F\u002Fairtable.com\u002Fdevelopers\u002Fweb\u002Fapi\u002Fintroduction",{"commands":56,"env_vars":57},[35],[58],"AIRTABLE_API_KEY","main","6746276ba18cb774853eb61b3b9399658bc9bc5c","core\u002Fskills\u002Fproductivity\u002Fairtable","https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN","https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch","2026-04-28 17:01:03",{"source":8,"skillName":5,"commands":66,"prompt":76,"zipAvailable":77},[67,70,73],{"tool":68,"command":69},"npx","npx skills add gjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN --skill airtable",{"tool":71,"command":72},"bunx","bunx skills add gjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN --skill airtable",{"tool":74,"command":75},"pnpm","pnpm dlx skills add gjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN --skill airtable","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fproductivity\u002Fairtable\u002FSKILL.md",true,[],[80,117,142,159,178,197],{"skillNo":81,"name":82,"nameZh":6,"description":83,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":96,"publishedAt":41},"01M71QG27G1R2H2ZWEG4S0WTZT","writing-plans","Write implementation plans: bite-sized tasks, paths, code.","https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fsoftware-development\u002Fwriting-plans\u002FSKILL.md",[86,89,92,93],{"categoryNo":87,"categoryName":88,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":90,"categoryName":91,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":23,"categoryName":24,"taxonomyType":25,"taxonomyTypeDesc":26},{"categoryNo":94,"categoryName":95,"taxonomyType":25,"taxonomyTypeDesc":26},"01JBSEEDCAT000000000000036","内容创作者",[97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116],"writing","writing plans","plans","test-driven","test-driven-development","superpowers","design","workflow","code","tasks","subagent","code-review","review","documentation","write","paths","planning","development","test","implementation",{"skillNo":118,"name":101,"nameZh":6,"description":119,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":120,"categories":121,"keywords":128,"publishedAt":41},"01M71QFZYRTJXMAE551MRAKZ14","TDD: enforce RED-GREEN-REFACTOR, tests before code.","https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fsoftware-development\u002Ftest-driven-development\u002FSKILL.md",[122,125],{"categoryNo":123,"categoryName":124,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000007","代码审查",{"categoryNo":126,"categoryName":127,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000008","测试与质量",[129,114,115,130,131,132,133,102,97,134,105,107,135,136,137,138,139,99,140,141],"test driven development","driven","green","red-green-refactor","testing","debugging","systematic","refactor","systematic-debugging","enforce","tests","tdd","quality",{"skillNo":143,"name":137,"nameZh":6,"description":144,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":148,"publishedAt":41},"01M71QFWR5Q8EYEQ6MCS3SM6MC","4-phase root cause debugging: understand bugs before fixing.","https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fsoftware-development\u002Fsystematic-debugging\u002FSKILL.md",[147],{"categoryNo":126,"categoryName":127,"taxonomyType":20,"taxonomyTypeDesc":21},[149,134,135,100,150,151,101,102,97,107,152,153,114,154,99,155,115,156,157,158],"systematic debugging","problem","problem-solving","phase","bugs","root cause","root-cause","investigation","understand","hermes",{"skillNo":160,"name":161,"nameZh":6,"description":162,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":163,"categories":164,"keywords":170,"publishedAt":41},"01M71QFS9S04MD8JP747W5WM0N","subagent-driven-development","Execute plans via delegate_task subagents (2-stage review).","https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fsoftware-development\u002Fsubagent-driven-development\u002FSKILL.md",[165,166,169],{"categoryNo":90,"categoryName":91,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":167,"categoryName":168,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":94,"categoryName":95,"taxonomyType":25,"taxonomyTypeDesc":26},[171,107,114,130,100,101,172,173,102,97,104,105,108,109,174,175,99,176,177,115],"subagent driven development","delegate","delegation","subagents","parallel","stage","task",{"skillNo":179,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":182,"categories":183,"keywords":186,"publishedAt":41},"01M71QFQ1DBCVAZ511MXFE6T2V","requesting-code-review","Pre-commit review: security scan, quality gates, auto-fix.","https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fsoftware-development\u002Frequesting-code-review\u002FSKILL.md",[184,185],{"categoryNo":123,"categoryName":124,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":23,"categoryName":24,"taxonomyType":25,"taxonomyTypeDesc":26},[187,188,105,109,100,101,189,190,102,191,97,192,193,107,108,194,195,114,196,99],"code review","requesting code review","fix","auto-fix","auto","security scan","commit","verification","security","scan",{"skillNo":198,"name":199,"nameZh":6,"description":200,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":201,"categories":202,"keywords":204,"publishedAt":41},"01M71QFM7KWM540YR67FRN807Q","python-debugpy","Debug Python: pdb REPL + debugpy remote (DAP).","https:\u002F\u002Fgithub.com\u002Fgjbvsjjvgm-sketch\u002FWORM_V2_SOVEREIGN\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fsoftware-development\u002Fpython-debugpy\u002FSKILL.md",[203],{"categoryNo":23,"categoryName":24,"taxonomyType":25,"taxonomyTypeDesc":26},[205,206,207,208,209,210,211,212,134,135,213,214,215,137,216,217,218,219,158,220],"debugpy","python debugpy","python","dap","inspect","post-mortem","pdb","remote","debugger","node","commands","debug","post","breakpoints","node-inspect","tui"]