The real problem
Why this is harder than it looks
Most teams look at Airtable's REST API and think the integration will take an afternoon. The read/write calls are clean. The docs are good. The problems start once you're running this for real users.
In production, each of your users connects their own Airtable workspace. That means per-user token isolation — one user's credentials must be completely invisible to another, even if they share the same base. Airtable uses OAuth 2.0 with PKCE, and access tokens have a short expiry; your refresh logic has to run proactively, before a call fails, not in response to a 401. If a user revokes your app from their Airtable account settings, you need to detect it and stop making calls on their behalf immediately — silent failures in a database context are particularly dangerous.
Then there's the scopes problem. Airtable's permission model is granular: read, write, schema access, and user metadata are all separate scopes. Ask for too little and your agent fails at runtime. Ask for too much and your users' IT admins reject the OAuth consent screen. Getting the scope set right — and keeping it right as your agent's capabilities grow — is its own ongoing maintenance task.
None of this is impossible. But it's weeks of plumbing that doesn't make your agent smarter. And you'll repeat it for every connector you add. Scalekit handles the token lifecycle, scope configuration, and revocation detection so your code only has to make the API calls.
Capabilities
What your agent can do with Airtable
Once connected, your agent can make authenticated calls across the full Airtable REST API:
- Read and query records: list records from any table with filters, sorts, and field selection; paginate large datasets automatically
- Create and update structured data: add new rows, patch existing records, upsert by a unique field value
- Manage tables and fields: create tables, add or modify fields, read schema metadata at runtime
- Inspect bases and workspaces: list all accessible bases, describe table structures, check record counts without fetching all data
- Delete records cleanly: remove individual records or batches with full error handling per operation
Setup context
What we're building
This guide connects a project data assistant to Airtable — helping team members query records, log updates, and manage structured project data without leaving your product.
🤖
Example agent
Project assistant reading and writing Airtable bases on behalf of each team member
🔐
Auth model
B2B SaaS — each user connects their own workspace. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Airtable secrets, no user tokens, nothing belonging to your customers.
pip install scalekit-sdk-python
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each user gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="airtable",
identifier="user_at_456" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The user authorizes your agent once. Scalekit generates the OAuth URL with correct scopes, PKCE challenge, and redirect handling pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="airtable",
identifier="user_at_456"
)
# Redirect user → Airtable consent screen
# Scalekit captures the token on callback
return redirect(link.link)
Token management is automatic
After the user approves, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. Access
tokens refresh before expiry. If a user revokes access from Airtable's app settings, the account moves to REVOKED
— no silent failures. Check account.status before critical write operations.
Bring Your Own Credentials — required for production
Unlike some connectors, Airtable requires you to register your own OAuth app in the Airtable Developer Hub
and supply your own Client ID and Secret. This means your users see your app name on the consent screen.
Register the Scalekit redirect URI in your Airtable app settings, then paste your credentials into the
Scalekit dashboard. Token management stays fully handled.
Already have credentials?
Calling Airtable
4 Calling Airtable: What your agent writes
With the connected account active, your agent makes API calls using actions.request(). Pass the Airtable API path and method. Scalekit injects the correct token, handles retries on refresh, and returns the parsed response.
List records from a table
Fetch records from a specific table. Use params to pass Airtable query options like filterByFormula, sort, or fields.
result = actions.request(
connection_name="airtable",
identifier="user_at_456",
path="/v0/appXXXXXXXX/Projects",
method="GET",
params={
"filterByFormula": "AND({Status}='In Progress', {Owner}='Alice')",
"fields[]": ["Name", "Status", "Due Date", "Priority"],
"sort[0][field]": "Due Date",
"sort[0][direction]": "asc"
}
)
# { "records": [ { "id": "recXXX", "fields": { "Name": "...", ... } }, ... ] }
Create a record
Add a new row to any table. Field names in fields must match the column names exactly as they appear in the base.
result = actions.request(
connection_name="airtable",
identifier="user_at_456",
path="/v0/appXXXXXXXX/Projects",
method="POST",
json={
"fields": {
"Name": "Q3 Roadmap Review",
"Status": "Not Started",
"Due Date": "2026-07-15",
"Priority": "High",
"Owner": "Alice"
}
}
)
# { "id": "recYYYYYYYY", "fields": { ... }, "createdTime": "..." }
Update a record
Patch an existing record by ID. Only the fields you supply are modified — other fields remain untouched.
result = actions.request(
connection_name="airtable",
identifier="user_at_456",
path="/v0/appXXXXXXXX/Projects/recYYYYYYYY",
method="PATCH",
json={
"fields": {
"Status": "In Progress",
"Notes": "Kickoff completed, design review scheduled for next week"
}
}
)
Inspect base schema at runtime
Your agent can read the schema of any accessible base without hardcoding field names — useful for dynamic agents that adapt to the user's actual Airtable structure.
schema = actions.request(
connection_name="airtable",
identifier="user_at_456",
path="/v0/meta/bases/appXXXXXXXX/tables",
method="GET"
)
# Returns all table names, field types, and relationship metadata
# Agent can use this to build dynamic queries without hardcoded field names
for table in schema["tables"]:
print(f"Table: {table['name']}, Fields: {[f['name'] for f in table['fields']]}")
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to look up or write; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
at_tools = get_tools(
connection_name="airtable",
identifier="user_at_456"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a project data assistant. Use the available tools to help manage Airtable records."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), at_tools, prompt)
result = AgentExecutor(agent=agent, tools=at_tools).invoke({
"input": "Find all high-priority projects owned by Alice that are due this month"
})
Other frameworks supported
API reference
Airtable REST API — Key endpoints
Scalekit proxies any Airtable REST API call. Your agent passes the path and method — here are the most useful endpoints grouped by purpose.
GET /v0/{baseId}/{tableId}
List records from a table. Supports filterByFormula, sort, fields, maxRecords, and pageSize parameters
GET /v0/{baseId}/{tableId}/{recordId}
Retrieve a single record by ID with all field values
POST /v0/{baseId}/{tableId}
Create one or more records. Pass a single fields object or a records array for bulk creation
PATCH /v0/{baseId}/{tableId}/{recordId}
Update specific fields on a record. Only supplied fields are changed
PUT /v0/{baseId}/{tableId}/{recordId}
Replace all fields on a record. Unsupplied fields are cleared
DELETE /v0/{baseId}/{tableId}/{recordId}
Permanently delete a record by ID
POST /v0/{baseId}/{tableId}/upsert
Upsert records by a unique field — creates if not found, updates if it exists
List all bases the authenticated user has access to, including base IDs and names
GET /v0/meta/bases/{baseId}/tables
Retrieve full schema for all tables in a base — field names, types, and relationships
POST /v0/meta/bases/{baseId}/tables
Create a new table in a base with specified fields and field types
PATCH /v0/meta/bases/{baseId}/tables/{tableId}
Update a table's name or description
POST /v0/meta/bases/{baseId}/tables/{tableId}/fields
Add a new field to an existing table with a specified type and configuration
PATCH .../{tableId}/fields/{fieldId}
Update an existing field's name, description, or type-specific options
Return the authenticated user's ID, email, and name — useful to verify authorization succeeded
Connector notes
Airtable-specific behavior
BYOC is required — no managed app available
Unlike some Scalekit connectors, Airtable does not provide a managed Connected App. You must register your
own OAuth app in the Airtable Developer Hub, configure the redirect URI, and supply your Client ID and
Secret in the Scalekit dashboard before the authorization flow will work.
Scope selection affects what your agent can do
Airtable's OAuth scopes are fine-grained. data.records:read and data.records:write cover most CRUD
operations, but schema access (schema.bases:read) is a separate scope. If your agent calls schema
introspection endpoints without this scope, you will get a 403 — not an auth error. Ensure your scope set
in the Scalekit dashboard matches your agent's full capability set before deploying.
Base IDs must be obtained at runtime
Airtable base IDs (e.g. appXXXXXXXX) are workspace-specific. For agents that work across multiple users'
bases, use GET /v0/meta/bases to discover base IDs at runtime rather than hardcoding them. This is
especially important in multi-tenant deployments where each user may have different bases.
Infrastructure decision
Why not build this yourself
The Airtable OAuth flow is documented. Token storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
Per-user token isolation across a multi-tenant system — one user's Airtable credentials must never be accessible to another
PROBLEM 02
Proactive refresh logic that runs before token expiry — not after a failed call returns a 401 mid-operation
PROBLEM 03
Revocation detection when users disconnect your app from Airtable — and graceful handling so agents stop making calls immediately
PROBLEM 04
Scope management as your agent's capabilities grow — adding new API endpoints without requiring users to re-authorize is a non-trivial token lifecycle problem
That's one connector. Your agent product will eventually need Salesforce, Gmail, Notion, HubSpot, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.