How to connect HubSpot to Google Sheets without exporting the duplicates too
A CRM export inherits every data quality problem in the portal. Cleaning before it lands, associations that do not fit a rectangle, and keeping a sheet from becoming a second CRM.
On this page
- What we'll cover
- The export inherits the mess
- Associations do not fit a rectangle
- Building it with CodeWords
- What the sheet should contain
- Cleaning on the way out
- Keeping the sheet from becoming a second CRM
- Properties that change under you
- Incremental exports, once the portal is large
- Making it survive
- Limits worth knowing about
- What to set up first
- Frequently asked questions
- Related reading
An export inherits everything wrong with the portal it came from. If the same company exists four times in HubSpot, it exists four times in the spreadsheet, and the total at the bottom of the column is wrong in a way that is difficult to spot and embarrassing to discover in a meeting.
That makes the interesting work upstream of the export. What the sheet should contain is usually a smaller, cleaner set than what the portal holds, and deciding what to exclude is the part worth thinking about.
What we'll cover
- The export inherits the mess
- Associations do not fit a rectangle
- Building it with CodeWords
- What the sheet should contain
- Cleaning on the way out
- Keeping the sheet from becoming a second CRM
- Properties that change under you
- Incremental exports, once the portal is large
- Making it survive
- Limits worth knowing about
- What to set up first
- Frequently asked questions
The export inherits the mess
Duplicate contacts and companies become duplicate rows, and any count or sum is wrong by however many duplicates there are.
Test and internal records are in every portal — colleagues, test submissions, competitors somebody added — and they distort exactly the numbers people quote.
Half-populated records produce columns that are empty for a third of rows, which a spreadsheet average treats as zero.
Deals with no associated company disappear from any company-level grouping, silently.
Records created by integrations follow different conventions from records created by people, which shows up as two spellings of everything.
Exporting first and cleaning in the spreadsheet is the instinct and it is the wrong order, because the cleaning has to be redone on every export and eventually is not.
Associations do not fit a rectangle
HubSpot's model is objects joined by associations, and the joins are where the meaning is.
A contact can be on several deals, so "one row per contact" and "one row per deal" are different sheets with different totals.
Association labels matter. A decision maker and a technical contact are different relationships, and an export that ignores labels flattens them into the same thing.
Many-to-many relationships cannot be one row. You choose a grain — one row per deal, per contact, per company — and everything else becomes a summary column or a separate sheet.
State the grain in the sheet. The reader cannot tell by looking, and will assume whichever grain suits their question.
Pick the grain from the question. A pipeline report is one row per deal; a contactable list is one row per contact; a revenue-by-account report is one row per company.
Building it with CodeWords
CodeWords connects to more than 3,000 integrations, and the connection is made once and reused.
- Open CodeWords and start a new automation.
- Describe what should happen in plain language to Cody, the automation builder: which objects, which filters, which properties, and which sheet.
- Authorize the connection with a HubSpot private app scoped to the objects involved, plus access to the target spreadsheet.
- Describe the exceptions: a probable duplicate, an internal or test record, a property whose options have changed.
- Run it against a small, known set of records and check the totals before circulating anything.
You describe the outcome; Cody builds it, connects it, and deploys it. The free plan covers light use, with Pro at $39 per month and Business at $100 per month as usage grows; details are on the pricing page.
What the sheet should contain
Fewer columns than you think. A dozen properties chosen for the question beats eighty exported because they exist.
The properties people will filter on, named readably rather than by their internal names.
A stable identifier, as text, so rows can be matched against a later export.
Derived values computed before export — days in stage, age, whether a field is populated — rather than as spreadsheet formulas that somebody will break.
A flag for excluded records, or a count of them somewhere visible, so the reader knows the sheet is filtered rather than complete.
Nothing that will not be used. Every extra column is a column somebody sorts by accident.
Cleaning on the way out
The step that makes the export worth trusting.
Exclude internal domains and known test records, which is the single largest improvement to most CRM reports.
Flag probable duplicates rather than merging. Merging is destructive and near-matches are sometimes genuinely different; a flag lets the reader decide and gives somebody a list to fix in HubSpot.
Normalise before comparing — lowercase, trim, strip punctuation from phone numbers, remove common company suffixes — or you will treat two spellings as two companies.
Report the counts. How many records were excluded and why, written in the sheet, so the difference between this total and the portal's is explainable.
Fix it in HubSpot too. An export that quietly works around bad data lets the bad data persist, so the exclusion list is also a work list.
Keeping the sheet from becoming a second CRM
The drift worth resisting.
Exports are read-only by nature, and people will start editing them anyway, adding notes and statuses in spare columns.
Those edits are lost on the next run, which teaches people the sheet is unreliable and teaches them to stop refreshing it.
If people need to annotate, decide where that lives. Either a separate column range the export never touches, or the annotation belongs in HubSpot as a property.
Never let the sheet become the source of truth for anything the CRM should own, because nothing enforces it and nothing syncs back.
Watch for the sheet nobody refreshes. A stale CRM export circulating in a board pack is a common and avoidable embarrassment, which is why the run time belongs in the sheet.
Properties that change under you
Portals are heavily customised and properties are added, renamed, and deprecated by whoever manages operations.
Match on internal property names, not labels, since labels are what people edit.
Read property definitions rather than hard-coding options, because dropdown options change and a value that no longer exists reads as blank.
Handle the deleted property. An export referencing one should report clearly rather than producing a column of empties.
Watch calculated and rollup properties, which can lag or be empty for records that have not been recalculated.
Incremental exports, once the portal is large
A full export stops being practical at a certain size, and the switch is worth making before it becomes urgent.
Export what changed since the last run, using the last-modified property, rather than everything every time.
Store the cursor durably after a successful run, not before, so a failure part way does not skip records permanently.
Deletions are invisible to a modified-since query. A deleted record simply stops being returned, so a sheet built incrementally keeps rows that no longer exist unless something reconciles.
Reconcile periodically. A full export monthly, compared against the incremental sheet, catches both deletions and anything the cursor missed.
Merge by identifier, not by row position, so an update corrects the existing row rather than adding a second.
Report how many rows were added, updated, and unchanged, since a run that updates nothing usually means the cursor is wrong rather than that nothing happened.
Making it survive
Use search with filters and pagination, rather than listing everything and filtering afterwards.
Batch where volume warrants, since batch endpoints are far kinder to the rate limit.
Replace a named range rather than appending, or retries duplicate rows.
Write the run time, the grain, and the exclusion counts into the sheet.
Report the outcome. Records exported, excluded, and flagged as possible duplicates.
Limits worth knowing about
Rate limits depend on your subscription, and a full export needs pagination and pacing.
Search is eventually consistent, so a record created and immediately searched for may not be found.
Sheets has a cell limit, which a wide export of a large portal will reach.
Some properties are not available through search filters, which changes how you select records.
Association limits apply on how many can be retrieved in one call, so a contact on many deals needs paging.
What to set up first
One report at one grain, with internal and test records excluded, probable duplicates flagged, and the run time and exclusion counts written into the sheet. Start with whichever export somebody currently produces by hand, because it already has an audience and a definition.
Two habits make the difference. Exclude internal domains from the first version, since that alone corrects most CRM reports more than any other single change. And write the exclusion counts into the sheet, because the first question anybody asks is why this total differs from the portal's.
Frequently asked questions
Why does the sheet total differ from HubSpot?
Duplicates, test records, and filtering. Write the exclusion counts into the sheet so the difference is explainable, because this is the first question anybody asks and an unexplained gap discredits the whole report.
Should duplicates be merged during export?
No — flag them. Merging is destructive and near-matches are sometimes different people or companies. A flagged list also gives somebody a work list for fixing the portal, which is the actual fix.
One row per contact or per deal?
Whichever matches the question, and say which in the sheet. A contact can be on several deals, so the two grains produce different totals and a reader will assume the one that suits them.
Why are some columns mostly empty?
Because those properties are optional and half-populated, which is normal in any CRM. A spreadsheet average over that column treats blanks as zero, so report the fill rate alongside anything computed from it.
Can people edit the exported sheet?
They will, and the edits vanish on the next run. Either give them a column range the export never touches, or put the annotation in HubSpot where it belongs. Never let the sheet become the source of truth.
Why did a column go blank after working for months?
A property was renamed, deprecated, or its options changed. Match on internal property names rather than labels, and report a missing property clearly rather than producing a column of empties.
What is the single biggest improvement to a CRM export?
Excluding internal domains and test records. It is a short filter, it corrects the numbers people actually quote, and almost no export does it by default.
Should the export be full or incremental?
Full while the portal is small, because it is simpler and self-correcting. Incremental once that becomes impractical — with a durable cursor, merging by identifier, and a periodic full reconciliation to catch deletions.
Why does an incremental sheet keep records that were deleted?
Because a modified-since query cannot see deletions — the record simply stops being returned. Only a periodic full comparison catches them, which is the main argument for keeping one even after you go incremental.
Does exporting contacts to a spreadsheet raise privacy questions?
It creates a copy outside HubSpot's access controls and outside its deletion process, so a later erasure request will not reach it. Export the columns the question needs rather than everything, and check who the sheet is shared with.
How do I stop a stale export being circulated?
Put the run time in a visible cell on every run. A CRM export from three weeks ago looks identical to a current one, and that is how an out-of-date number ends up in a board pack.