How (and why) to migrate from Airtable to Retool
Overview
Migrating from Airtable to Retool means moving each base into a Postgres database, then rebuilding its views, automations, and permissions as Retool apps and Workflows. The rows load in minutes, but most of the work is schema design, and this guide walks through all of it with a real base.
Airtable is an interesting premise. On the surface, it looks like a colorful spreadsheet. You have rows, columns, and filters. But underneath, it works more like a database, giving you links between records, attachment storage, and formulas and lookups that recompute every time a record changes.
Anyone can build on it. Nobody has to ask engineering for a schema, a migration, or a deploy.
Sound familiar? If you are deciding to move away from Airtable, a migration to Retool makes a lot of sense. Conceptually, the products come from the same place—democratizing who gets to build the tools a team runs on. Airtable did it for people who think in spreadsheets. Retool does it for teams whose data, access, and audit need to hold up when more than one team depends on them–and for the engineers who have to stand behind what gets built.
Practically, you get a real Postgres database, apps that sit on top of it, and workflows that run on a schedule or when someone clicks a button. Every query is readable SQL, and every workflow run leaves a log.
Here, we'll show you how that works by migrating a real Airtable base to Retool and building an app on top of it.
What is an Airtable-to-Retool migration?
An Airtable-to-Retool migration moves a base into a relational database and rebuilds the views, automations, and permissions on top of it in Retool. The database is Postgres, either Retool Database or one you already run.
So far, so easy. But there is a mismatch. Airtable has field types but doesn't enforce much around them, and a relational database like Postgres enforces everything.
- Airtable fields are typed, but nothing is required or unique. Every column in Postgres has constraints, and it rejects rows that break them.
- A linked-record field can hold one record or five. "Single link" is a preference you tick in the field settings so that a review can end up with two vendors and nothing complains. In Postgres, that's a foreign key column, or a junction table when there can be several, and you choose before loading a row.
- Formulas and lookups are computed every time a record is read, so the export gives you the value and not the calculation. The deadline on a review has to become a SQL expression or a query in the app, and you rebuild it yourself.
So most of the migration isn’t data migration; it’s schema design. The rows move in a few minutes. The automation that emails procurement when a review is approved, and the list of who can edit what, don't export at all. You build those again in Retool and test them like anything else you'd ship.
Airtable vs. Retool after migration
It's worth being clear about what's on the other side, so you can build the right mental model before you start.
- Data: Airtable keeps data inside the base, capped per plan at 1,000 to 125,000 records (opens in a new tab) on self-serve tiers. Retool uses Postgres you own, or Retool Database with 5 GB and no row cap.
- Linked records: In Airtable, a link is a field on the record. In Retool, links become foreign keys and joins, enforced by the database.
- Automations: Airtable automations become Retool workflows, triggered by a schedule, a webhook, or an app, with a log for every run.
- Attachments: Airtable stores files in the base and serves them through URLs that expire (opens in a new tab). Retool keeps files in Retool Storage or your own object storage, with the URL saved in the database.
- Permissions: Airtable sets access per base and per collaborator. Retool uses groups per app and resource, plus table, column, and row rules through access policies on Postgres (Enterprise plan).
- Audit trail: Airtable’s audit logs require the Enterprise Scale plan (opens in a new tab). Retool’s audit logs record every query run, with its text and parameters, on Business and Enterprise.
- Best suited for: Airtable fits a team tracking its own work. Retool fits apps on production data that more than one team depends on.
None of this is a knock on Airtable. It's the right tool for a team tracking its own work; it just gets awkward once other teams and auditors depend on the base. Permissions and audit are where the real change is, and both are things you configure in Retool, not things the migration gives you for free.
Why teams migrate from Airtable to Retool
Plenty of companies run real operations on Airtable on purpose, and it works for a long time. What gets hard is knowing what happened. A vendor's bank details changed at some point, or an approval got flipped back to draft, and now the question is: who did it, and when?
Airtable does keep a revision history, but it's per record. You open the record, expand the activity, and read. Nothing answers “which vendors had their contact details changed last month, and by whom” across a base, and audit logs that cover everything are on the Enterprise Scale plan (opens in a new tab). On any other tier, the compliance answer is: you don't know.
The same thing happens one level up, with the bases themselves. Bases get duplicated for a project, renamed, shared with a contractor, or split in two when they hit the record cap (opens in a new tab). Each one has its own collaborators and its own automations. After a couple of years, nobody has the list of what exists, or which base is the real one when two of them disagree about a vendor.
The teams that move usually have one of these in front of them:
- A base that's the system of record for something regulated, with nothing to show an auditor.
- Sprawl nobody can see across, where the same vendor exists in four bases and none of them agree.
- An app that needs to be built on the data, with a database behind it and row-level access rules, and no clean way to do that on top of a base.
- A vendor question they can't answer, which brings us to the acquisition.
What the Bending Spoons acquisition means for your evaluation
Bending Spoons agreed to buy Airtable for $1.28 billion in cash on August 4, 2026 (opens in a new tab), and the deal closed on September 4 (opens in a new tab). This isn't a distressed company. Airtable continues to grow, with over 500,000 organizations as customers.
What's got people looking is the buyer. Bending Spoons owns Evernote, WeTransfer, Eventbrite, and Vimeo, and TechCrunch describes its usual approach as buying companies at a discount, trimming staff, streamlining products, and running them for profit. Its CEO has said the plan for Airtable is investing in the product, support, and go-to-market (opens in a new tab), but no roadmap changes have been announced.
So this is a vendor review, not a five-alarm emergency. A change of ownership is a fair reason to ask what your exit would look like if you needed one, and how long it would take. If the answer is “we'd have to rebuild everything, and we don't know where to start,” that's worth knowing now, while nothing is on fire.
Risks and challenges when migrating off Airtable
Four things in an Airtable base don't come out of an export the way you'd expect. The rows do. Everything attached to the rows goes wrong, and each one has a fix.
Linked records
In a CSV, a link field becomes the linked records' names joined by commas. That's fine until someone renames a vendor between export and import, or two vendors share a name, and the join points to the wrong record or to nothing. “Single link” is also just a preference in Airtable, so a field you think holds one record can hold two.
The fix is to never join on names. The API returns every link as record IDs ("Vendor": ["recbqSQtfj1rxqcXx"]), and if you're stuck with CSV, you can add a RECORD_ID() formula field to each table and lookups that pull those IDs across the links. Keep the Airtable ID as a column on every new table, load parents before children, and resolve each link with a join on that column.
Count the links per record first, and any field that ever holds two becomes a junction table instead of a foreign key.
Attachments
An export gives you a URL per file, and that URL expires in about two hours (opens in a new tab). Import the column as-is, and you'll get a table full of links that look right and say “This URL has expired” when anyone clicks one.
So the files are a separate job, done while the links are live:
- Fetch each file from its export URL and hash the bytes.
- Upload it to storage you control, such as Retool Storage or your own bucket.
- Write the new URL and the hash into a documents table, keyed on the attachment's Airtable ID.
- Fetch each new URL back and check that the hash matches.
For a few files, that's a script in a browser tab. At thousands, it's a workflow that runs the same loop in batches.
Formulas and lookups
An export gives you the value a formula or lookup produced, not the rule behind it. A deadline comes out as a date. A lookup comes out as a copy of whatever the linked record said when you exported.
In a database, a lookup doesn't need a column at all. Say each review in a base shows its vendor's risk category through a lookup. After the migration, the risk category is a column on the vendor's row, and any query that needs it joins to the vendor and reads it from there. There's one copy, and it's always current.
A formula becomes an expression in whichever query needs it. If the Airtable formula was “five days after the submitted date for high-risk vendors, fourteen days for everyone else,” the SQL is:
SQL
submitted_on + case when vendor_risk = 'High' then 5 else 14 end as deadlineThen you check the rewrite. The export holds the value Airtable computed for every row, so load those values into a temporary table and run a query that compares them with your expression. Any row where the two differ is a rewrite mistake. There should be none.
Automations and permissions
These aren't in the export at all. Each automation gets rebuilt as a workflow with a trigger you pick, and each permission as a group in Retool.
The part people miss is the memory. A “when a record matches conditions, send an email” automation knows which records it already handled. Your workflow doesn't, unless you add a column like notified_at and have it pick up only rows where that's still empty. With that column, the trigger can be a schedule, a call from the app, or both, and a run that fires twice won't send anything twice.
What a proper Airtable migration requires
The basic way to get data from Airtable to Retool takes literally minutes:
- Download each table from Airtable as a CSV, or pull the base through the API.
- Open Retool Database, create a table from each file, and let the importer guess the column types.
- Fix the guesses it got wrong. Text IDs that look like dates are the usual one.
Now the records are in Postgres, and for a one-off report that's all you need. For anything people are going to work on every day, it leaves several problems untouched:
- The schema. Links are still text, and nothing references anything. Two tables that used to link to each other are now two tables.
- The files. An attachments column holds URLs that stop working within about two hours of the export.
- The calculations. A formula column holds whatever value the formula last produced. The formula itself didn't come across, and any row where it failed holds the text #ERROR.
- The app. There's nothing for anyone to open. Views, forms, and buttons all stayed in Airtable.
- The automations. Nothing fires when a record changes because automations were configured in the base, not the data.
- The permissions. Whoever can see the database resource can see every row in it.
Mapping linked records and lookups to a relational schema
Every link field in Airtable becomes one of two things. A link that only ever holds one record becomes a foreign key column. A link that can hold several becomes a junction table. You find out which by counting the links per record in the export, because the field type doesn't say.
Let's work through an example. We have a vendor onboarding base in Airtable for a procurement team that we want to migrate to Retool. It has a table for vendors, a table for reviewers, and a table for reviews that links to both. The reviews have a lookup, a formula, and an attachment field. There is also an automation that emails procurement when a review is approved.

