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.
| Action | No record matches | One record matches | Several records match |
|---|---|---|---|
insertRecord | Creates one | Creates one more | Creates one more |
updateRecords | Fails the row: “No records have been updated” | Updates it | Updates every match |
upsertRecords | Creates one, including the matched value | Updates it | Updates 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
updateRecordsfor Id matches. WithupdateRecords, an Id that no longer exists (the record was merged or deleted) fails that row.upsertRecordson a stale Id can create a new record instead, when askipIfExistmapping 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.
skipIfExistprotects 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:
- 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. - Website (
Website). The AccountWebsitefield holds a URL as someone typed it (https://www.acme.com,acme.com/), not a bare domain, so an equality match onacme.commisses most of them. Count with thecontainsoperator instead, then read the one match back withmatchRecordto check its host really is your domain (containsalso matchesacme.com.au), and update it byId. If your org keeps a normalized domain in a custom field, match on that with equality instead. - 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:
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.
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.
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_countis 1: runmatchRecordwith the same filter, check thatrecord.Websiteis your domain, and run step 1’s command withrecord.IdasmatchingValue.cargo_countis more than 1: skip the row and list it for review.cargo_countis 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:
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,updateRecordsandupsertRecordsreturndataas 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.countRecordsreturnscount, from a SOQLCOUNT()query, so it is not capped at a page.matchRecordreturnsmatch(true or false) andrecord, 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 #
Yes. The Salesforce connector’s upsertRecords action queries for records
whose field equals your value, updates every one it finds, and creates a
record when it finds none. updateRecords only updates and insertRecord
only creates.
updateRecords and upsertRecords update both. To skip ambiguous matches,
run countRecords first and only write the rows where the count is exactly
- A built-in skip option is in review.
Not by equality on Website, because that field holds a URL rather than a
bare domain. Count with the contains operator on Website, confirm the
single match with matchRecord, and update it by Id; or match on a custom
field that stores the normalized domain.
Not with updateRecords: the update by Id fails that row, and no
replacement is created.
Only if you let it. Set skipIfExist on a mapping and an update writes that
field only when it is empty in Salesforce.