[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6JDS99MMFEQ29BBGWA31SSM":3,"skill-same:01M6JDS99MMFEQ29BBGWA31SSM":84},{"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":30,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":62,"commitSha":63,"skillDirectory":64,"forksCount":65,"repositoryHtmlUrl":66,"repositoryHomepageUrl":67,"ownerHtmlUrl":68,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":69,"install":70,"resources":83},"01M6JDS99MMFEQ29BBGWA31SSM","airtable",null,"Airtable REST API via curl. Records CRUD, filters, upserts.","S3YED\u002Fappie-kit","S3YED","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F62743867?v=4","01M6JBKBRRVP6KMS0VWZ27T3R6","01M6JBKBX0KG30284PW1DBYJT1",14,"MIT","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fintegrations\u002Fairtable\u002FSKILL.md",[17,22,25],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":26,"categoryName":27,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[5,31,32,33,34,35,36,37,38,39,40,41,42,43],"web","database","https","productivity","REST","api","crud","curl","filters","community","rest api","via","records","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\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":40,"license":14,"version":49,"metadata":50,"description":7,"prerequisites":58},"1.1.0",{"hermes":51},{"tags":52,"homepage":57},[53,54,55,56],"Airtable","Productivity","Database","API","https:\u002F\u002Fairtable.com\u002Fdevelopers\u002Fweb\u002Fapi\u002Fintroduction",{"commands":59,"env_vars":60},[38],[61],"AIRTABLE_API_KEY","master","8ec37df6d71fb664ea19d32c079e7d3ff931a8e0","skills\u002Fintegrations\u002Fairtable",3,"https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit","https:\u002F\u002Fweblyfe.ai","https:\u002F\u002Fgithub.com\u002FS3YED","2026-08-27 01:54:02",{"source":8,"skillName":5,"commands":71,"prompt":81,"zipAvailable":82},[72,75,78],{"tool":73,"command":74},"npx","npx skills add S3YED\u002Fappie-kit --skill airtable",{"tool":76,"command":77},"bunx","bunx skills add S3YED\u002Fappie-kit --skill airtable",{"tool":79,"command":80},"pnpm","pnpm dlx skills add S3YED\u002Fappie-kit --skill airtable","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fintegrations\u002Fairtable\u002FSKILL.md",true,[],[85,117,150,176,204,229],{"skillNo":86,"name":87,"nameZh":6,"description":88,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":89,"categories":90,"keywords":98,"publishedAt":44},"01M6JFD4M6N4DSGC7TXEJ5R3J2","ai-music","AI music generation and audio tools: HeartMuLa lyrics-to-music generation, songwriting craft (+ Suno prompts), and audio spectrogram\u002Ffeature visualization.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fwethlete\u002Fai-music\u002FSKILL.md",[91,94,97],{"categoryNo":92,"categoryName":93,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000013","分析与可视化",{"categoryNo":95,"categoryName":96,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000016","图像与多媒体",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116],"music","ai music","humanizer","visualization","Tools","Audio","hermes","hermes agent","songwriting","suno","lyrics","prompts","heartmula","craft","generation","feature","ai music generation","music generation",{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":129,"publishedAt":44},"01M6JFCZE4XNFQFMNZDHKHT5V5","agentic-webdesign-design-system-ingestion","Use v0 Design Systems 2.0, Bolt Design System Agents, or Lovable to generate production UI from a client's real design system components — not generic Tailwind. Use when building a client website where brand consistency matters.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fweb-design\u002Fagentic-webdesign-design-system-ingestion\u002FSKILL.md",[123,126],{"categoryNo":124,"categoryName":125,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":127,"categoryName":128,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000032","设计师",[130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149],"design","agentic","design system","system","ingestion","webdesign","agentic webdesign design system ingestion","components","generate","design systems","website","ui","Tailwind","bolt","lovable","v0","design-system","wed","design system components","appie",{"skillNo":151,"name":152,"nameZh":6,"description":153,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":154,"categories":155,"keywords":158,"publishedAt":44},"01M6JFCVPX3913CFAS6QQJ33ZB","agentic-webdesign-agent-elements","Use 21st.dev Agent Elements — 25 shadcn\u002Fui components purpose-built for AI agent UIs. Tool cards (Bash, Edit, Search, Plan, Subagent, MCP), chat, input, streaming. Open source, shadcn-registry install, no runtime lock-in.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fweb-design\u002Fagentic-webdesign-agent-elements\u002FSKILL.md",[156,157],{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":127,"categoryName":128,"taxonomyType":28,"taxonomyTypeDesc":29},[131,159,135,160,161,162,137,163,164,165,166,167,168,169,170,171,172,173,174,175],"elements","agentic webdesign agent elements","MCP","ai agent","registry","search","streaming","open source","open","Bash","plan","subagent","edit","install","input","lock","source",{"skillNo":177,"name":178,"nameZh":6,"description":179,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":180,"categories":181,"keywords":186,"publishedAt":44},"01M6JFCQC7H65M9QJ6TRE71BS1","vercel-deployment","Deploy static sites and frontends to Vercel — handles both CLI and REST API paths, including the CLI token sanitization workaround.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Fvercel-deployment\u002FSKILL.md",[182,185],{"categoryNo":183,"categoryName":184,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000011","运维与部署",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[187,188,189,190,191,192,193,194,195,35,36,196,197,198,41,199,200,201,202,203],"deployment","vercel deployment","vercel","macos","cli","linux","windows","deploy","vercel deploy","site","deploy to vercel","token","host","sites","paths","sanitization","static site",{"skillNo":205,"name":206,"nameZh":6,"description":207,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":208,"categories":209,"keywords":211,"publishedAt":44},"01M6JFCM4SMTXBM5A01BK2YQG1","software-quality-workflows","Umbrella workflow for TDD, debugging, code simplification, spikes, pre-commit review, and debugger-assisted investigation.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Fsoftware-quality-workflows\u002FSKILL.md",[210],{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228],"workflows","quality","software","software quality workflows","review","tdd","workflow","commit","debugging","code","debugger","pre-commit","investigation","code simplification","user","simplification","for",{"skillNo":230,"name":231,"nameZh":6,"description":232,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":233,"categories":234,"keywords":242,"publishedAt":44},"01M6JFCGD0M6CEATY01W7YN6DB","react-component-refactoring","Safely refactor large React components into smaller, maintainable pieces using proven patterns","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Freact-component-refactoring\u002FSKILL.md",[235,238,241],{"categoryNo":236,"categoryName":237,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000007","代码审查",{"categoryNo":239,"categoryName":240,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[243,244,245,246,247,248,249,250,190,251,252,137,253,193,254,221,255,256,257,258],"react","component","refactoring","component refactoring","react component","react component refactoring","frontend","writing","safe","typescript","react components","modular","refactor","simplify","patterns","test-driven-development"]