Migrate from Salesforce to Relm
Salesforce exports cleanly to CSV, and Relm is built to be written by an AI agent. Hand your agent the export files and a Relm API key, and it maps Accounts, Contacts, Leads and Opportunities into an API-first CRM in one run - no import wizard, no field-mapping screens, no consultants.
This guide is the full recipe: getting your data out of Salesforce, the exact object-to-object mapping, the one-time setup calls, the batch import itself, and how to verify the result. Every step works by hand with curl, but the point is that you should not have to - hand it to Claude, ChatGPT or any MCP client as a prompt and the agent executes the whole thing.
Why teams move a Salesforce org to Relm
Salesforce is the deepest CRM ever built. If your org runs on territory management, CPQ, enterprise permissioning and an AppExchange stack, none of this article applies - stay where you are. The teams that migrate to Relm look different: small, technical, and increasingly operating the CRM through AI agents rather than through screens. For them the contrasts are practical, not ideological:
- Per-seat pricing versus per-request pricing. Salesforce editions run from $25 to several hundred dollars per user per month, and the model assumes humans in seats. An AI agent is not a seat. Relm charges for API requests: free for 1,000 a month, $29 for 100,000, $249 for 2,000,000 - see pricing.
- API access is edition-gated. In Salesforce, full API access comes with the higher editions and is an add-on below that. In Relm the API is the product on every plan, including Free, plus a native MCP server at
https://api.relmcrm.com/mcp. - Configuration weight. Profiles, permission sets, page layouts and validation rules are the right tools for a 500-person org with an admin team. A four-person team with an agent doing the data entry needs a schema it can read in one call:
GET /v1/schema. - Agent ergonomics. Salesforce's REST and Bulk APIs are powerful but assume an integration engineer. Relm assumes the caller is a model: self-describing schema, RFC-9457 errors with
valid_optionsandsuggestion, prefixed IDs, idempotent writes. The full case is in what is a CRM for LLMs.
Step 1 - Export your data from Salesforce
Three well-trodden paths, in order of convenience:
- Data Export Service - Setup, then Data Export. Salesforce generates a zip of CSV files, one per object, on a scheduled cadence that depends on your edition. This is the simplest complete snapshot.
- Data Loader or the Bulk API - on-demand CSV extraction of specific objects, the right tool if you want a fresh pull the day you migrate.
- Report exports - for small orgs, exporting a tabular report per object to CSV works fine.
You want one CSV per object: Account.csv, Contact.csv, Lead.csv, Opportunity.csv, and Task.csv (plus Event.csv if you log meetings). If you use contact roles on deals, grab OpportunityContactRole.csv too. Two things to know about the files: custom fields appear as columns suffixed __c, and every row carries the Salesforce record ID (Accounts start with 001, Contacts 003, Opportunities 006, Leads 00Q). Keep those IDs - the import below stores them in a custom field so every Relm record stays traceable to its source row.
Step 2 - The field mapping
Relm's model is four core objects - contacts, companies, deals, activities - plus named pipelines and typed custom fields. Here is the whole map:
| Salesforce | Relm | Notes |
|---|---|---|
AccountName, Website, Industry | companyname, domain | Industry and other extra columns go under custom_fields. |
ContactFirstName, LastName, Email, Phone, Title, AccountId | contactfirst_name, last_name, email, phone, company_id | Title becomes custom_fields.title; AccountId resolves to the new cmp_ id. |
Lead (unconverted)Status, LeadSource, Company, Rating | contact with type: "lead" + a deal in a leads pipeline | The Status picklist becomes the pipeline's stages 1:1. LeadSource and Rating go under custom_fields. Skip rows where IsConverted is true - they already exist as Contacts and Opportunities. |
OpportunityName, Amount, StageName, CloseDate, AccountId | dealtitle, value_cents, stage, close_date, company_id | Amount is a decimal in dollars; value_cents is integer minor units, so multiply by 100. StageName picklist maps 1:1 to pipeline stages. The primary contact role becomes primary_contact_id. |
Task / EventSubject, Description, ActivityDate, WhoId, WhatId | activitytype, body, contact_id, deal_id, occurred_at | Task Type values like Call and Email map to activity types; ActivityDate goes into occurred_at so history keeps its dates. |
Custom fieldsLead_Score__c, ... | custom_fields | Register each once via POST /v1/fields, then send values under custom_fields. Picklists become select fields with enum values; numbers stay typed. |
Step 3 - One-time setup: fields and pipelines
Before loading rows, register the Salesforce columns Relm does not have built in, and create a pipeline whose stages are your StageName picklist, in order:
# 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": "sf_id", "data_type": "text" }'
# repeat for contact.title, contact.lead_source, company.industry - and
# sf_id on each object you import (fields are registered per object)
# a pipeline whose stages ARE your Opportunity StageName picklist
curl -X POST https://api.relmcrm.com/v1/pipelines \
-H "Authorization: Bearer relm_test_..." -H "Content-Type: application/json" \
-d '{ "key": "salesforce", "name": "Sales (Salesforce)", "stages": [
{ "key": "prospecting", "label": "Prospecting" },
{ "key": "qualification", "label": "Qualification" },
{ "key": "needs_analysis", "label": "Needs Analysis" },
{ "key": "proposal", "label": "Proposal/Price Quote" },
{ "key": "negotiation", "label": "Negotiation/Review" },
{ "key": "closed_won", "label": "Closed Won" },
{ "key": "closed_lost", "label": "Closed Lost" }
]}'
Create a second pipeline the same way for unconverted Leads, using your Lead Status values as stages. Re-running the setup is safe: field registration is idempotent on the key, a duplicate pipeline key returns a 409 instead of a second pipeline, stages are validated before any write, duplicate stage keys are rejected, and deals are always checked against the stages of their own pipeline.
Step 4 - Let the agent run the import
Point any MCP client - Claude, Cursor, a custom agent - at https://api.relmcrm.com/mcp with your bearer key (setup details in the docs and the agent hub), then give it the job in plain language:
Import my Salesforce export into Relm. The CSVs are in ./sf-export
(Account.csv, Contact.csv, Lead.csv, Opportunity.csv, Task.csv).
1. Call relm_describe_schema first.
2. Register custom fields for Title, LeadSource, Industry and the
Salesforce record ID (sf_id) via the fields endpoint.
3. Create a "salesforce" pipeline from my Opportunity StageName values
and a "leads" pipeline from Lead Status. Map stages 1:1.
4. Load everything with relm_batch, up to 100 operations per call, in
dependency order: companies, then contacts, then deals, then activities.
5. Convert Amount to value_cents (x100), set occurred_at from ActivityDate,
and skip Leads where IsConverted is true.
6. Use my test key. When counts match the CSVs, rerun against the live key.
Under the hood every write lands on POST /v1/batch: up to 100 operations per call, with a per-operation result array, so one bad row fails alone instead of failing the batch. Each operation is metered against your quota exactly like a single call - batch saves round-trips, not money. Here is what one batch looks like with real Salesforce-shaped data:
curl -X POST https://api.relmcrm.com/v1/batch \
-H "Authorization: Bearer relm_test_..." \
-H "Content-Type: application/json" \
-d '{ "operations": [
{ "object": "company", "data": { "name": "Globex Corporation", "domain": "globex.com",
"custom_fields": { "industry": "Manufacturing", "sf_id": "001Kd00000rTxWQAU" } } },
{ "object": "contact", "data": { "first_name": "Ada", "last_name": "Lovelace",
"email": "[email protected]", "phone": "+1 415 555 0143",
"company_id": "cmp_...",
"custom_fields": { "title": "VP Engineering", "lead_source": "Web",
"sf_id": "003Kd00000pQrStAB" } } },
{ "object": "deal", "data": { "title": "Globex - Annual Renewal",
"value_cents": 4800000, "currency": "USD",
"pipeline": "salesforce", "stage": "negotiation",
"close_date": "2026-09-30", "company_id": "cmp_...",
"primary_contact_id": "con_..." } },
{ "object": "activity", "data": { "type": "call",
"body": "Renewal call - pricing agreed, legal review next",
"contact_id": "con_...", "occurred_at": "2026-05-18T16:00:00Z" } }
]}'
Records get server-assigned IDs, and operations inside one batch cannot reference each other's new IDs - so the agent loads in dependency order across batches: companies first, then contacts referencing cmp_ IDs, then deals referencing con_ IDs, then activities. If it sends a stage or field Relm does not know, the 422 response includes valid_options and suggestion, so it corrects itself in one turn instead of guessing. Two more guardrails matter at import scale: a duplicate contact email returns a 409 conflict carrying the existing record, so messy exports and re-runs do not create duplicates, and on single-record writes an Idempotency-Key header makes a retried call return the original result (POST /v1/batch does not take one - batch re-runs lean on the duplicate checks). One thing batch does not switch off: automation rules and sequence enrollments fire on created records like any other write, so pause any on-create rules before the load (PATCH /v1/automations/{id} with enabled: false) and re-enable them after.
Step 5 - Verify before you switch anything off
Run the entire migration against a relm_test_ key first. Test mode is free, unlimited, isolated from live data and invisible to billing - the agent can make every mistake there at zero cost. Then verify three ways:
- Counts. Sum the
succeededfield of each batch response, or page through the list endpoints (GET /v1/contacts?limit=100with cursor pagination) and compare totals against your CSV row counts, minus skipped converted Leads. - Spot-checks. The list endpoints take a
qsubstring filter:GET /v1/contacts?q=globexmatches email, first name, last name, phone and LinkedIn URL; on companies it matches name and domain; on deals it matches the title. Pick ten records you know well in Salesforce and confirm each one landed with the right stage, amount and history dates. - The dashboard. Live records show up at app.relmcrm.com - a fast human sanity pass over the kanban view catches anything the numbers missed.
When test-mode counts match, swap the key prefix to relm_live_ and rerun. The calls are identical; only the destination changes.
What the migration costs
Budget roughly one API request per record. A typical small org - 2,000 Accounts, 6,000 Contacts, 1,500 Opportunities, 10,000 Tasks - is about 20,000 requests, well inside Pro at $29/mo for 100,000. The Free plan's 1,000 requests, no card required, cover a small org or a trial slice of a big one, and the test-mode rehearsal costs nothing at any size. Scale at $249/mo covers 2,000,000 requests, and paid plans meter overage instead of hard-stopping. Day-to-day agent usage after the import is usually a fraction of the migration itself - see how to give your AI agent a CRM.
FAQ
How do I export my data from Salesforce for the migration?
Use the built-in Data Export Service (Setup, then Data Export), which produces a zip of CSV files, one per object. For on-demand pulls, Data Loader or the Bulk API export specific objects to CSV, and small orgs can simply export reports. The agent only needs one CSV per object: Account, Contact, Lead, Opportunity, and Task or Event.
What happens to Salesforce Leads in Relm?
Relm has no separate Lead object. Each unconverted Lead becomes a contact with type: "lead", and its Status picklist becomes the stages of a dedicated leads pipeline, so a deal per lead preserves where it was in qualification. Leads already converted in Salesforce are skipped, because they exist in the export as Contacts and Opportunities.
How are Opportunity stages mapped?
One call to POST /v1/pipelines creates a pipeline whose ordered stages are your exact StageName picklist values, mapped 1:1. Deals are validated against the stages of their own pipeline, and a wrong stage returns a 422 with valid_options and suggestion so the agent corrects itself.
What about custom fields ending in __c?
Register each one once with POST /v1/fields, giving it a key and a data type, then send values under custom_fields on every record. Types are enforced - a number field rejects text, so bad data surfaces during the import instead of later. Salesforce picklists map to select fields with enum values.
Is it safe to re-run the import?
Yes. Creating a contact with an email that already exists returns a 409 conflict that includes the existing record, so re-runs do not create duplicates. On single-record writes an Idempotency-Key header makes a retried call return the original result; POST /v1/batch does not take one, so batch re-runs lean on the duplicate checks. And the whole load should be rehearsed first with a relm_test_ key, which is free and invisible to live data.
How many API requests does a Salesforce migration take?
Roughly one request per record: each operation inside POST /v1/batch is metered individually, so batching saves round-trips, not quota. The Free plan includes 1,000 requests a month with no card, Pro is $29/mo for 100,000 and Scale is $249/mo for 2,000,000, with metered overage on paid plans. Test mode is free and unlimited.
Bring your Salesforce data with you
Mint a free test key and let your agent rehearse the whole migration at zero cost.
Start free →