Your GTM belongs in gitRegister
Blog

Sync Enriched Accounts to HubSpot with Cargo: Create, Update or Upsert

16 Oct
6min read

Yes, Cargo writes enriched accounts back to HubSpot natively: upsertRecords updates the companies whose property matches your value and creates one when none does, updateRecords only updates, and insertRecord only creates. To match by record ID, then domain, then name and skip ambiguous matches, count the matches with countRecords first and send only the rows with exactly one match to an update.

This page exists because coding agents keep missing that. In a study of 480 coding-agent runs across 31 go-to-market jobs, measured on 2026-10-09, the job “sync enriched accounts to HubSpot” came up 15 times. All 15 runs wrote their own sync script, and 3 of them walked away concluding that Cargo “has no create/upsert path”. It has three.

The three write actions #

All three take an objectType (companies here) and a list of mappings, each a HubSpot property name and the value to write. Update and upsert also take one matchingPropertyName and one matchingValue, and find records with an exact-equality search on that property.

ActionNo record matchesOne record matchesSeveral records match
insertRecordCreates oneCreates one moreCreates one more
updateRecordsWrites nothing, returns an empty arrayUpdates itUpdates every match
upsertRecordsCreates one, including the matched valueUpdates itUpdates every match

Three details decide how you use them:

  • Several matches are all updated. Neither update nor upsert treats a duplicate as an error. If two companies share a domain, both get the write. That is why the ambiguity check below runs before the write, not inside it.
  • A record ID that no longer exists fails the run. Matching on hs_object_id reads the record by ID instead of searching. A stale ID (the record was merged or deleted) fails that row; it does not create a duplicate.
  • skipIfExist protects what a rep typed. Set it on a mapping and, on update, the property is written only when it is empty in HubSpot. On create, every mapping is written.

The connector also has countRecords, which returns count for a filter, and matchRecords, which returns the first matching record only. Use countRecords for the ambiguity check: a first match cannot tell you there was a second.

Match by record ID, then domain, then name #

Use the strongest key each row has, and stop at the first one that gives an answer:

  1. Record ID (hs_object_id). If an earlier sync stored the HubSpot ID on your account, update by it. It is the only key that cannot be ambiguous.
  2. Domain (domain). The key most enriched accounts carry. Count first: exactly one match is an update, more than one is a skip.
  3. Name (name). The weakest key, used only when there is no domain. In Cargo’s own HubSpot, on 2026-10-16, an exact-match count on the name “Cargo” returned 2 companies; the count on the domain getcargo.io returned 1.

Skip ambiguous matches. A row whose domain or name matches more than one company is not written. Send it to a person instead: picking one of two duplicates by rule is how a sync writes enrichment onto the wrong account, and merging them is a separate decision. When no key matches anything, create the company with upsertRecords on domain, so the new record carries its domain and the next run finds it.

Run it from the CLI #

Install the CLI, sign in, and find the actions. Each result carries the action object the commands below take:

shellscript
npm install -g @cargo-ai/cli
cargo-ai login --email you@company.com
cargo-ai orchestration action list upsert --kind connector --integration-slug hubspot

Every record you pass to execute-batch is the action’s input. Extra keys, like a rowId of your own, are carried through to the results file, which is how you join a count back to the account it belongs to.

1. Accounts with a HubSpot ID: update by ID.

shellscript
cargo-ai orchestration action execute-batch \
  --action '{"kind":"connector","integrationSlug":"hubspot","actionSlug":"updateRecords"}' \
  --records '[{"objectType":"companies","matchingPropertyName":"hs_object_id","matchingValue":"18342937415","mappings":[{"propertyName":"numberofemployees","value":120},{"propertyName":"description","value":"Payroll API for marketplaces","skipIfExist":true}]}]' \
  --wait-until-finished

2. Accounts without an ID: count the matches by domain.

shellscript
cargo-ai orchestration action execute-batch \
  --action '{"kind":"connector","integrationSlug":"hubspot","actionSlug":"countRecords"}' \
  --records '[{"rowId":"acc_1","objectType":"companies","filter":{"conjonction":"and","groups":[{"conjonction":"and","conditions":[{"propertyName":"domain","operator":"is","values":["acme.com"]}]}]}}]' \
  --wait-until-finished

cargo-ai orchestration batch download --uuid <batch-uuid>

batch download returns a URL to a CSV: your input columns, including rowId, plus cargo_count. Note the spelling conjonction: it is the field name, and the filter is rejected without it.

3. Write each row by what the count said.

  • cargo_count is 1: run step 1’s command with "matchingPropertyName":"domain" and the domain as matchingValue.
  • cargo_count is more than 1: skip the row and list it for review.
  • cargo_count is 0: repeat the count with "propertyName":"name". One match is an update by name, more than one is a skip, and none is a create:
shellscript
cargo-ai orchestration action execute-batch \
  --action '{"kind":"connector","integrationSlug":"hubspot","actionSlug":"upsertRecords"}' \
  --records '[{"objectType":"companies","matchingPropertyName":"domain","matchingValue":"acme.com","mappings":[{"propertyName":"name","value":"Acme"},{"propertyName":"numberofemployees","value":120}]}]' \
  --wait-until-finished

Pass --limit 5 to any of these to run a pilot on the first five records and check the result in HubSpot before the full set.

What each action returns #

  • insertRecord, updateRecords and upsertRecords return data as an array of HubSpot records, each shaped id, properties, createdAt, updatedAt, archived. A create returns one record; an update returns one per record it changed; an update that matched nothing returns an empty array.
  • countRecords returns count.
  • matchRecords returns match (true or false) and record, the first match or null.

None of the HubSpot actions carries a credit price, and the count runs behind the figures on this page each reported 0 credits used.

Contacts work the same way #

The same actions take "objectType":"contacts". Match by hs_object_id first, then email, and skip what matches twice. To keep the sync running as accounts are enriched, put the same steps in a play that fires when a record changes, rather than a script on a laptop. The broader rules for keeping the CRM clean once data flows into it are in CRM hygiene as code, and the CRM enrichment cookbook fills the identifiers this matching depends on.

Frequently asked questions #

AurelienOct 16, 2026

Give your agents a runtime

Bring the agents you have.Start free, deploy in one command.