+1 (415) 612-6492

Migrating from Google Sheets to Airtable: A Field-by-Field Tutorial

Most Airtable projects start as a spreadsheet that outgrew itself: forty columns, colour-coded rows, three people editing the same tab, and a VLOOKUP nobody dares touch. Moving it into Airtable is not a copy-paste job. A good migration turns columns into typed fields, tabs into related tables, and manual rituals into automations.

This tutorial walks the whole path on a realistic example and flags the decisions that cause rework if you get them wrong on day one.

The example

A Google Sheet called Client Work with three tabs:

  • Projects — client name, project name, status, owner, start date, due date, fee, notes.
  • Time Log — date, person, project name (typed by hand), hours, billable?
  • Contacts — name, email, company, phone.

That is three entities (projects, time entries, people/companies) held together by typed text. Airtable's job is to make those relationships real.

Step 1: audit before you import

Spend thirty minutes in the sheet, not in Airtable. For each column, write down:

  1. What it really holds. A Status column with values Active, active, ACTIVE - urgent, and on hold? is one field with dirty data, not four statuses.
  2. Whether it is entered or calculated. Calculated columns should become formula fields or rollups, never imported values.
  3. Whether it repeats. Client name appearing on 400 rows means clients are an entity that deserves its own table.
  4. Merged cells, header rows, and blank spacer rows. These break every importer. Flatten them now.

A quick clean-up pass in Sheets pays for itself: use Data > Data cleanup > Trim whitespace, then a pivot table or =UNIQUE() on each categorical column to see the real value list. Fix casing and typos in the sheet where find-and-replace is cheap.

Step 2: map columns to Airtable field types

This is the step that decides whether your base feels native or feels like a spreadsheet in a costume.

Sheet columnAirtable field typeWhy
Client name (repeats)Link to another record → ClientsEnables rollups, filters, and one place to fix a name
Status (short value list)Single selectValidation, colour, groupable, usable in automation conditions
Owner (a person on the team)CollaboratorPowers "my records" views, Interface filters, and notifications
Tags (comma-separated)Multiple selectSplit on comma at import time
FeeCurrencyFormatting plus correct rollup maths
Start / Due dateDateSet the field's timezone behaviour deliberately
Billable? (TRUE/yes/1)CheckboxNormalise the source values first
NotesLong textEnable rich text only if you actually need it
Row-level total, days-late, marginFormulaRecalculated forever, never stale
Sum of hours per projectRollupLives on the parent, driven by children
Sheet row number, ad-hoc IDsAutonumber / formulaDo not import a spreadsheet's row numbers as data

Two type choices deserve extra thought:

Single select vs link to another record. If the value has attributes of its own (a client has an address, a contact, an owner), it is a record. If it is a flat label with under roughly twenty stable options, it is a select. Getting this wrong is the single most common cause of a base rebuild.

Collaborator vs text for people. Use collaborator for team members who log in; that unlocks personal views, record assignment, and "notify assignee" automations. Use a linked People table for contractors or contacts who do not have base access.

Step 3: design the tables before importing anything

For the example, the target schema is:

  • Clients — Name (primary), Domain, Owner, Status.
  • Contacts — Name (primary), Email, Phone, Client (link → Clients).
  • Projects — Name (primary), Client (link), Owner (collaborator), Status (select), Start, Due, Fee (currency), Hours logged (rollup), Notes.
  • Time Entries — Entry ID (autonumber or formula primary), Date, Person, Project (link), Hours, Billable.

Create the tables and fields empty first. Importing into a table whose fields already exist and are already typed avoids Airtable guessing types for you, and guessing is where phone numbers become numbers and 03/04 becomes a date in the wrong locale.

Also decide the primary field for each table now. It should be human-readable and as unique as you can make it. For Time Entries, a concatenation such as 2026-03-04 — Priya — Website Rebuild beats a bare date.

Step 4: import the data in dependency order

Import parents first, children second: Clients, then Contacts, then Projects, then Time Entries. Airtable can create linked records from text on import, but only if the target record already exists — otherwise you get duplicates with slightly different spellings.

Per tab:

  1. In Sheets, File > Download > Comma-separated values for that tab only.
  2. In Airtable, open the target table and use Add or import > CSV file, choosing to append into the existing table.
  3. In the mapping screen, match every column explicitly. Set any column you do not want to "Do not import" rather than letting it create a stray field.
  4. Import a 10-row test slice first. Check dates, decimals, links, and select options, then delete the test rows and run the full file.

For linked columns, make sure the source text matches the parent's primary field exactly. A quick =TRIM(PROPER(A2)) helper column in Sheets, or a lookup against your cleaned client list, removes most mismatches before they become records.

For multiple-select columns, Airtable splits on commas during import. If your source uses semicolons or slashes, convert them first.

When a script beats the importer

Use the Scripting extension when you need transformation the importer cannot do: splitting one sheet into two tables in a single pass, generating deterministic keys, or matching to existing records by email. A sketch:

const rows = await input.fileAsync('Upload CSV'); // or paste JSON
const clients = base.getTable('Clients');
const projects = base.getTable('Projects');

// build a lookup of existing clients by lowercased name
const existing = new Map();
for (const r of (await clients.selectRecordsAsync({fields: ['Name']})).records) {
    existing.set((r.name || '').trim().toLowerCase(), r.id);
}

