Airtable logo

Airtable

Organization
TerminalSkills
airtable

Build integrations with the Airtable Web API — bases, tables, records, fields, views, webhooks, and OAuth. Use when tasks involve reading or writing Airtable data, syncing external sources with Airtable bases, building automations triggered by record changes, or migrating data to/from Airtable.

Overview

PublisherTerminalSkills
Repositoryskills
Skill nameairtable
Stars
155
Forks
21
Bundled files
1
LicenseApache-2.0
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by TerminalSkills on GitHub. Read the source before you install it.

Installation

Install the Airtable AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/TerminalSkills/skills.git /tmp/skills
mkdir -p .claude/skills
cp -r /tmp/skills/skills/airtable .claude/skills/airtable
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Airtable in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Airtable on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Airtable is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Airtable API Integration

Automate and integrate with Airtable bases using the REST API.

Authentication

Personal Access Token (simplest)

Generate at https://airtable.com/create/tokens. Scope to specific bases and permissions.

bash
export AIRTABLE_TOKEN="pat..."

OAuth 2.0 (multi-user apps)

Register at https://airtable.com/create/oauth. Supports PKCE for public clients.

python
"""airtable_oauth.py — OAuth 2.0 with PKCE for Airtable."""
import hashlib, secrets, base64, requests

def start_oauth(client_id: str, redirect_uri: str) -> tuple[str, str]:
    """Generate authorization URL with PKCE challenge.

    Args:
        client_id: From Airtable OAuth integration settings.
        redirect_uri: Your callback URL.

    Returns:
        Tuple of (authorization_url, code_verifier) — store verifier for token exchange.
    """
    verifier = secrets.token_urlsafe(64)
    challenge = base64.urlsafe_b64encode(
        hashlib.sha256(verifier.encode()).digest()
    ).rstrip(b"=").decode()

    url = (
        f"https://airtable.com/oauth2/v1/authorize?"
        f"client_id={client_id}&redirect_uri={redirect_uri}"
        f"&response_type=code&scope=data.records:read data.records:write schema.bases:read"
        f"&code_challenge={challenge}&code_challenge_method=S256"
    )
    return url, verifier

def exchange_token(code: str, verifier: str, client_id: str, redirect_uri: str) -> dict:
    """Exchange authorization code for access token.

    Args:
        code: From Airtable's redirect.
        verifier: The PKCE code_verifier from start_oauth.
        client_id: OAuth client ID.
        redirect_uri: Must match the one used in authorization.
    """
    resp = requests.post("https://airtable.com/oauth2/v1/token", data={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": redirect_uri,
        "client_id": client_id,
        "code_verifier": verifier,
    })
    return resp.json()  # access_token, refresh_token, expires_in

Core API Patterns

Record Operations

python
"""airtable_records.py — CRUD operations on Airtable records."""
import requests, time

API = "https://api.airtable.com/v0"

def headers(token: str) -> dict:
    return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

def list_records(token: str, base_id: str, table_name: str,
                 view: str = None, formula: str = None,
                 fields: list = None, sort: list = None) -> list:
    """List all records from a table with optional filtering.

    Args:
        token: Personal access token or OAuth token.
        base_id: Base ID (starts with 'app').
        table_name: Table name or ID.
        view: Optional view name to filter/sort by.
        formula: Airtable formula for filtering (e.g., "AND({Status}='Active', {Score}>80)").
        fields: List of field names to return (reduces payload).
        sort: List of dicts with 'field' and 'direction' keys.

    Returns:
        List of all matching record objects.
    """
    params = {}
    if view:
        params["view"] = view
    if formula:
        params["filterByFormula"] = formula
    if fields:
        for i, f in enumerate(fields):
            params[f"fields[{i}]"] = f
    if sort:
        for i, s in enumerate(sort):
            params[f"sort[{i}][field]"] = s["field"]
            params[f"sort[{i}][direction]"] = s.get("direction", "asc")

    records = []
    offset = None
    while True:
        if offset:
            params["offset"] = offset
        resp = requests.get(f"{API}/{base_id}/{table_name}",
                            params=params, headers=headers(token))
        resp.raise_for_status()
        data = resp.json()
        records.extend(data["records"])
        offset = data.get("offset")
        if not offset:
            break
        time.sleep(0.2)  # Stay under 5 req/s rate limit

    return records

