An Airtable code generator, focused on developer experience.
Project description
myAirtable
An Airtable code generator, focused on developer experience.
[!WARNING] myAirtable is under active development.
Languages supported:
- Python (via pyAirtable)
- TypeScript (via airtable.js)
- JavaScript (via airtable.js)
- Rust (via a custom-built client)
- Swift (via a custom-built client)
- Kotlin (via a custom-built client on Ktor + kotlinx.serialization)
- Java (via a custom-built client on the JDK's
java.net.http.HttpClient+ Jackson) - Go (via a custom-built client on the standard library —
net/http+encoding/json, zero third-party dependencies) - C# (via a custom-built .NET 8 client on
HttpClient+System.Text.Json, async/await, zero third-party dependencies) - C++ (via a custom-built C++20 client on libcurl + a vendored nlohmann/json; header-only distribution)
Kotlin
The generated Kotlin code uses @Serializable models, so the kotlinx.serialization compiler plugin is REQUIRED — without kotlin("plugin.serialization") the generated code will not compile. A known-good Gradle setup (build.gradle.kts):
plugins {
kotlin("jvm") version "2.2.20"
kotlin("plugin.serialization") version "2.2.20" // REQUIRED for the generated @Serializable models
}
sourceSets {
main {
kotlin.srcDir("path/to/generated/output") // wherever you generate into
}
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("io.ktor:ktor-client-core:3.2.3")
implementation("io.ktor:ktor-client-cio:3.2.3")
}
Java
The generated Java code targets Java 21+ (sealed interfaces, records, pattern-matching switch) and is a plain blocking API (cheap on virtual threads). It only needs Jackson databind on the classpath:
java {
toolchain { languageVersion = JavaLanguageVersion.of(21) }
}
sourceSets {
main {
java.srcDir("path/to/generated/output") // wherever you generate into
}
}
dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind:2.18.2")
}
[!IMPORTANT] Do NOT register
jackson-datatype-jsr310(or callfindAndRegisterModules()) on the runtime's mapper — the bundledAirtableJacksonModuleownsInstant/Durationencoding (Airtable durations are numeric seconds, not ISOPT…strings).
Models are mutable POJOs created through a generated Builder (the Java analog of the other targets' named arguments): PrimaryModel.builder().primaryKey("x").build(). Bulk deletes are named deleteAll(ids) / deleteModels(models) because Java's type erasure forbids overloading delete(List<String>) against delete(List<Model>).
Error handling
Every failure is an AirtableException — a sealed, unchecked hierarchy (checked exceptions would poison every generated signature). The subtypes are Http (non-2xx, with statusCode()/body()), Api (a structured Airtable error envelope, with code()/apiMessage()), RateLimited (429, with retryAfterSeconds()), Decoding, Network, InvalidUrl, and MissingCredentials. Catch the base type for everything, or a specific subtype for targeted handling:
try {
var contacts = airtable.primary().get();
} catch (AirtableException.RateLimited e) {
// Only reached after retries are exhausted (see below).
log.warn("rate limited; retry after {}s", e.retryAfterSeconds());
} catch (AirtableException e) {
log.error("airtable call failed", e);
}
Because the hierarchy is sealed, you can also pattern-match exhaustively with a switch (Java 21):
String describe(AirtableException e) {
return switch (e) {
case AirtableException.Http h -> "HTTP " + h.statusCode();
case AirtableException.Api a -> a.code() + ": " + a.apiMessage();
case AirtableException.RateLimited r -> "rate limited (" + r.retryAfterSeconds() + "s)";
case AirtableException.Decoding d -> "decode error";
case AirtableException.Network n -> "network error";
case AirtableException.InvalidUrl u -> "bad url";
case AirtableException.MissingCredentials m -> "missing credentials";
};
}
[!NOTE] 429 and 5xx responses are automatically retried with exponential backoff (plus jitter, honoring a server
Retry-After).AirtableException.RateLimitedonly surfaces once retries are exhausted.
Resource lifecycle
Airtable (and the underlying AirtableClient) implements AutoCloseable. A long-lived application should construct one Airtable and hold it for the process lifetime. For short-lived use, a try-with-resources block is idiomatic:
try (var airtable = new Airtable(baseId, apiKey)) {
var contacts = airtable.primary().get();
}
[!NOTE]
close()only closes theHttpClientthe runtime owns — when you inject your ownHttpClient(via the fullAirtableClientconstructor),close()is a no-op and the client remains yours to manage.
Clearing a field
To clear a field server-side, set it to null on a fetched model and update it. Dirty-tracking diffs the model against the snapshot taken at fetch time and emits an explicit JSON null for the cleared field:
var contact = airtable.primary().get("rec1234567890"); // fetched: carries a snapshot
contact.setSingleLineText(null);
airtable.primary().update(contact); // sends {"fields": {"fld...": null}}, clearing it
[!IMPORTANT] This only works on a model obtained from the table — it has a snapshot to diff against. A freshly built model (
PrimaryModel.builder()...build()) has no snapshot, so anullfield is simply omitted from the create payload rather than sent as an explicit clear.
Filtering by formula
Filter with the per-field f accessor and pass the resulting formula string via AirtableQuery.withFormula:
var bobs = airtable.primary()
.get(new AirtableQuery().withFormula(PrimaryModel.f.singleLineText.contains("Bob")));
Text fields expose eq/neq/contains/startsWith/endsWith/regexMatch (etc.); number fields expose eq/neq/greaterThan/lessThan/between (etc.). Combine clauses with the Formulas combinators and / or / not / xor:
var formula = Formulas.and(
PrimaryModel.f.singleLineText.contains("Bob"),
PrimaryModel.f.numberInt.greaterThan(10));
var results = airtable.primary().get(new AirtableQuery().withFormula(formula));
Upsert
upsert(model, mergeOnFieldIds) inserts or updates a record, matching on the given field IDs. It returns an OrmTable.UpsertResult<T> exposing .model() (the merged record) and .wasCreated() (insert vs update). Merge keys come from the generated {Table}Fields ID constants:
var result = airtable.primary().upsert(
PrimaryModel.builder().primaryKey("alice@example.com").build(),
List.of(PrimaryFields.primaryKeyId));
if (result.wasCreated()) {
log.info("created {}", result.model().getId());
}
Go
The generated Go code targets Go 1.21+ and depends on the standard library only (net/http + encoding/json — no third-party modules). All files (the static runtime and the generated code) live in one flat package airtable, and the generator emits no go.mod — the consuming project owns its module and imports the generated package:
import airtable "yourmodule/path/to/output"
at := airtable.NewWithBase(apiKey, baseID, 0) // cacheSeconds; 0 disables caching
Idioms:
- Models are structs with
json:"fldID"tags; optional fields are pointers (*string,*float64, …) so absent and cleared are distinguishable. Build them with the pointer helpers:airtable.PrimaryModel{PrimaryKey: airtable.String("x")}. context.Contextis the first argument of every I/O method:at.Primary.GetOne(ctx, id),rec.Save(ctx).- Errors are returned, never panicked — typed errors via
errors.As(*airtable.APIError,*airtable.RateLimitError, …) and sentinels viaerrors.Is(airtable.ErrNotFound). 429/5xx are retried with backoff before surfacing. - Typed table layer uses generics:
at.Primaryis aTable[PrimaryModel, *PrimaryModel]. Methods follow an explicit*One/*Manynaming convention (Go has no overloading):GetOne/GetMany,CreateOne/CreateMany,UpdateOne/UpdateMany,DeleteOne/DeleteMany, plusUpsertand.Dict()for raw access. Per-model fluentSave/Fetch/Deletework on models bound viaat.Primary.Attach(&m)or returned from the table. - Select options are typed-string constants; computed fields decode into
*MaybeSpecialOrError[T](read with.Value()), lookups/rollups into*VecOrValue[MaybeSpecialOrError[T]](read withCleanValues). - Filtering:
at.Primary.GetMany(ctx, (&airtable.Query{}).WithFilterFormula(airtable.PrimaryF.PrimaryKey.Eq("alice@example.com"))).
Generated code is gofmt-clean by construction; format/lint via gofmt/go vet/golangci-lint.
C#
The generated C# code targets .NET 8 / C# 12 and depends on the base class library only (HttpClient + System.Text.Json — no third-party packages). Every file declares the single file-scoped namespace MyAirtable;, and the generator emits no .csproj — the consuming project owns its build:
var airtable = new Airtable(baseId, apiKey, cacheSeconds: 0); // 0 disables caching
Idioms:
- Models are mutable
sealed classes with[JsonPropertyName("fldID")]and nullable auto-properties. Writable fields are{ get; set; }; computed fields are[JsonInclude] { get; private set; }(decode-only). Build them with object initializers:new PrimaryModel { PrimaryKey = "x" }. - async/await throughout: every I/O method is
…Asyncand takes aCancellationToken—await at.Primary.GetAsync(id),await model.SaveAsync(). - Errors are an unchecked hierarchy —
catch (AirtableException)or pattern-match a nested case (AirtableException.ApiError,.RateLimitedError, …). 429/5xx are retried with jittered backoff before surfacing. - Typed ORM is the default on each table accessor —
at.Primaryis anOrmTable<PrimaryModel>(reified generics, no class token), soat.Primary.GetAsync(id)/CreateAsync/UpdateAsync/UpsertAsync/DeleteAsyncreturn typed models.at.Primary.Dictgives raw field-bag access. Per-model fluentSaveAsync/FetchAsync/DeleteAsync. - Select options are C# enums with a generated per-enum
JsonConverter; computed fields decode intoMaybeSpecialOrError<T>?(read with.ValueOrDefault), lookups/rollups intoVecOrValue<MaybeSpecialOrError<T>>?(read withVecOrValue.CleanValues). - Filtering:
at.Primary.GetAsync(new AirtableQuery().WithFormula(PrimaryModel.F.PrimaryKey.Eq("alice@example.com"))).
Generated code is formatted with CSharpier.
C++
The generated C++ code targets C++20 and is header-only: add the output folder to your include path, link libcurl (preinstalled on macOS/Linux), and compile the vendored timezone implementation in exactly one translation unit:
// tz_impl.cpp — one file in your project (stb-style single-TU implementation)
#define MYAIRTABLE_TZ_IMPLEMENTATION
#include "static/airtable_tz.hpp"
#include "airtable.hpp"
myairtable::Airtable at(apiKey, /*cache_seconds=*/0); // 0 disables caching
Idioms:
- Models are aggregate structs over a data-free CRTP behavior base: public
std::optional<T>members, created with designated initializers —PrimaryModel{.primary_key = "x"}— and passed to the table'screate(). Absent and JSON-null both decode tostd::nullopt. - Blocking, thread-safe client: per-request curl handles over a shared DNS/TLS-session cache; 429/5xx retried with jittered backoff before surfacing.
- Errors are exceptions rooted in
std::runtime_error—catch (const myairtable::AirtableException&)or a specific subclass (ApiError,RateLimitedError, …). - Typed ORM is the default on each table accessor —
at.primary()is anOrmTable<PrimaryModel>, soget/get_all/create/update/upsertreturn typed models;.dict()gives raw field-bag access. Per-model fluentsave()/fetch()/remove()(deleteis a C++ keyword — the one naming divergence). - Select options are
enum classes with generated wire serializers; computed fields decode intostd::optional<MaybeSpecialOrError<T>>(read with->value()), lookups/rollups intostd::optional<VecOrValue<MaybeSpecialOrError<T>>>(read with->clean_values()). Computed members are public but never serialized on write — mutating one and callingsave()sends nothing for it (pinned by tests). - Filtering:
at.primary().get_many(AirtableQuery{.formula = PrimaryModel::F.primary_key.eq("alice@example.com")}). Combinators areFormulas::and_/or_/not_(and/or/notare C++ alternative tokens). - Portability note: designated-initializer-over-base relies on the C++20 P1975 defect resolution; AppleClang is the CI-gated compiler, GCC/MSVC are unverified.
Generated code is formatted with clang-format (vendored headers exempt).
Features
The following examples are in Python, but most features are supported in every language. See notes in each section for language-specific differences.
[!NOTE] In the examples below,
myairtable_outputstands in for your generated package — the actual import path depends on the output folder you generate into.
ORM Models
myAirtable generates strongly-typed RecordDicts and ORM classes, intended for use with the pyAirtable library.
# Fully-typed versions of pyAirtable's RecordDict TypedDict class
class ContactsRecordDict(RecordDict):
fields: dict[ContactsField, Any] # ContactsField is a Literal of the field names in the Contacts table
name = contact["fields"]["Name"] # your IDE will suggest "Name"
# Instance of pyAirtable's ORM
class ContactsModel(Model):
name: SingleLineTextField = SingleLineTextField(field_name="fld123")
address: MultilineTextField = MultilineTextField(field_name="fld789")
# etc
name = contact.name
[!NOTE] For JavaScript & TypeScript, the ORM models are custom to myAirtable, though they still use the Airtable.js client for save/delete, and contain methods for conversion to/from Airtable.js's "Record" class. Also, they use Zod validation under-the-hood.
For Rust, Swift, Kotlin, and Java, 100% of the code is custom to myAirtable. The convenient linked-record traversal syntax in Python and TS/JS is not (yet?) implemented in these targets — linked records are raw record-ID lists resolved through the linked table's
get.
Formula Builders
myAirtable also generates formula builders, for use when filtering by formula. pyAirtable already includes decent formula builders, but their options are currently limited to simple operations (e.g. =, >, <, etc), without any type-specific operations. myAirtable's formula builders include additional operations (e.g. "string contains", "date is N days ago", etc). You can access the myAirtable formula helpers from the .f property on each ORM class. myAirtable's formula builders are fully compatible with pyAirtable's formula builders.
from myairtable_output import Airtable, AND, OR, ContactsModel
formula: str = AND(
ContactsModel.f.first_name.contains("Bob") & (ContactsModel.f.last_name == "Smith"),
ContactsModel.f.birthday.after().years_ago(30),
ContactsModel.f.birthday < "2019-04-01",
(ContactsModel.f.age < 10) | (ContactsModel.f.age == 12) | (ContactsModel.f.age > 15),
"{fld1234567890}='you can also put raw strings here'",
)
Airtable().contacts.get(formula=formula)
[!NOTE] For JavaScript, TypeScript, Rust, Swift, Kotlin, and Java, the formula builders output strings, and lack the Python-specific convenience of dunder methods, but are otherwise the same. (Kotlin and Java name the equality pair
eq/neq—equalscollides withAny.equals/Object.equalson the JVM.)
Table/CRUD Wrappers
Finally, myAirtable generates custom lightweight wrapper classes, which expose pyAirtable's CRUD methods with strongly-typed kwargs, and provide easy access to the tables through a simple interface.
from myairtable_output import Airtable, ContactsModel, ContactsRecordDict
airtable = Airtable()
# CRUD operations for pyAirtable ORMs
contact: ContactsModel = airtable.contacts.get("rec1234567890")
contact.name = "Bob"
contact.save() # pyAirtable's ORM models have handy functions like .save()
airtable.contacts.update(contact) # or you can use myAirtable's wrapper if you prefer that syntax
# table.get() method has kwargs for most of pyAirtable's options, which are otherwise less clear. View and Fields kwargs are typed.
contacts: list[ContactsModel] = airtable.contacts.get(view="Family & Friends", fields=["Name", "Age"])
for contact in contacts:
contact.age = contact.age + 1
contact.save()
# CRUD operations for pyAirtable RecordDicts
contact: ContactsRecordDict = airtable.contacts.dict.get("rec1234567890")
contact["fields"]["name"] = "Joe"
airtable.contacts.dict.update(contact)
[!NOTE] For JavaScript & TypeScript, the equivalents of
.dictare integrated into the standard CRUD operations. They will return/accept the myAirtable'sAirtableModelclasses, Airtable.js'sRecord<FieldSet>class, or a plain interface containing the json data.
Name-locking and custom names
myAirtable optionally generates CSV files containing the names/ids of the tables and fields, including the "property name" or "model name" (for the ORM models) that they will be given in myAirtable's output. These CSV files, if present in the destination folder, will be used as the source of truth for table, model, and field names in the generated code. They can thus be used to prevent class/property names from changing unexpectedly when someone else changes a field name in Airtable, or for customizing the class/property names as they appear in code, if you prefer a different name for a given table/field. They can also be handy for resolving duplicate property name issues if that happens.
Documentation (Markdown and HTML)
myAirtable also includes support for generating documentation for your Airtable base. There is a version in Markdown (intended for Obsidian) and in HTML (intended as a static website). This documentation includes:
- Files for every table & field, with metadata for each.
- Tags for each field type for easy sorting/filtering
- Links between related tables/fields, whether by link, lookup, rollup, or formula.
- Formula fields are where it really shines. It shows:
- A "flattened" version of the formula. If the formula references another formula (etc), the whole thing is shown.
- A formatted and syntax-colored version of the formula, for easy readability.
- A Mermaid representation of the formula.
Extra Goodies (because I can't stop coding...)
- All the types: just about everything from the schema has a constant/dict/type for convenience. Want an array of the options for a select field? How about a map between field ids and names? Or perhaps a union type representing all table names? It's all in there.
- Convenience functions to:
- build an Airtable URL for base/table/view/record
- get the schema, either static (the one used to build the code) or live (from Airtable's API)
- Optional caching
- Optional runtime formula evaluation: if enabled, formula fields have their formula transpiled to native code, to allow runtime (re)evaluation. Supports nearly all formulas (can't do LAST_MODIFIED_TIME or CREATED_TIME).
- MCP Server: Allows agents to analyze your Airtable schema
Getting Started
Requires Python 3.12+.
Install
To run the code generator, install with the cli extra:
uv tool install "myairtable[cli]"
myairtable --help
Then provide credentials, either as flags (--base-id, --api-key) or via a .env
file in the working directory:
AIRTABLE_API_KEY=your_airtable_api_key_here
AIRTABLE_BASE_ID=app1234567890
Extras
The package splits along what you actually need, so importing the analysis layer doesn't drag in the whole generator:
| Install | Gets you |
|---|---|
myairtable |
The analysis layer only — myairtable.schema_tools (28 read-only tools over the public meta API), for importing into your own code |
myairtable[cli] |
The above plus the code generator for all 11 targets (the myairtable command) |
myairtable[mcp] |
The above plus the MCP server |
Developing on myAirtable itself
- Clone the repo
- Install uv
uv sync— installs the project editable, with every extra and dev tool- Add the
.envabove uv run myairtable --help
MCP Server
myAirtable includes an MCP server that exposes read-only tools for Airtable schema introspection and analysis.
Setup
Add the following to your MCP client config (e.g. claude_desktop_config.json for Claude Desktop, or .claude/settings.json for Claude Code):
{
"mcpServers": {
"myairtable": {
"command": "uv",
"args": ["run", "--directory", "/path/to/myairtable", "python", "-m", "myairtable.mcp_server"],
"env": {
"AIRTABLE_API_KEY": "your_airtable_api_key_here",
"AIRTABLE_BASE_ID": "app1234567890"
}
}
}
}
If you have a .env file configured (see step 4 above), you can omit the env block.
Tools on the CLI
Every MCP tool is also a CLI subcommand under myairtable tools — handy for scripting,
piping to jq, or testing without an MCP client:
myairtable tools list-tables # public meta-API tools
myairtable tools transpile Jobs "Total" --language python
myairtable tools --help # list all commands
Output is JSON by default (--pretty for rich rendering). Add --save (before the
subcommand) to offload large results to .data/tools/ and print the envelope, mirroring the
MCP server. The same schema_tools.py functions back both the MCP tools and these commands.
Large results are saved to disk
Many tools can return large payloads (whole-base scans, full schema dumps). When a result
exceeds the inline limit (default 16 KB; override with MYAIRTABLE_INLINE_MAX_BYTES), the
full result is written to a gitignored .data/tools/<tool>__<args>.json and the caller
receives a small envelope instead:
{ "saved_to": "...", "bytes": 412934, "record_count": 287,
"summary": { ... }, "schema": { ...compact shape skeleton... }, "note": "..." }
schema is a depth-capped structural skeleton of the file (keys → type tags, arrays → length +
element shape) — enough to jq/grep the file without re-running the tool. This applies to
all MCP tools via a server middleware. From the CLI, add --save (e.g.
myairtable tools --save list-automations) to exercise the same path. Note: audit_shares
writes share-URL tokens to that local file.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file myairtable-0.1.0rc1.tar.gz.
File metadata
- Download URL: myairtable-0.1.0rc1.tar.gz
- Upload date:
- Size: 708.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a0c91934d545a77995bc7f4bfcfae57c290b9c45770858dbda3da4a67381c3d
|
|
| MD5 |
7413758847486c88dcf1fc8281ad468f
|
|
| BLAKE2b-256 |
ebdcf90d32b5fc2b231c7e2580e3ae61c4d13f3a84ffd5e3eba65e635ddcd366
|
Provenance
The following attestation bundles were made for myairtable-0.1.0rc1.tar.gz:
Publisher:
release.yml on danielrbaughman/myairtable
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
myairtable-0.1.0rc1.tar.gz -
Subject digest:
5a0c91934d545a77995bc7f4bfcfae57c290b9c45770858dbda3da4a67381c3d - Sigstore transparency entry: 2190588504
- Sigstore integration time:
-
Permalink:
danielrbaughman/myairtable@e9bf1763f844e8a836b2d768b87e0c164ae25c08 -
Branch / Tag:
refs/tags/v0.1.0rc1 - Owner: https://github.com/danielrbaughman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e9bf1763f844e8a836b2d768b87e0c164ae25c08 -
Trigger Event:
push
-
Statement type:
File details
Details for the file myairtable-0.1.0rc1-py3-none-any.whl.
File metadata
- Download URL: myairtable-0.1.0rc1-py3-none-any.whl
- Upload date:
- Size: 846.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1f8a590e65d5417c7d07e06ba856ad3b933253b9220e052cdb9e46ba155d5a3
|
|
| MD5 |
44c2b9edef9ad26bdb75c764a390949c
|
|
| BLAKE2b-256 |
5c9d94d3205f7b93cbe70a45fada190224713bb4d6229461c509a0f37e6f92fe
|
Provenance
The following attestation bundles were made for myairtable-0.1.0rc1-py3-none-any.whl:
Publisher:
release.yml on danielrbaughman/myairtable
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
myairtable-0.1.0rc1-py3-none-any.whl -
Subject digest:
e1f8a590e65d5417c7d07e06ba856ad3b933253b9220e052cdb9e46ba155d5a3 - Sigstore transparency entry: 2190588514
- Sigstore integration time:
-
Permalink:
danielrbaughman/myairtable@e9bf1763f844e8a836b2d768b87e0c164ae25c08 -
Branch / Tag:
refs/tags/v0.1.0rc1 - Owner: https://github.com/danielrbaughman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e9bf1763f844e8a836b2d768b87e0c164ae25c08 -
Trigger Event:
push
-
Statement type: