Back to skills
SkillHub ClubAnalyze Data & AIFull StackBackendData / AI

airtable

Read Airtable bases, tables, and records directly via the Airtable API. Use when you need spreadsheet/database data from Airtable. Calls api.airtable.com directly with no third-party proxy.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,135
Hot score
99
Updated
March 20, 2026
Overall rating
C0.0
Composite score
0.0
Best-practice grade
A92.4

Install command

npx @skill-hub/cli install openclaw-skills-native-airtable

Repository

openclaw/skills

Skill path: skills/codeninja23/native-airtable

Read Airtable bases, tables, and records directly via the Airtable API. Use when you need spreadsheet/database data from Airtable. Calls api.airtable.com directly with no third-party proxy.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Backend, Data / AI.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install airtable into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding airtable to shared team environments
  • Use airtable for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: airtable
description: "Read Airtable bases, tables, and records directly via the Airtable API. Use when you need spreadsheet/database data from Airtable. Calls api.airtable.com directly with no third-party proxy."
metadata:
  openclaw:
    requires:
      env:
        - AIRTABLE_PAT
      bins:
        - python3
    primaryEnv: AIRTABLE_PAT
    files:
      - "scripts/*"
---

# Airtable

Read bases, tables, and records directly via `api.airtable.com`.

## Setup (one-time)

1. Go to https://airtable.com/create/tokens
2. Click **+ Create new token**, give it a name
3. Add scopes:
   - `data.records:read`
   - `schema.bases:read`
4. Under **Access**, select which bases to grant access to (or all)
5. Copy the token — it starts with `pat`
6. Set the environment variable:
   ```
   AIRTABLE_PAT=pat_your_token_here
   ```

## Commands

### List all accessible bases
```bash
python3 /mnt/skills/user/airtable/scripts/airtable.py list-bases
```

### List tables in a base
```bash
python3 /mnt/skills/user/airtable/scripts/airtable.py list-tables <base_id>
```

### List records in a table
```bash
python3 /mnt/skills/user/airtable/scripts/airtable.py list-records <base_id> "Table Name"
python3 /mnt/skills/user/airtable/scripts/airtable.py list-records <base_id> "Table Name" --limit 50
```

### Filter records with a formula
```bash
python3 /mnt/skills/user/airtable/scripts/airtable.py list-records <base_id> "Tasks" --filter "{Status}='Done'"
python3 /mnt/skills/user/airtable/scripts/airtable.py list-records <base_id> "Contacts" --filter "NOT({Email}='')"
```

### Filter to specific fields only
```bash
python3 /mnt/skills/user/airtable/scripts/airtable.py list-records <base_id> "People" --fields "Name,Email,Company"
```

### Use a specific view
```bash
python3 /mnt/skills/user/airtable/scripts/airtable.py list-records <base_id> "Tasks" --view "Active Tasks"
```

### Get a specific record
```bash
python3 /mnt/skills/user/airtable/scripts/airtable.py get-record <base_id> "Table Name" <record_id>
```

### Search records
```bash
python3 /mnt/skills/user/airtable/scripts/airtable.py search-records <base_id> "Contacts" "Smith"
python3 /mnt/skills/user/airtable/scripts/airtable.py search-records <base_id> "Contacts" "[email protected]" --field "Email"
```

## Notes

- Free plan: unlimited bases, 1,000 records per base. API reads work on free.
- Base IDs start with `app`, record IDs start with `rec`.
- Table names are case-sensitive and must match exactly. Use quotes if the name has spaces.
- Airtable deprecated old API keys in Feb 2024. Only Personal Access Tokens (PAT) work now.


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "codeninja23",
  "slug": "native-airtable",
  "displayName": "Native Airtable",
  "latest": {
    "version": "0.1.0",
    "publishedAt": 1771927570214,
    "commit": "https://github.com/openclaw/skills/commit/2a897a29ffa74c9c0651510e4b97f608e0c4ae5b"
  },
  "history": []
}

```

### scripts/airtable.py

```python
#!/usr/bin/env python3
"""
Airtable API — calls api.airtable.com directly.
No third-party proxy.
"""
import argparse
import json
import os
import sys
import urllib.parse
import urllib.request

BASE_URL = "https://api.airtable.com/v0"
META_URL = "https://api.airtable.com/v0/meta"


# ── Auth ──────────────────────────────────────────────────────────────────────

def get_token():
    token = os.environ.get("AIRTABLE_PAT")
    if not token:
        print("Error: AIRTABLE_PAT not set.", file=sys.stderr)
        print("Get a token at: https://airtable.com/create/tokens", file=sys.stderr)
        sys.exit(1)
    return token


