Your GTM belongs in gitRegister
Blog

Sync Enriched Records to Salesforce with Cargo: Create, Update or Upsert

16 Oct
6min read

Yes, Cargo writes enriched go-to-market records back to Salesforce natively: upsertRecords updates the records whose field matches your value and creates one when none does, updateRecords only updates, and insertRecord only creates. To match by record Id, then website, then name and skip ambiguous matches, count the matches with countRecords first and send only the rows with exactly one match to an update.

Cargo platform data, 90 days to Oct 16 2026: salesforce.updateRecords ran 6.6M times across 37 workspaces.

The three write actions #

All three take an objectType (Account here) and a list of mappings, each a Salesforce field API name and the value to write. Update and upsert also take one matchingPropertyName and one matchingValue, and find records with a SOQL query on matchingPropertyName = matchingValue.

ActionNo record matchesOne record matchesSeveral records match
insertRecordCreates oneCreates one moreCreates one more
updateRecordsFails the row: “No records have been updated”Updates itUpdates every match
upsertRecordsCreates one, including the matched valueUpdates itUpdates every match

Four details decide how you use them:

  • Several matches are all updated. Neither update nor upsert treats a duplicate as an error. If two accounts share a name, both get the write. The matches come from one page of SOQL results (up to 2,000 records) and are written in a single Salesforce update request, which accepts at most 200 records, so a value that matches more than 200 records fails the row instead of updating them. An option to skip or fail on several matches (onMultipleMatches) is in review; until it ships, run the ambiguity check below before the write.
  • Use updateRecords for Id matches. With updateRecords, an Id that no longer exists (the record was merged or deleted) fails that row. upsertRecords on a stale Id can create a new record instead, when a skipIfExist mapping makes it search first and the search finds nothing.
  • A partial failure is not a failed row. When several records match and some updates are rejected, the row succeeds and returns only the records that were updated. It fails only when none was.
  • skipIfExist protects what a rep typed. Set it on a mapping and, on update, the field is written only when it is empty in Salesforce. On create, every mapping is written.

The connector also has countRecords, which returns count for a filter, and matchRecord, 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 website, then name #

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

  1. Record Id (Id). If an earlier sync stored the Salesforce Id on your account, update by it. It is the only key that cannot be ambiguous.
  2. Website (Website). The Account Website field holds a URL as someone typed it (https://www.acme.com, acme.com/), not a bare domain, so an equality match on acme.com misses most of them. Count with the contains operator instead, then read the one match back with matchRecord to check its host really is your domain (contains also matches acme.com.au), and update it by Id. If your org keeps a normalized domain in a custom field, match on that with equality instead.
  3. Name (Name). The weakest key, used only when there is no website. Count first: exactly one match is an update, more than one is a skip.

Skip ambiguous matches. A row whose website or name matches more than one account 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 account with insertRecord, writing its Website, so the next run finds it.

Run it from the CLI #

Install the CLI, sign in, check the Salesforce connector, 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 connection connector list --integration-slug salesforce
cargo-ai orchestration action list upsert --kind connector --integration-slug salesforce

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 Salesforce Id: update by Id.

shellscript
cargo-ai orchestration action execute-batch \
  --action '{"kind":"connector","integrationSlug":"salesforce","actionSlug":"updateRecords"}' \
  --records '[{"objectType":"Account","matchingPropertyName":"Id","matchingValue":"001Hs00003XyZabIAF","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 website.

shellscript
cargo-ai orchestration action execute-batch \
  --action '{"kind":"connector","integrationSlug":"salesforce","actionSlug":"countRecords"}' \
  --records '[{"rowId":"acc_1","objectType":"Account","filter":{"conjonction":"and","groups":[{"conjonction":"and","conditions":[{"propertyName":"Website","operator":"contains","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 matchRecord with the same filter, check that record.Website is your domain, and run step 1’s command with record.Id 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" and "operator":"is". One match is an update with "matchingPropertyName":"Name", more than one is a skip, and none is a create:
shellscript
cargo-ai orchestration action execute-batch \
  --action '{"kind":"connector","integrationSlug":"salesforce","actionSlug":"insertRecord"}' \
  --records '[{"objectType":"Account","mappings":[{"propertyName":"Name","value":"Acme"},{"propertyName":"Website","value":"https://acme.com"},{"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 Salesforce before the full set.

What each action returns #

  • insertRecord, updateRecords and upsertRecords return data as an array of the Salesforce records they wrote, read back after the write with every field. A create returns one record; an update returns one per record it changed.
  • countRecords returns count, from a SOQL COUNT() query, so it is not capped at a page.
  • matchRecord returns match (true or false) and record, the first match or null.

None of the Salesforce actions carries a credit price.

Contacts and leads work the same way #

The same actions take "objectType":"Contact" or "objectType":"Lead". Match by 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 HubSpot version of this page is sync enriched accounts to HubSpot, the broader rules for keeping the CRM clean 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.