Reviews link to one vendor, so that's a foreign key. Reviews link to several reviewers, so that's a junction table. Here's the reviews table in Postgres, with the junction table next to it:
SQL
create table reviews (
id bigint generated always as identity primary key,
airtable_id text not null unique,
review_ref text not null unique,
vendor_id bigint not null references vendors (id),
status text not null default 'Draft'
check (status in ('Draft', 'In review', 'Approved', 'Rejected')),
submitted_on date,
notes text,
notified_at timestamptz
);
create table review_reviewers (
review_id bigint not null references reviews (id) on delete cascade,
reviewer_id bigint not null references reviewers (id),
primary key (review_id, reviewer_id)
);Two things worth pointing out:
vendor_idisnot null, because a review belongs to one vendor. Airtable never enforced that, and if any review had two vendors, the load would have failed right there, which is what you want.- Every table keeps its Airtable record ID in
airtable_id. The API hands you each link as a record ID, so the insert joins on that column instead of matching on a vendor's name:
SQL
insert into reviews (airtable_id, review_ref, vendor_id, status, submitted_on, notes)
select v.airtable_id, v.review_ref, vendors.id, v.status, v.submitted_on::date, v.notes
from (values
('recARmDCQJnwI6pTc', 'R-1002', 'recbqSQtfj1rxqcXx', 'In review', '2026-08-24', 'New vendor. Security questionnaire returned with two open items.')
-- one row per review
) as v (airtable_id, review_ref, vendor_airtable_id, status, submitted_on, notes)
join vendors on vendors.airtable_id = v.vendor_airtable_id;The lookup doesn't get a column. The vendor's risk category lives on the vendor row, and a join reads it.
The formula does get rewritten. In Airtable it was:
PlainText
DATEADD({Submitted}, IF({Vendor risk} = "High", 5, 14), "days")In SQL it's:
SQL
submitted_on + case when v.risk_category = 'High' then 5 else 14 endBecause the export has Airtable's answer for every row, you can check yours against it. Ours matched on all fifteen reviews. The one with no submitted date came through as NULL where Airtable had #ERROR.
Copying the files while the links work
The attachment field exported as one URL per file, with an expiry baked into the path. We fetched one at 3 pm and got the PDF. We fetched the same one the next morning and got a 410 and the words “This URL has expired.” So copying must be done while links are live:
- Fetch each file from its export URL and hash the bytes.
- Upload it to Retool Storage, in a folder for the base, with access set to Private.
- Write the new URL and the hash into the documents table, keyed on the attachment's Airtable ID.
- Fetch each new URL back and check that the hash matches.
All four hashes matched on both ends. The new URLs only serve the file to someone with a Retool session. Without one, you get a 401.
Building the app on top
This is the part Airtable made invisible. Every view, form, and button was in the base, and none of it exported.
We can tell Retool's app builder what we want via a simple prompt: a reviews table with the deadline computed in SQL, a detail dialog with the notes, reviewers, vendor contact, document links, and an Approve button that's only enabled while the status is In review, and a vendors page with review counts.
Because Retool has the data and schema within the Retool database, it can infer exactly the app we need, along with backend functions that run parameterized SQL. Approval is one statement:
TypeScript
const result = await retoolDb.query(
`UPDATE reviews
SET status = 'Approved'
WHERE id = $1 AND status = 'In review'
RETURNING id, status`,
[reviewId]
)The status check is in the WHERE clause, so a second click updates nothing.

