Migrate from Airtable (or Google Sheets) to Relm
Your Airtable base or Google Sheet already is a CRM: names, a Status column, a notes field, maybe a Kanban view. Moving it into Relm is one agent run - export the CSVs, hand your AI agent the files and an API key, and it maps, loads and verifies everything through the batch API.
This guide is for the spreadsheet-as-CRM crowd. You never bought a CRM; you built one out of a table, and it worked - which is exactly why you now have 2,000 rows, a Status single-select doing the job of a pipeline, and an AI agent that has to parse a grid to find out who to follow up with. The migration below turns that table into typed CRM objects an agent can operate directly - no hand-copying.
Why move off the spreadsheet
Airtable is a superb general-purpose database, and Google Sheets is the most flexible tool ever shipped. Nothing here is a knock on either. But a spreadsheet used as a CRM has structural limits that show up exactly when an AI agent becomes the main operator:
- Rows are not objects. A row that mixes a person, their company and a deal into one line has no identity an agent can hold. Relm splits them into contacts, companies, deals and activities with prefixed IDs (
con_,cmp_,deal_,act_), so "update Maya's deal" is one precise call, not a row hunt. - Nothing stops duplicates. Paste the same lead twice and you have two rows forever. Relm dedupes contacts by email at write time - the duplicate comes back as a 409 with the existing record attached.
- A Status column is not a pipeline. A single-select can hold "Qualified", but nothing validates it, orders it, or fires anything when it changes. Relm pipelines have ordered stages and emit events automations can act on.
- Formulas compute; they don't act. A formula can flag a stale lead but can't send the follow-up. Event-triggered automations and an agent with API access can.
- The agent contract is weak. Both tools have APIs, but they describe cells and records, not CRM semantics. Relm's API is the product - self-describing via
GET /v1/schema, with errors a model can act on. That is the whole argument of what a CRM for LLMs is.
Step 1: export your data
In Airtable, exports are per table: open each table in a grid view and download it as CSV from the view menu. A base with Contacts, Companies and Deals tables produces three files. Three export behaviors matter for the mapping:
- Formula, rollup and lookup fields export as computed values, not logic. What you get is a snapshot.
- Linked-record fields export as the linked record's primary field - the name, not an ID. "Brightloop" in the Company column of your Contacts export is a string your agent will match against the Companies file.
- Attachment fields export as URLs - and Airtable's attachment URLs are temporary signed links that expire within hours. Download any files you actually need, re-host them somewhere durable, and keep that location in a custom field or a note; Relm does not store files.
In Google Sheets, download each tab as CSV (File, then Download). One tab per file, same idea. Put everything in one folder - that folder plus an API key is the entire input.
Step 2: the mapping
Here you decide what your tables were pretending to be. The good news: almost every spreadsheet CRM converges on the same shapes.
| Airtable / Sheets source | Relm object / field | Notes |
|---|---|---|
| Companies table, or a Company text column | company: Name → name, Website → domain | Create these first; everything else links to them. Dedupe company rows in the CSV before loading - the API creates one company per call. |
| Contacts / Leads table | contact: Full Name split into first_name + last_name, Email → email, Phone → phone, LinkedIn → linkedin_url, linked Company → company_id | Email is optional - blank cells are fine. Company names resolve to the cmp_ IDs created in the previous pass. |
| Status single-select (New, Contacted, Qualified, Won, Lost...) | a pipeline whose stages are your options, 1:1; each row becomes a deal in its stage | One POST /v1/pipelines call. Your Kanban view grouped by Status becomes a real board. |
| Deal Value / Amount (currency field) | deal: value_cents | Multiply by 100; currency defaults to USD. |
| Notes long-text, call logs, Last Contacted dates | activity (note / call) with occurred_at | Backdate to the original date so history stays history. |
| Custom columns: Source, Lead Score, Tier, tags | custom_fields on the matching object | Register each once via POST /v1/fields, then send under custom_fields. Typed - a number field rejects "high". |
| Formula / rollup / lookup columns | usually drop; keep important ones as a custom_fields snapshot | The logic is replaced by automations and agent queries (see below). |
| Attachment columns | URLs into a text custom field or an activity note | Exported URLs expire - re-host files you care about and store the durable link. |
Step 3: rehearse in test mode
Do the first run with a relm_test_ key. Test mode writes to an isolated dataset that is free, unlimited, and invisible to live data and billing - a full-dress rehearsal with zero risk. When the numbers check out, swap in the relm_live_ key and run the identical pass.
Step 4: one-time setup
Two setup calls before any rows load - register the custom fields your columns map to, and create a pipeline whose stages are exactly your Status options. Re-running them is safe: field registration is idempotent on the key, and a duplicate pipeline create returns a 409 you can ignore:
# register custom fields once, then send them under custom_fields
curl -X POST https://api.relmcrm.com/v1/fields \
-H "Authorization: Bearer relm_test_..." -H "Content-Type: application/json" \
-d '{ "object": "contact", "key": "source", "data_type": "text" }'
curl -X POST https://api.relmcrm.com/v1/fields \
-H "Authorization: Bearer relm_test_..." -H "Content-Type: application/json" \
-d '{ "object": "contact", "key": "lead_score", "data_type": "number" }'
# a pipeline whose stages ARE your Status options, 1:1
curl -X POST https://api.relmcrm.com/v1/pipelines \
-H "Authorization: Bearer relm_test_..." -H "Content-Type: application/json" \
-d '{ "name": "Leads", "stages": ["new", "contacted", "qualified", "won", "lost"] }'
Step 5: hand it to your agent
Point any MCP client - Claude, Cursor, anything that speaks the protocol - at Relm's MCP server at https://api.relmcrm.com/mcp with your key in the Authorization header (config snippet in the docs), give it the export folder, and prompt:
Import my Airtable CRM into Relm from the CSVs in ./export.
1. Call relm_describe_schema first and read what exists.
2. Create a pipeline named "Leads" whose stages are exactly the
options in my Status column: New, Contacted, Qualified, Won, Lost.
3. Register custom fields: contact.source (text), contact.lead_score (number).
4. Load in dependency order: companies first, then contacts (resolving
the Company column to company_id), then one deal per row that has a
Status, then Notes as activities with occurred_at = Last Contacted.
5. Use relm_batch, 100 operations at a time. Rows without an email
are fine - import them anyway.
6. When done, report counts per object and every failed row.
The dependency order matters because IDs are server-assigned: companies → contacts → deals → activities. Within a single batch, operations cannot reference each other's new IDs, so the agent runs one pass per object type and carries the returned IDs forward. If it sends an unknown stage or field, the 422 comes back with valid_options and a did-you-mean suggestion, and it self-corrects - the same contract described in how to give your AI agent a CRM.
Under the hood: what the agent sends
POST /v1/batch takes up to 100 operations per call and returns a per-op result array - one bad row does not fail the batch. Here is a mid-import batch with spreadsheet-realistic values; the cmp_ and con_ IDs were returned by earlier passes:
curl -X POST https://api.relmcrm.com/v1/batch \
-H "Authorization: Bearer relm_test_..." \
-H "Content-Type: application/json" \
-d '{ "operations": [
{ "method": "create", "object": "contact",
"data": { "first_name": "Maya", "last_name": "Chen",
"email": "[email protected]",
"company_id": "cmp_9k2f8x3mq1",
"custom_fields": { "source": "Webinar", "lead_score": 82 } } },
{ "method": "create", "object": "deal",
"data": { "title": "Brightloop - annual", "pipeline": "leads",
"stage": "qualified", "value_cents": 480000,
"primary_contact_id": "con_7d4h2n8pw5" } },
{ "method": "create", "object": "activity",
"data": { "type": "note", "contact_id": "con_7d4h2n8pw5",
"body": "Asked for annual pricing (Notes column)",
"occurred_at": "2026-03-14T00:00:00Z" } }
]}'
{ "object": "batch", "count": 3, "succeeded": 3, "failed": 0, "results": [...] }
Two guardrails keep re-runs safe. Creating a contact whose email already exists returns a 409 conflict with the existing record in the response, so a second pass resolves to the same contact instead of duplicating it. For rows without a natural key - deals, activities, no-email contacts - send them as individual creates with an Idempotency-Key header per row: the header applies to POST /v1/<object> calls (not to operations inside a batch), and a retried write replays the original result. If the agent dies at row 1,400, re-run only the rows the batch results marked failed or never sent, not every batch from the top.
One honest note on metering: every batch operation counts against your monthly quota exactly like an individual call - batching saves round-trips, not quota. A typical spreadsheet CRM (500 contacts, 150 companies, 500 deals, 800 notes) is roughly 2,000 requests. Free covers 1,000 requests a month with no card; Pro is $29/mo for 100,000; Scale is $249/mo for 2,000,000.
Step 6: verify your data
Do not trust "done" - check it. Three cheap passes:
- Counts. Every batch response reports
count,succeededandfailed- the agent sums them per object and compares against the CSV row counts. As a second opinion, page through the list endpoints (GET /v1/contacts?limit=100and follownext_cursor) and tally. - Spot-checks. The list endpoints take a
q=substring filter:GET /v1/contacts?q=mayamatches email, first and last name, phone and LinkedIn URL;GET /v1/companies?q=brightloopmatches name and domain;GET /v1/deals?q=annualmatches title. Pick ten rows you know and have the agent read back each one's stage, value and custom fields. - Failures. Any failed op in the results array carries a machine-readable error with the reason. Typed custom fields make bad data surface here, at import time - a
lead_scoreof "high" fails loudly instead of hiding in your CRM for a year.
Once test mode passes all three, run the same import with the live key, spot-check again, and archive the spreadsheet.
What replaces formulas and views
The parts of Airtable you actually liked have direct equivalents, just active instead of passive. Views become queries: an agent with the API or MCP answers "who have we not touched in 30 days?" on demand, with no saved view to maintain. Formulas that computed state become automations (/v1/automations): event-triggered rules that move a deal's stage, set a field, log an activity or send an email - they act, rather than recompute. The Kanban view is now a first-class pipeline your agent moves deals through one call at a time. The machine-readable manifests for all of it live in the agent hub.
FAQ
How do I get my data out of Airtable or Google Sheets?
Airtable exports are per table: open each table in a grid view and download it as CSV. Formulas, rollups and lookups export as computed values; linked records export as names, not IDs. In Google Sheets, download each tab as CSV. Put all the files in one folder for the agent.
Will re-running the import create duplicates?
Not if you lean on the guardrails. A duplicate contact email returns a 409 conflict that includes the existing record, so re-runs resolve to the same contact. For everything else, send an Idempotency-Key header per record on the individual create endpoints - operations inside /v1/batch are not individually idempotent - and a retried write replays the original result instead of writing twice.
What happens to my Airtable formulas, rollups and views?
They export as static computed values. Keep the ones that matter as typed custom fields, then replace the logic: automations cover event-driven updates, agent queries with q= replace saved views, and a Kanban view grouped by Status becomes a real pipeline.
How many API requests does a migration use, and what does it cost?
Each batch operation is metered like an individual call. A 500-contact spreadsheet CRM with companies, deals and notes lands around 2,000 requests. Free covers 1,000 requests a month with no card; Pro is $29/mo for 100,000; Scale is $249/mo for 2,000,000. Test-mode rehearsals are free and never counted.
Can I rehearse the migration without touching live data?
Yes - run the whole thing first with a relm_test_ key. Test mode is free, unlimited and completely isolated from live data and billing. When it checks out, run the identical pass with the live key.
What about rows with no email address?
They import fine. Contacts are email-optional: name-only, phone-only, LinkedIn-only or company-only contacts are valid; only a completely empty one is rejected. Migrate everyone now, enrich later.
Retire the spreadsheet
Mint a free test key, point your agent at the CSVs, and rehearse the whole migration today. See pricing.
Start free →