def create_records(token: str, base_id: str, table_name: str,
                   records: list[dict], typecast: bool = False) -> list:
    """Create up to 10 records at a time.

    Args:
        token: Auth token.
        base_id: Base ID.
        table_name: Target table.
        records: List of dicts with field values (max 10 per call).
        typecast: If True, Airtable auto-converts string values to proper types.

    Returns:
        List of created record objects with IDs.
    """
    # Airtable limits to 10 records per request
    created = []
    for i in range(0, len(records), 10):
        batch = [{"fields": r} for r in records[i:i + 10]]
        resp = requests.post(
            f"{API}/{base_id}/{table_name}",
            json={"records": batch, "typecast": typecast},
            headers=headers(token),
        )
        resp.raise_for_status()
        created.extend(resp.json()["records"])
        if i + 10 < len(records):
            time.sleep(0.2)
    return created

def update_records(token: str, base_id: str, table_name: str,
                   updates: list[dict]) -> list:
    """Update existing records (PATCH — partial update).

    Args:
        token: Auth token.
        base_id: Base ID.
        table_name: Target table.
        updates: List of dicts with 'id' and 'fields' keys (max 10 per call).
    """
    updated = []
    for i in range(0, len(updates), 10):
        batch = updates[i:i + 10]
        resp = requests.patch(
            f"{API}/{base_id}/{table_name}",
            json={"records": batch},
            headers=headers(token),
        )
        resp.raise_for_status()
        updated.extend(resp.json()["records"])
        if i + 10 < len(updates):
            time.sleep(0.2)
    return updated

def delete_records(token: str, base_id: str, table_name: str,
                   record_ids: list[str]) -> list:
    """Delete records by ID (max 10 per call).

    Args:
        token: Auth token.
        base_id: Base ID.
        table_name: Target table.
        record_ids: List of record IDs to delete.
    """
    deleted = []
    for i in range(0, len(record_ids), 10):
        batch = record_ids[i:i + 10]
        params = "&".join(f"records[]={rid}" for rid in batch)
        resp = requests.delete(
            f"{API}/{base_id}/{table_name}?{params}",
            headers=headers(token),
        )
        resp.raise_for_status()
        deleted.extend(resp.json()["records"])
        if i + 10 < len(record_ids):
            time.sleep(0.2)
    return deleted

Formula Filtering

Airtable formulas are powerful for server-side filtering:

python
# Common formula patterns
formulas = {
    # Exact match
    "status_active": "{Status} = 'Active'",

    # Multiple conditions
    "high_priority_open": "AND({Priority} = 'High', {Status} != 'Done')",

    # Date filtering — tasks due this week
    "due_this_week": "IS_BEFORE({Due Date}, DATEADD(TODAY(), 7, 'days'))",

    # Text search (case-insensitive)
    "name_contains": "FIND('search term', LOWER({Name}))",

    # Linked records — has at least one linked project
    "has_project": "{Project} != ''",

    # Number range
    "score_range": "AND({Score} >= 80, {Score} <= 100)",

    # Empty/non-empty checks
    "missing_email": "{Email} = ''",
    "has_attachment": "{Attachments} != ''",
}

Schema Operations

Read and modify base structure:

python
def get_base_schema(token: str, base_id: str) -> dict:
    """Get all tables, fields, and views in a base.

    Args:
        token: Auth token with schema.bases:read scope.
        base_id: Base ID.
    """
    resp = requests.get(f"https://api.airtable.com/v0/meta/bases/{base_id}/tables",
                        headers=headers(token))
    return resp.json()

