[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6DDCR3TJ0MXRNAYDC2JS4EA":3,"skill-same:01M6DDCR3TJ0MXRNAYDC2JS4EA":94},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":62,"commitSha":63,"skillDirectory":64,"forksCount":65,"repositoryHtmlUrl":66,"repositoryHomepageUrl":6,"ownerHtmlUrl":67,"ownerBlogUrl":6,"ownerDescription":68,"updatedAt":69,"install":70,"resources":83},"01M6DDCR3TJ0MXRNAYDC2JS4EA","airtable",null,"Airtable REST API via curl. Records CRUD, filters, upserts.","zapabob\u002FSkills","zapabob","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F130830376?v=4","01M6DBMYZAGRTFQDSQYW3W764P","峯岸 亮","01M6DBMZ3GKDSJ01NZ9XTZD7JD",2,"MIT","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Farchive\u002Fskill-md-reviewed\u002Fsha256\u002F28\u002F28253f1e20c4d250725eb7f475c886952a4c6a1ef43ef6900c2dc0e75ebb40e6\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[5,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"macos","linux","web","windows","database","https","productivity","REST","api","crud","curl","filters","community","rest api","2026-11-10 10:12:30",false,"---\nname: airtable\ndescription: Airtable REST API via curl. Records CRUD, filters, upserts.\nversion: 1.1.0\nauthor: community\nlicense: MIT\nplatforms: [linux, macos, windows]\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 `${HERMES_HOME:-~\u002F.hermes}\u002F.env` (or via `hermes setup`):\n   ```\n   AIRTABLE_API_KEY=[REDACTED]\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\" | python -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 `python -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\" | python -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\" | python -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\" | python -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\" | python -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=$(python -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\" | python -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\" | python -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\" | python -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\"}}' | python -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  }' | python -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\"}}' | python -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  }' | python -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\" | python -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\" | python -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\" | python -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\" | python -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 `${HERMES_HOME:-~\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 `python urllib.parse.quote` before splicing into a URL.\n- **Pretty-print with `python -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 `${HERMES_HOME:-~\u002F.hermes}\u002F.env` (or via `hermes setup`):\n   ```\n   AIRTABLE_API_KEY=[REDACTED]\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\" | python -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 `python -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\" | python -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\" | python -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\" | python -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\" | python -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=$(python -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\" | python -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\" | python -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\" | python -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\"}}' | python -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  }' | python -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\"}}' | python -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  }' | python -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\" | python -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\" | python -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\" | python -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\" | python -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 `${HERMES_HOME:-~\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 `python urllib.parse.quote` before splicing into a URL.\n- **Pretty-print with `python -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":41,"license":15,"version":48,"metadata":49,"platforms":57,"description":7,"prerequisites":58},"1.1.0",{"hermes":50},{"tags":51,"homepage":56},[52,53,54,55],"Airtable","Productivity","Database","API","https:\u002F\u002Fairtable.com\u002Fdevelopers\u002Fweb\u002Fapi\u002Fintroduction",[30,29,32],{"commands":59,"env_vars":60},[39],[61],"AIRTABLE_API_KEY","main","0d1434f89f4bc6b07e9d272accd8c1013768820f","archive\u002Fskill-md-reviewed\u002Fsha256\u002F28\u002F28253f1e20c4d250725eb7f475c886952a4c6a1ef43ef6900c2dc0e75ebb40e6",0,"https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills","https:\u002F\u002Fgithub.com\u002Fzapabob","The Open University of JAPAN　2019-now\r\n\r\nTokyo University of Science　2007-2010","2026-10-26 16:29:01",{"source":8,"skillName":5,"commands":71,"prompt":81,"zipAvailable":82},[72,75,78],{"tool":73,"command":74},"npx","npx skills add zapabob\u002FSkills --skill airtable",{"tool":76,"command":77},"bunx","bunx skills add zapabob\u002FSkills --skill airtable",{"tool":79,"command":80},"pnpm","pnpm dlx skills add zapabob\u002FSkills --skill airtable","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Farchive\u002Fskill-md-reviewed\u002Fsha256\u002F28\u002F28253f1e20c4d250725eb7f475c886952a4c6a1ef43ef6900c2dc0e75ebb40e6\u002FSKILL.md",true,[84,90],{"path":85,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":88,"sourceHtmlUrl":89},"provenance\u002Foriginal.SKILL.md","REFERENCE","参考文档",6013,"https:\u002F\u002Fgithub.com\u002Fstrmt7\u002Fai_skills_collection_benchmarked\u002Fblob\u002Fmain\u002Fincluded\u002Frepaired\u002Fskills\u002Fby-category\u002Fscience-research-data-analysis\u002Flatest-release-community\u002Fparallel-web\u002Fprovenance\u002Foriginal.SKILL.md",{"path":91,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":92,"sourceHtmlUrl":93},"repair.json",1070,"https:\u002F\u002Fgithub.com\u002Fstrmt7\u002Fai_skills_collection_benchmarked\u002Fblob\u002Fmain\u002Fincluded\u002Frepaired\u002Fskills\u002Fby-category\u002Fscience-research-data-analysis\u002Flatest-release-community\u002Fparallel-web\u002Frepair.json",[95,120,155,182,209,226],{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":106,"publishedAt":43},"01M6GJTE1GESGGF9K028Y7753E","xurl","X\u002FTwitter via xurl CLI: post, search, DM, media, v2 API.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fxurl\u002Fvariants\u002Fhermes-agent\u002FSKILL.md",[101,102,105],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":103,"categoryName":104,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},[97,107,29,108,109,30,32,110,111,34,112,37,113,114,115,116,62,117,118,119],"openclaw","cli","github","official","search","social-media","Media","hermes","hermes agent","twitter","social","via","post",{"skillNo":121,"name":122,"nameZh":6,"description":123,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":124,"categories":125,"keywords":137,"publishedAt":43},"01M6GJT8S1PXQ8WPXT1HSNBWH2","writing-plans","Write implementation plans: bite-sized tasks, paths, code.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fwriting-plans\u002Fvariants\u002Fhermes-agent\u002FSKILL.md",[126,129,132,133,134],{"categoryNo":127,"categoryName":128,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":130,"categoryName":131,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":103,"categoryName":104,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},{"categoryNo":135,"categoryName":136,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000036","内容创作者",[138,139,140,141,29,142,30,143,32,144,145,146,147,148,149,150,151,152,153,154],"writing","writing plans","plans","design","review","workflow","documentation","tasks","development","code","planning","code-review","subagent","write","superpowers","paths","test-driven-development",{"skillNo":156,"name":157,"nameZh":6,"description":158,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":159,"categories":160,"keywords":164,"publishedAt":43},"01M6GJST0H5X7WHBJ3BZK0DVKH","whisper","OpenAI's general-purpose speech recognition model. Supports 99 languages, transcription, translation to English, and language identification. Six model sizes from tiny (39M params) to large (1550M params). Use for speech-to-text, podcast transcription, or multilingual audio processing. Best for robust, multilingual ASR.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fwhisper\u002Fvariants\u002Fhermes-agent-optional\u002FSKILL.md",[161],{"categoryNo":162,"categoryName":163,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体",[157,29,30,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181],"openai","speech","multimodal","Transformers","ASR","Audio","text","model","speech recognition","use","general","multilingual","translation","english","identification","processing","Audio Processing",{"skillNo":183,"name":184,"nameZh":6,"description":185,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":186,"categories":187,"keywords":191,"publishedAt":43},"01M6GJSMRQ8EETZTYMG72P70EA","weights-and-biases","W&B: log ML experiments, sweeps, model registry, dashboards.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fweights-and-biases\u002Fvariants\u002Fhermes-agent\u002FSKILL.md",[188],{"categoryNo":189,"categoryName":190,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000013","分析与可视化",[192,29,193,194,32,195,196,197,198,199,200,172,201,202,203,204,205,206,207,208],"weights and biases","tensorflow","experiments","pytorch","visualization","registry","real-time","tuning","dashboards","experiment","log","time","Experiment Tracking","Model Registry","Hyperparameter Tuning","WandB","Real-Time Visualization",{"skillNo":210,"name":211,"nameZh":6,"description":212,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":213,"categories":214,"keywords":216,"publishedAt":43},"01M6GJS19KDPRY112JTR326S06","watchers","Poll RSS, JSON APIs, and GitHub with watermark dedup.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fwatchers\u002Fvariants\u002Fhermes-agent-optional\u002FSKILL.md",[215],{"categoryNo":103,"categoryName":104,"taxonomyType":21,"taxonomyTypeDesc":22},[211,29,217,109,30,218,219,220,221,222,223,114,115,224,225],"terminal","automation","http","monitoring","cron","json","rss","devops","with",{"skillNo":227,"name":228,"nameZh":6,"description":229,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":230,"categories":231,"keywords":234,"publishedAt":43},"01M6GJRHJD2A6Q830SWF9W48BC","serving-llms-vllm","vLLM: high-throughput LLM serving, OpenAI API, quantization.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fvllm\u002Fvariants\u002Fhermes-agent\u002FSKILL.md",[232,233],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},[235,236,237,30,238,165,239,240,241,168,242,37,243,244,245,246,247,248,249,250],"llms","vLLM","serving llms vllm","research","openai api","tensor","Inference Serving","Production","llm serving","High Throughput","PagedAttention","Continuous Batching","Quantization","Tensor Parallelism","high-throughput","throughput"]