The document links are the Retool Storage URLs from the file copy, and they open with the user's session. The review with no submitted date shows a blank deadline instead of #ERROR.
Rebuilding automations as governed workflows
The Airtable automation was “when a review's status becomes Approved, send an email.” A Retool workflow doesn't have a trigger for “a record changed,” so you pick what starts it. We gave ours two starts.
The app calls it with the review's ID when someone clicks Approve, and a schedule runs it every fifteen minutes with no ID, to catch anything the app missed.

Both starts run the same query, which finds reviews that are approved and haven't been notified, and the same loop, which sends the email and then sets notified_at on the row to the current time:
SQL
select r.id, r.review_ref, v.name as vendor, v.contact_email
from reviews r
join vendors v on v.id = r.vendor_id
where r.status = 'Approved'
and r.notified_at is null
and r.id = coalesce({{ input.data.reviewId }}::bigint, r.id)
order by r.id;That notified_at value is what stops the schedule from emailing anyone twice, because the query only picks up rows where it's still empty. Before we turned the schedule on, we set it on the six reviews Airtable had already emailed about, so the first run did nothing.
The email went out. The notified_at update didn't. The loop wasn't passing the row into the function, so the update got {} where it expected an ID, and the log said so: invalid input syntax for type bigint: "{}". Left alone, the schedule would have sent that email again every fifteen minutes. We fixed the mapping, published a new release, approved another review, and set notified_at 6.5 seconds after the click.
Setting permissions again
Reviewers were collaborators on the Airtable base, which meant they could see and edit everything. In Retool, they get the app, and only the app. The app is shared with a group, and the group's access to the database resource is a separate setting, so nobody who reviews vendors ever sees the Query Library or the database editor.
If you need rules per table, column, or row, that's access policies on a Postgres resource, on the Enterprise plan. One thing to know before you switch enforcement on: it blocks any query a policy doesn't allow, including the workflow's queries when a run isn't tied to a user, so the notification workflow needs its own allowance first.
How to evaluate whether you're ready to migrate
You're ready when you can name the thing Airtable can't do for you and the person who'll own the replacement. Everything else on this list is a way of checking that those two answers are real:
- Record volume. If you've split a base to stay under the record cap (opens in a new tab), or you're within a quarter of doing so, the split is already a migration with none of the benefits.
- Linked-record complexity. Count the link fields and the lookups that read through them. Each one becomes a foreign key or a junction table, plus a join, and that count is most of the schema work.
- Compliance needs. If anyone has asked who changed a record and when, and the answer took longer than a minute, write down which regulation or audit is behind the question. That's the requirement.
- Automation count. List every automation, what triggers it, and what it remembers. Each one is a workflow to build and test, and the ones that remember state need a column.
- Active users per base. If people outside the team that built the base use it every day, its permissions are already wrong. They have a collaborator seat on a base when they need one screen.
- Vendor-risk tolerance. Decide how long you could tolerate a pricing or feature change you didn't choose, and whether you'd rather find out your exit path now or then.
Access control is the item that decides the most on its own. In Airtable, access is set per base by whoever owns it. In Retool, it's set once per group across every app and every database resource, and you can set row-level rules on a Postgres resource. If your security team wants a single place to answer “who can see vendor bank details,” that's the difference between the two models, and it doesn't depend on how many records you have.
The list also tells you when to stay. One team tracking its own work, under the record cap, with no auditor asking questions and no engineer who wants to own a database, is fine in Airtable. A migration without an owner produces a Postgres database nobody is watching, which is worse than the base you started with.
Migration paths from Airtable to Retool
There are two ways to get data out of Airtable, and a third way to run either of them more than once.
CSV download, by hand
Open each table, pick a view, download. Links come out as the linked records' names joined by commas, lookups and formulas come out as values, attachments come out as filename (url). This is fine for a small base with no link fields, where every table stands alone. With links, it only works if you first add a RECORD_ID() formula field to each table and lookup fields that pull the IDs across each link, which means editing the base you're leaving.
The API, scripted
A script lists every table's records a page at a time and saves the JSON. Links arrive as record IDs, attachments as objects with a filename, size, and URL, and the schema endpoint gives you each formula's text. The limits are 5 requests per second per base (opens in a new tab) and 100 records per page, so a base with 50,000 rows across a few tables is a few hundred requests. Ours took four.
A workflow that does it on a schedule
Put the same API calls in a Retool workflow that writes into Retool Database, and you can rerun the load every night while the two systems overlap, then run it one last time at cutover. That's the closest thing to an assisted migration, and it's what you want when a write freeze isn't an option.
Here’s how the two export methods compare:
- Links: A CSV download gives you names, or record IDs if you add helper fields first. The Web API gives you record IDs.
- Formulas and lookups: A CSV download gives you values only. The Web API gives you values, plus the formula text from the schema endpoint.
- A failed formula: A CSV download shows the text
#ERROR. The Web API returns{"error": "#ERROR"}. - Attachments: A CSV download puts
filename (url)in a single cell. The Web API returns an object per file with its filename, size, and URL. - Repeatable: A CSV download isn’t repeatable. The Web API is, from a script or a workflow.
Whichever path you take, the same three things are done by hand and are where migrations go wrong: mapping links to foreign keys, rebuilding automations, and setting permissions.
How Retool approaches Airtable migrations
After the migration, your data sits in a Postgres database you own, with the constraints you chose. Each screen is a Retool app with queries you can read, and each automation is a Workflow that logs every run. Access and audit live in one place: permission groups set once for your organization, audit logs on Business and Enterprise, and access policies for row-level rules on Enterprise. You still configure all of it, but admins can see it in one place instead of piecing it together base by base.