def create_field(token: str, base_id: str, table_id: str,
                 name: str, field_type: str, options: dict = None) -> dict:
    """Add a new field (column) to a table.

    Args:
        token: Auth token with schema.bases:write scope.
        base_id: Base ID.
        table_id: Table ID (not name).
        name: Field name.
        field_type: One of: singleLineText, multilineText, number, percent,
                    currency, singleSelect, multipleSelects, date, checkbox, etc.
        options: Type-specific options (e.g., select choices, currency symbol).
    """
    body = {"name": name, "type": field_type}
    if options:
        body["options"] = options
    resp = requests.post(
        f"https://api.airtable.com/v0/meta/bases/{base_id}/tables/{table_id}/fields",
        json=body, headers=headers(token),
    )
    return resp.json()

Webhooks

Listen for changes in real time:

python
def create_webhook(token: str, base_id: str, notification_url: str,
                   table_id: str = None) -> dict:
    """Register a webhook for record changes.

    Args:
        token: Auth token with webhook:manage scope.
        base_id: Base ID to watch.
        notification_url: Your HTTPS endpoint for receiving payloads.
        table_id: Optional — watch a specific table instead of entire base.
    """
    spec = {"options": {"filters": {"sourceType": "client", "recordChangeScope": base_id}}}
    if table_id:
        spec["options"]["filters"]["watchDataInFieldIds"] = "all"
    resp = requests.post(
        f"https://api.airtable.com/v0/bases/{base_id}/webhooks",
        json={"notificationUrl": notification_url, "specification": spec},
        headers=headers(token),
    )
    return resp.json()

def list_webhook_payloads(token: str, base_id: str, webhook_id: str,
                          cursor: int = None) -> dict:
    """Fetch webhook payloads (poll-based — payloads expire after 7 days).

    Args:
        token: Auth token.
        base_id: Base ID.
        webhook_id: Webhook ID from create_webhook.
        cursor: Cursor from previous response for pagination.
    """
    params = {}
    if cursor:
        params["cursor"] = cursor
    resp = requests.get(
        f"https://api.airtable.com/v0/bases/{base_id}/webhooks/{webhook_id}/payloads",
        params=params, headers=headers(token),
    )
    return resp.json()

Rate Limits

  • 5 requests per second per base
  • Record operations: max 10 records per create/update/delete call
  • List: max 100 records per page (use offset for pagination)
  • Implement backoff on 429 responses

Field Type Reference

TypeAPI nameNotes
Single line textsingleLineTextPlain string
Long textmultilineTextSupports markdown
NumbernumberInteger or decimal
Single selectsingleSelectOne from predefined list
Multiple selectmultipleSelectsArray of selections
DatedateISO 8601 string
CheckboxcheckboxBoolean
URLurlValidated URL string
EmailemailValidated email string
PhonephoneNumberString
CurrencycurrencyNumber with currency formatting
PercentpercentNumber displayed as percentage
AttachmentmultipleAttachmentsArray of file objects with URL
Linked recordmultipleRecordLinksArray of record IDs
LookupmultipleLookupValuesRead-only, from linked records
RolluprollupRead-only, aggregation of linked records
FormulaformulaRead-only, computed value

Guidelines

  • Always paginate list requests -- never assume all records fit in one response
  • Use filterByFormula to reduce payload instead of fetching all and filtering client-side
  • Batch operations in groups of 10 (API limit) with 200ms delays
  • Use typecast: true for imports where field values might need conversion
  • Attachment fields require URLs -- upload to a hosting service first, then pass the URL
  • Linked record fields accept record IDs, not display values
  • Formula and rollup fields are read-only -- you cannot write to them via API
  • Webhook payloads don't contain full record data -- they signal changes, then you fetch details
  • Rate limits are per-base, not per-token -- multiple integrations on the same base share the limit

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Airtable AI skill do?

Build integrations with the Airtable Web API — bases, tables, records, fields, views, webhooks, and OAuth. Use when tasks involve reading or writing Airtable data, syncing external sources with Airtable bases, building automations triggered by record changes, or migrating data to/from Airtable.

Why use Airtable on TypingMind?

Because you install it once and use it with any model. Airtable is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Airtable in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/TerminalSkills/skills/tree/main/skills/airtable. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Airtable?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Airtable?

As many as you like. As long as a model supports skills, you can use Airtable with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Airtable AI skill free?

Yes. It is published on GitHub by TerminalSkills under the Apache-2.0 license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