A 32-object export, a two-person-per-record contact model, and an Excel engine that ran the entire business. Here's how we rebuilt all of it in HubSpot without losing a single booking.
Placeholders to fill before publishing: client name or descriptor, region, and the results figures in the Outcomes section.
Quick answer: Migrating a decade of Act! CRM data into HubSpot without losing bookings meant solving four problems before importing a single row: splitting shared two-person contact records into individual people, preserving a four-level object hierarchy (contact, trip, itinerary, operator), rebuilding the client's real operational engine (a linked Excel system) as middleware instead of just moving the CRM, and working around a vendor-gated export process with no direct database access. The fix combined a custom identity-resolution model, a 100-record pilot before the full migration, and AWS middleware that keeps the spreadsheet workflow alive while writing straight into HubSpot.
| Industry | Bespoke tours and travel, multi-country itineraries |
| Legacy system | Act! CRM, in continuous use for 10+ years |
| Team | Sales consultants, operations staff, finance |
| Records migrated | Contacts, trips, itinerary lines, operators, booking records |
| Target | HubSpot Sales Hub + Operations Hub, custom objects, custom-coded middleware |
| Duration | Weeks, discovery to hypercare exit |
This client sells long-form, high-value guided travel. A single booking is a 10 to 15 day itinerary across multiple countries, handled on the ground by a chain of local operator companies, sold to couples and small groups who often travel with them six or seven times over a decade.
Ten years of that lives in Act!. And Act! had been bent into a shape no off-the-shelf migration tool could read.
Most CRM migrations are a mapping exercise. Contacts to contacts, deals to deals, done. This one had four structural problems that had to be solved before a single row could be imported.
In Act!, a travelling couple was stored as a single contact record with a primary and a secondary contact inside it. Husband and wife, or two friends travelling as a pair, shared one Act! ID.
That model breaks immediately in HubSpot. HubSpot's contact object is one record per person. Marketing consent, communication history, passport data, and dietary requirements all belong to individuals, not to a pair.
Worse, the source data was inconsistent:
HubSpot deduplicates contacts on email address by default. Importing this data naively would have merged spouses into one another and collapsed a decade of travel history into the wrong person.
Act! held a custom object structure that looked like this:
Every layer had to survive with its relationships intact. An itinerary line detached from its trip is meaningless. An operator not linked to the correct leg of the correct trip is an operational failure with a customer standing in an airport.
The real operational engine was a spreadsheet system the client called JPS. When a trip was confirmed, a client-specific itinerary sheet was generated automatically from a master brain sheet holding collections, amounts, operator rates, and supplier data. Operations worked that sheet for the life of the booking. When the trip closed out, the finished numbers were synced back into Act!.
Migrating the CRM without migrating this loop would have delivered a beautiful HubSpot portal that nobody used.
This was the constraint that shaped the entire project plan.
Neither the client nor our team had access to the underlying Act! SQL database. No table-level export, no direct query access, no ODBC connection. The only route to the data was through the Act! vendor's own export team.
That meant every export was a formal request with a turnaround time. We could not iterate quickly, we could not re-pull a table because we misread a column, and we could not afford to discover a schema surprise late.
Before requesting any data, we mapped the target architecture. Getting this wrong would mean re-importing tens of thousands of records.
| Act! CRM | HubSpot | Notes |
|---|---|---|
| Contact (primary) | Contact | Standard object, one record per person |
| Contact (secondary) | Contact | Separate record, labelled association back to primary |
| Company / agency | Company | Standard object |
| Operator | Custom object: Operator | Kept separate from Company so partner records and ground-handler records never mix in reporting |
| Trip | Deal | Needs amount, pipeline, stages, forecasting |
| Itinerary | Custom object: Itinerary | Child of Deal, one record per travel day or leg |
| Booking form / pax data | Custom object: Booking (Pax) | Passport, dietary, signature docs, one per traveller per trip |
| Brain sheet output | Deal properties + Operator associations | Written by middleware, not manually |
| Act! user | HubSpot owner | Includes deactivated historical users |
Two decisions here did most of the heavy lifting later.
Trips became Deals, not a custom object. Trips have a value, a lifecycle, a close date, and a forecast. Deals give the client pipelines, revenue reporting, and forecasting for free. A custom object would have meant rebuilding all of that.
Operators became a custom object rather than Companies. The client works with both travel agency partners and in-country ground handlers. Both are "companies" in the loosest sense, but they behave nothing alike. Splitting them keeps operator performance reporting clean and prevents a decade of partner records from being polluted by supplier data.
This was the technical centrepiece of the migration.
We created a custom property on the Contact object, act_contact_id, and set it as a unique identifier property in HubSpot. We added a second property, act_contact_role, with values Primary or Secondary.
Because the same Act! ID belonged to both halves of a pair, the actual dedupe key became a composite written into a third property: act_identity_key = {ACT_ID}-{ROLE}.
Imports were then run against act_identity_key rather than email. This meant:
We built a pre-import resolution pass over the raw export that classified every pair into one of four cases:
Every record in categories 2, 3, and 4 was surfaced in a review workbook and signed off by the client before import. No automated guessing on identity. Pairs requiring manual adjudication were flagged and resolved individually.
Secondary contacts with no independent email were set as non-marketing contacts, which kept consent handling clean and avoided inflating the client's marketing contact tier.
We built custom labelled associations so the model reads correctly in the UI and in reporting:
A consultant opening a Deal now sees both travellers, correctly labelled, plus every operator on the trip and the full day-by-day itinerary, in one view.
With the model designed, we went to the Act! vendor's export team.
What came back was 32 separate objects and sheets. Not all of them were live data. Some were legacy fields abandoned years earlier, some were duplicated views of the same underlying table, and some were system artefacts.
We worked through all 32 with the client's operations lead and narrowed it to 16 sheets that genuinely needed to reach HubSpot. Everything else was documented and archived rather than migrated. Cutting 16 objects out of scope was one of the highest-value decisions in the project.
Rather than requesting the full export, we asked the Act! team for a sample of roughly 100 records across every planned object, with referential integrity preserved so the sample included the parents and children of the same trips.
We then ran the entire migration against that sample:
The client reviewed the result in a sandbox portal and formally approved it. Only then did we request the full master export.
This is the step most migrations skip, and it is exactly the step that a vendor-gated, slow-turnaround export makes non-negotiable. The pilot caught mapping defects that would have been catastrophic at full volume, including date-format inversion on two fields and a picklist that used different values in two different sheets for the same concept.
The Act! team delivered the complete master export. We loaded it in strict dependency order, because associations cannot be created before both sides of the relationship exist.
| # | Load | Why this order |
|---|---|---|
| 1 | Users and owner mapping table | Every subsequent record needs a valid owner |
| 2 | Companies | Parents for contacts |
| 3 | Operators (custom object) | Referenced by trips and itineraries |
| 4 | Contacts (primary) | Anchor records |
| 5 | Contacts (secondary) | Requires primaries to exist for association |
| 6 | Contact ↔ Contact labelled associations | Pair structure |
| 7 | Deals (trips) | Requires contacts, companies, owners |
| 8 | Deal → Contact / Company / Operator associations | Requires all of the above |
| 9 | Itineraries | Requires parent Deals |
| 10 | Bookings / pax data | Requires Deals and Contacts |
| 11 | Notes, activities, attachments | Timeline history |
Trips were imported as Deals with full financials: gross value, deposits and collections, currency, booking date, departure date, return date, group size, and pipeline stage derived from the Act! trip status. Historical trips landed in a dedicated Closed Won (Historical) stage so they contributed to lifetime-value reporting without distorting active pipeline forecasts.
Itinerary records carried day index, destination, arrival and departure dates, accommodation, meal plan, and the operator responsible for that leg. A 14-day trip produced 14 linked itinerary records rather than a wall of text in a description field, which is what made day-level operational reporting possible afterwards.
Ten years of Act! meant a long list of users, many of them long gone. We built an explicit mapping table covering:
Ownership was mapped consistently across contacts, deals, operators, itineraries, and bookings. Without this, historical performance reporting and territory routing would have been unusable from day one.
The client's website booking form already captured passport details, signed documents, and dietary requirements, and pushed them into Act! via API.
We re-pointed that pipeline at HubSpot:
The result is that from go-live, new bookings populate HubSpot natively. There is no residual dependency on Act! for new business.
This is the part that made the CRM migration actually stick.
The client's operational loop was: trip confirmed → client itinerary sheet generated from the master brain sheet → operations works the sheet → final figures pushed back into the CRM.
We rebuilt that loop with middleware on AWS, triggered from Google Drive:
The client kept the spreadsheet workflow their operations team already knew. HubSpot became the accurate system of record, updated automatically, instead of a place people were told to update manually and didn't.
Nothing went live on assertion. Every object was reconciled on four dimensions:
The identity resolution work was validated specifically by pulling the client's known repeat travellers and confirming that all six or seven of their historical trips appeared on the correct pair of contact records, with the correct labels.
Act! was kept in read-only reference mode for a defined period and then decommissioned.
Design the target model before you request the export. Especially when export requests are gated behind a vendor team with a turnaround time. Every schema surprise you catch on paper is a week you don't lose.
Never let email be your dedupe key on legacy travel or family data. Shared household emails are the norm, not the exception. Custom unique identifier properties carried from the source system are the only safe anchor.
Cut scope aggressively at the object level. Half the objects in a decade-old CRM are abandoned. Migrating them costs money twice: once to move them and forever to maintain them.
Pilot with a full vertical slice, not a wide sample. 100 records that include complete parent-child-grandchild chains will teach you more than 10,000 flat contact rows.
Migrate the workflow, not just the data. If a spreadsheet is the real system of record, the migration is not finished until that spreadsheet writes to HubSpot automatically. Otherwise you have built a very expensive archive.
Map departed users deliberately. Ten years of staff turnover will silently destroy your historical reporting if unowned records land on a default nobody agreed to.
Yes. Act! data can be migrated into HubSpot's standard and custom objects, but a decade-old Act! database usually needs a custom identity and object model designed first, especially where Act! stored shared or paired contact records that HubSpot's one-record-per-person model can't read directly.
When a legacy CRM vendor controls the only export route, the fix is to request a small referentially-intact pilot sample first, validate the full mapping and import logic against it, get client sign-off, then request the full master export. This avoids re-requesting exports and catching schema errors only after a full-volume import.
HubSpot's Contact object is built for one person per record, so a shared legacy record has to be split into two individual Contacts with a labelled association between them (for example, Primary Traveller and Secondary Traveller). Deduplication should run on a custom unique identifier from the source system, not on email, since paired records frequently share one email address.
If a spreadsheet is genuinely running the business, the migration isn't complete until that spreadsheet writes into the new CRM automatically. That usually means custom middleware that reads the sheet, resolves it against the correct CRM record, and pushes the data in, rather than asking the operations team to switch to manual entry.
It depends on record volume, the number of custom objects, and how many operational workflows (not just data) need to be rebuilt. A migration involving multiple custom objects, identity resolution, and custom middleware typically runs longer than a standard contacts-and-deals migration, and should include a hypercare period after go-live.
Not usually. Keeping the legacy CRM in read-only reference mode for a defined period after go-live gives the team a safety net for historical lookups while HubSpot is confirmed as the accurate system of record, before the old system is fully retired.
If you're on Act!, or any legacy CRM with custom objects, a spreadsheet-driven operations layer, and no direct database access, the migration is an architecture project rather than a data-transfer job. We design the target model, negotiate the export, prove it on a pilot, and rebuild the operational loop so the new system holds.