CRM migration
Migrating from Salesforce to HubSpot without losing history
Converted leads, stage history, files and logic in Flows or Apex: where Salesforce migrations go wrong and how to make them repeatable.
To migrate from Salesforce to HubSpot without losing history you need three things: a documented mapping of every object and field, the Salesforce ID stored on every HubSpot record and a full test load before cutover. With those in place, accounts, opportunities, activities and attachments arrive with their original dates and relationships, and you can repeat the process as many times as needed without duplicating anything.
Below is the method we follow on every HubSpot migration, with the points where Salesforce migrations tend to go wrong: converted leads, stage history, files and the logic that lives in Flows and Apex.
What "not losing history" means in Salesforce
When someone asks to keep the history, they almost never mean just the records. They want to open a customer record and see what was discussed three years ago, which opportunity was lost and why, who was handling it and which contract was attached. In Salesforce that information is spread across many objects: Task and Event for activities, EmailMessage for emails, ContentDocument and ContentVersion (or the older Attachment) for files, OpportunityHistory for stage changes and the Field History tables for tracked fields.
Before mapping, take an inventory: how many records there are per object, which fields actually contain data, which custom objects are in use and which automations depend on them. In orgs with years of use it is normal to find fields nobody has filled in for ages and objects created for a project that no longer exists. Not everything deserves to travel, and what does not travel does not need validating.
How Salesforce objects map to HubSpot
The basic correspondence is well known, but every row hides decisions worth putting in writing before writing any code.
| Salesforce | HubSpot | Decision to make |
|---|---|---|
| Account | Company | Hierarchies (ParentId) as a parent and child company association |
| Contact | Contact | Contacts linked to several accounts: labelled associations and a primary company |
| Unconverted lead | Contact (lifecycle stage) or Leads object | Leads already converted are not migrated as separate records |
| Opportunity | Deal | Pipelines, stages, lost reason, currency |
| OpportunityContactRole | Contact–deal association | Roles as labels: decision-maker, influencer, signatory |
| Product2 and OpportunityLineItem | Products and line items | Price books and per-line discounts |
| Case | Ticket | Case queues as teams or assignment rules |
| Task, Event, EmailMessage | Tasks, calls, meetings, emails | Original activity date and associations |
| Custom objects | Custom object or properties | HubSpot custom objects require an Enterprise edition |
Converted leads are the most frequent source of duplicates. When Salesforce converts a lead, the Lead record still exists with IsConverted ticked and its information moves to Contact, Account and sometimes Opportunity. If you migrate every lead without filtering, you duplicate every customer. The right approach is to migrate only unconverted leads and, if useful, carry the source data from the converted lead (source, campaign, creation date) into contact properties.
The other trap is contacts related to several accounts. HubSpot lets you associate a contact with several companies and label each association, so you do not have to choose just one, but you do need to decide which is the primary, because that is the one reports and workflows will use.
Activities, emails and files: the heaviest part
Activities make up the bulk of the volume and are the reason many migrations end up half done. In HubSpot each activity is an object (note, call, email, meeting, task) with an activity date property you can fill in with the original date. If you leave it empty, HubSpot uses the load date and the customer's timeline gets squashed into a single day.
In Salesforce, Task mixes calls, logged emails and genuine tasks: you have to read the subtype to send each one to the right object in HubSpot. Events become meetings. EmailMessage records can carry very long HTML bodies, and you need to decide whether to migrate the full body or a summary with the original attached.
Files need an extra step: download them from Salesforce, upload them to HubSpot's files tool and associate them with the record, usually through a note that contains them. It is slow and takes up space, so agree on a rule: only contracts and proposals, only active customers or only the last few years.
The HubSpot properties that record when a deal entered each stage are read-only and calculated from movements inside HubSpot. Salesforce stage history cannot be loaded into them: store the dates that matter (entered proposal, original close date) in your own properties or in a separate history record.
Owners and users: who handled each account
The owner of a record in HubSpot has to be a user in the account. Before loading anything, create the users and prepare a lookup table between the Salesforce user Id and the HubSpot owner. Reps who have left the company are the typical case: assign their records to a current manager and keep the original owner in a text property so you do not lose who handled the relationship.
Something similar happens with cases. If Salesforce distributed them through queues, decide whether HubSpot will handle that with teams, the ticket owner or assignment rules in workflows.
Is HubSpot's tool enough or do you need a script?
HubSpot offers Smart Transfer, a guided tool for bringing data in from other applications, Salesforce included. According to its documentation, it converts accounts into companies, contacts and leads into contacts, opportunities into deals and cases into tickets, carries over tasks, events, notes, emails and users, and moves attachments in a later one-off transfer. It is also clear about its limits: it does not support custom objects or lookup or formula fields, and it is designed for a one-time migration. If you are going to keep using Salesforce, HubSpot recommends the native integration and warns that combining both creates duplicates. The detail is in the official list of supported apps.
For a standard org with little customisation, Smart Transfer or HubSpot's importer may be enough. When there are custom objects, multi-level relationships, stage history to preserve or lots of files, the usual route is a script against both systems' APIs: extract with the Salesforce API or Data Loader, transform with the mapping rules and load with the HubSpot API, leaving the Salesforce Id on every record.
The method: identifiers, test loads and delta
- 01Org inventory
- 02Validated mapping
- 03Full test load
- 04Validation with users
- 05Delta and cutover
What makes the process repeatable is the source identifier. On every HubSpot object, create a "Salesforce ID" property that requires unique values and use it as the key when loading. If the second test load brings in a record that already exists, it is updated instead of duplicated, and associations are resolved by looking up that ID. As a bonus, every HubSpot record can be traced back to its origin.
The test load must be complete, not a sample of a hundred records. It is the only way to see the errors that appear at real volume: phone numbers in odd formats, retired picklist values, invalid emails, opportunities without an account. Then it is validated with counts per object and with key users opening records they know well.
On cutover day Salesforce is frozen, the delta is loaded (whatever was created or modified since the last load, filtering by LastModifiedDate) and the team starts working in HubSpot. A good cutover plan sets the freeze time, who validates what and the rollback criteria in advance.
What is not data: Flows, Apex and validations
Salesforce tends to accumulate logic: Flows, Apex triggers, validation rules, formula fields and roll-up summaries. None of that travels with the data. It has to be inventoried and you need to decide how it is rebuilt: workflows, calculated properties, property validation rules, custom code actions or custom development when the logic is complex.
At a fintech SaaS company we migrated from Salesforce, that logic ended up spread across around 12 microservices: sequential numbering, ARR and account type calculated daily, chained renewals, stage history for the data warehouse and the connection to its contract management tool, with a backfill of 1,615 flows. You can read the full case study on our site. Not every org needs that much, but it is worth knowing before you budget.
If you are still deciding whether the switch is worth it, the HubSpot vs Salesforce comparison helps you see what you gain and what you leave behind.