def request(method, url, params=None, body=None):
    token = get_token()
    if params:
        url += "?" + urllib.parse.urlencode(params)

    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(url, data=data, method=method)
    req.add_header("Authorization", f"Bearer {token}")
    req.add_header("Content-Type", "application/json")

    try:
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read())
    except urllib.error.HTTPError as e:
        print(f"Error {e.code}: {e.read().decode()}", file=sys.stderr)
        sys.exit(1)


# ── Commands ──────────────────────────────────────────────────────────────────

def cmd_list_bases(args):
    result = request("GET", f"{META_URL}/bases")
    bases = result.get("bases", [])
    print(f"# {len(bases)} bases\n")
    for b in bases:
        print(f"{b['id']}\t{b['name']}\t{b.get('permissionLevel', '')}")


def cmd_list_tables(args):
    result = request("GET", f"{META_URL}/bases/{args.base_id}/tables")
    tables = result.get("tables", [])
    print(f"# {len(tables)} tables\n")
    for t in tables:
        fields = [f["name"] for f in t.get("fields", [])]
        print(f"{t['id']}\t{t['name']}\t[{', '.join(fields[:5])}{'...' if len(fields) > 5 else ''}]")


def cmd_list_records(args):
    params = {"pageSize": args.limit}
    if args.view:
        params["view"] = args.view
    if args.filter:
        params["filterByFormula"] = args.filter
    if args.fields:
        for f in args.fields.split(","):
            params[f"fields[]"] = f.strip()

    result = request("GET", f"{BASE_URL}/{args.base_id}/{urllib.parse.quote(args.table)}", params=params)
    records = result.get("records", [])
    offset = result.get("offset")
    print(f"# {len(records)} records\n")
    for r in records:
        row = {"id": r["id"]}
        row.update(r.get("fields", {}))
        print(json.dumps(row))
    if offset:
        print(f"\n# More results — use --offset '{offset}' to continue")


def cmd_get_record(args):
    result = request("GET", f"{BASE_URL}/{args.base_id}/{urllib.parse.quote(args.table)}/{args.record_id}")
    print(json.dumps(result, indent=2))


def cmd_search_records(args):
    # Use SEARCH() formula to find records matching a string in any field
    formula = f"SEARCH(\"{args.query}\", ARRAYJOIN(VALUES({{}}), \" \"))"
    # Simpler: search in a specific field if provided
    if args.field:
        formula = f"SEARCH(\"{args.query}\", {{{args.field}}})"

    params = {"filterByFormula": formula, "pageSize": args.limit}
    result = request("GET", f"{BASE_URL}/{args.base_id}/{urllib.parse.quote(args.table)}", params=params)
    records = result.get("records", [])
    print(f"# {len(records)} results\n")
    for r in records:
        row = {"id": r["id"]}
        row.update(r.get("fields", {}))
        print(json.dumps(row))


# ── CLI ───────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="Airtable CLI")
    sub = parser.add_subparsers(dest="cmd", required=True)

    sub.add_parser("list-bases", help="List all bases you have access to")

    p = sub.add_parser("list-tables", help="List tables in a base")
    p.add_argument("base_id", help="Base ID (starts with 'app')")

    p = sub.add_parser("list-records", help="List records in a table")
    p.add_argument("base_id", help="Base ID (starts with 'app')")
    p.add_argument("table", help="Table name or ID")
    p.add_argument("--limit", type=int, default=25)
    p.add_argument("--view", help="View name to use")
    p.add_argument("--filter", help="Airtable formula to filter records, e.g. \"{Status}='Active'\"")
    p.add_argument("--fields", help="Comma-separated field names to return")

    p = sub.add_parser("get-record", help="Get a specific record")
    p.add_argument("base_id", help="Base ID")
    p.add_argument("table", help="Table name or ID")
    p.add_argument("record_id", help="Record ID (starts with 'rec')")

    p = sub.add_parser("search-records", help="Search records")
    p.add_argument("base_id", help="Base ID")
    p.add_argument("table", help="Table name or ID")
    p.add_argument("query", help="Search string")
    p.add_argument("--field", help="Field name to search in (default: all)")
    p.add_argument("--limit", type=int, default=25)

    args = parser.parse_args()
    {
        "list-bases": cmd_list_bases,
        "list-tables": cmd_list_tables,
        "list-records": cmd_list_records,
        "get-record": cmd_get_record,
        "search-records": cmd_search_records,
    }[args.cmd](args)


if __name__ == "__main__":
    main()

```

airtable | SkillHub