const toCreate = [];
for (const row of rows) {
    const key = (row['Client name'] || '').trim().toLowerCase();
    let clientId = existing.get(key);
    if (!clientId) {
        clientId = (await clients.createRecordsAsync([{fields: {Name: row['Client name'].trim()}}]))[0];
        existing.set(key, clientId);
    }
    toCreate.push({fields: {
        Name: row['Project name'],
        Client: [{id: clientId}],
        Status: {name: row['Status']},
        Fee: Number(row['Fee']) || null,
    }});
}

// createRecordsAsync accepts a maximum of 50 records per call
for (let i = 0; i < toCreate.length; i += 50) {
    await projects.createRecordsAsync(toCreate.slice(i, i + 50));
}

The same 50-record batching rule applies to the REST API, so this pattern ports directly if you would rather run the migration from Node or Python.

Step 5: rebuild formulas, do not port them

Spreadsheet formulas reference cells; Airtable formulas reference fields on the current record. Common translations:

  • VLOOKUP / INDEX(MATCH()) → a link field plus a lookup field.
  • SUMIF / COUNTIF across a child sheet → a rollup on the parent (SUM(values), COUNTALL(values)).
  • IF(ISBLANK(...)) chains → IF({Field} = BLANK(), ...) or SWITCH() for cleaner branching.
  • TODAY() - A2 for ageing → DATETIME_DIFF(TODAY(), {Due}, 'days').
  • CONCATENATE& or CONCATENATE(), with \n handling via Notes & "\n" & More.
  • Conditional formatting rules → coloured views, or a formula field that emits an emoji flag such as IF(DATETIME_DIFF(TODAY(), {Due}, 'days') > 0, "🔴", "🟢").

Rules that lived in a human's head ("we highlight it yellow when the client hasn't replied in a week") are the best candidates for automations: a scheduled trigger, a conditional check, and a Slack or email nudge.

Step 6: de-duplicate and reconcile

Before anyone trusts the base, prove the numbers.

  1. Row counts. Compare each table's record count against the source tab's row count minus headers and blanks. A grouped view by an imported Source batch field makes this easy — add that field on purpose and stamp each import.
  2. Financial totals. Sum the Fee column in Sheets and the same field in Airtable's summary bar. They should match to the cent.
  3. Orphans. Create a filtered view where the link field is empty. Every orphan is a spelling mismatch you need to resolve.
  4. Duplicates. Add a formula key such as LOWER(TRIM({Email})), group by it, and look for groups larger than one. The Deduplicate extension does this interactively; for a one-off clean-up it is usually faster than a script.
  5. Spot-check 10 random records field by field against the sheet. Automated checks miss silent type coercions; eyes do not.

Step 7: sync or migrate? Pick one deliberately

If the spreadsheet must stay alive — a partner owns it, or a tool exports to it nightly — do not migrate it. Use Airtable Sync to pull the sheet in as a read-only synced table, then link your editable records to it. Synced tables cannot be edited in Airtable, which is a feature, not a limitation: it keeps one system of record.

Migrate outright when Airtable becomes the system of record. In that case the old sheet must die, or people will keep using it. Practical hygiene:

  • Move the sheet to an archive folder and rename it ARCHIVED 2026-03-04 — Client Work (read only).
  • Set it to view-only for everyone except the admin.
  • Put a link to the new Airtable interface in cell A1 of every tab.

Step 8: build the interface, then cut over

Nobody adopts a base by looking at a grid. Before cutover, build one Interface page per role: a project list with filters for delivery, a "log my hours" form for the team, a client summary for the account lead. Grid views stay for admins; everyone else lives in interfaces.

A cutover that works:

  1. Freeze the sheet at a known time (end of a Friday, or the end of a billing period).
  2. Final delta import of any rows added since your test import.
  3. Reconcile counts and totals one more time.
  4. Train for twenty minutes on the interfaces, not the schema.
  5. Run parallel for one week in read-only mode on the sheet, so anyone who says "the old numbers were different" can be shown otherwise.
  6. Set permissions, then archive the sheet for good.

Mistakes we clean up most often

  • One giant table. Everything in Master, with 60 fields and a Record type select. Split by entity; link them.
  • Text where links belong. Client names typed into every row means no rollups and endless typos.
  • Importing calculated columns as values. They are stale the moment they land.
  • Attachments left in Drive links. If files matter, move them into attachment fields during migration, or accept forever that half of them will break.
  • Skipping the primary field decision. A base full of records named Untitled or 1, 2, 3 is unusable in linked-record pickers.
  • No import batch stamp. Without it, rolling back a bad import means hand-picking rows.

Checklist

  • Source columns audited; entered vs calculated separated
  • Categorical values normalised in the sheet
  • Target tables and typed fields created empty, parents first
  • Primary fields chosen and human-readable
  • 10-row test import verified per table
  • Links resolve; zero orphans in the "link is empty" view
  • Formulas rebuilt as formula, lookup, and rollup fields
  • Counts and financial totals reconciled
  • Duplicates resolved with a normalised key
  • Interfaces built per role and permissions set
  • Old sheet archived, renamed, and read-only

Migrations look small until you are three tabs deep in mismatched client names. If you would rather hand over the sheet and get back a base that reconciles to the cent, get in touch — data migration to Airtable is one of the things we do every week.