The part everyone underestimates: associations
Moving 70,000 ticket rows is a batching problem, and batching problems are solved. What breaks migrations is the graph around those tickets — which contact raised it, which company they belong to, which conversation thread and which attachments hang off it.
We migrate in dependency order: companies, then contacts, then deals, then tickets, then engagements and attachments. Every object carries its source system ID into a dedicated HubSpot property so associations can be rebuilt by lookup rather than by guesswork, and so anything can be traced back afterwards.
The mapping document is the contract
Before a single record moves, every source field gets a row: destination property, type, transformation rule, and what happens when the value is empty or unrecognised. Picklist values are enumerated exhaustively — unmatched values are flagged rather than silently dropped.
The client signs this. When someone asks six months later why a field looks different, the answer is a row in a document rather than an archaeology project.
Dry run into a sandbox
- 01Load the full dataset into a sandbox portal, not a sample. Samples hide the records that break things.
- 02Diff record counts per object against the source.
- 03Diff fill rates per property — a field that is 94% populated at source and 61% populated after load is a mapping bug.
- 04Count associations per object type and compare.
- 05Have two support agents work real tickets in the sandbox for a day. They find what queries do not.
Delta sync and the cutover window
Historical load runs in the background over days while the source system stays fully in use. On cutover day, a delta sync picks up everything created or modified since the snapshot, which is typically a few thousand records rather than seventy thousand.
The read-only window is then under an hour. On the Savance engagement this ran outside US business hours, so agents finished on the old system on a Friday and started in HubSpot on Monday.
Deduplication is a separate project
The same engagement carried around 20,000 duplicate records. Merging during a migration is how you lose data — you are changing two things at once and cannot attribute a discrepancy to either.
Load first with source IDs intact, reconcile, then run deduplication as a controlled second phase with its own before-and-after counts.
| Phase | Duration | Deliverable |
|---|---|---|
| Discovery and mapping | 2 weeks | Signed field mapping document |
| Sandbox dry run | 2–3 weeks | Diff report, agent test sign-off |
| Historical load | 1–2 weeks | Loaded portal with source IDs |
| Delta sync and cutover | 1 weekend | Live portal, <1 hour read-only |
| Reconciliation and dedupe | 2–4 weeks | Reconciliation report, merge log |
The reconciliation report
The report lists, per object: source count, destination count, variance and explanation for every variance. Expected variances (test records excluded, soft-deleted rows skipped by agreement) are listed with the rule that produced them. Unexpected variances are worked until they are zero.
This is the artefact that lets a client shut down the old system. Without it, teams keep paying for a legacy licence 'just in case' for years.
Common mistakes
- Migrating and deduplicating in the same pass, then being unable to explain a count difference.
- Not writing source system IDs into HubSpot, which makes tracing a specific record impossible afterwards.
- Testing with a 500-record sample. The broken records are always in the tail.
- Scheduling cutover on a Monday morning. Cut over when nobody is depending on the system.
- Treating attachments as an afterthought — they are usually the slowest object to move.
