Some Records Didn't Sync, but the Run Says Success
When a batch write partly fails, Salesforce sObject Collections still returns 200 OK with success false on the failed items, HubSpot returns 207 Multi-Status with numErrors, and Shopify GraphQL returns 200 with userErrors, so n8n, Zapier and Make record a successful step. The fix is to read the per-record results after every batch write, require that succeeded plus failed equals records sent, and route the failures by a key you control.
When some records fail to sync but the run shows success, the batch write almost always returned a success status with the failures listed inside the response body. Salesforce sObject Collections returns 200 OK with "success": false on each failed item, HubSpot batch endpoints return 207 Multi-Status with a numErrors count, and Shopify's GraphQL Admin API returns 200 with a userErrors array. n8n, Zapier and Make decide pass or fail from the status code, so the step goes green, and the missing records are found weeks later by a person, not by the automation.
The status code describes the request, not the records
A batch endpoint answers two different questions, and the HTTP status only answers the first one. Was the request well formed and accepted? Yes, here is a 200. Did every record in it get written? That answer lives in the body, one entry per record.
Salesforce says this in so many words: if the request is well formed, the API returns 200 OK, the success flag shows per item, and error information comes back in the errors array (Salesforce REST API Developer Guide, checked October 2026). Its allOrNone parameter defaults to false, which means one malformed ID fails one record and the other 199 are written.
Now look at what the workflow tool does with that. The n8n HTTP Request node documentation states that by default the node returns success only when the response comes back with a 2xx code. Make's HTTP module stops the scenario on a 4xx or 5xx when you set "Return error if HTTP request fails" to Yes. A 207 is a 2xx. A 200 with three failed records inside is a 2xx. Neither tool opens the body to look.
This is the write-side twin of the read-side problem in syncs that only pull the first 100 records. There, the run requested less than it should have. Here, the run sent everything and the destination quietly declined part of it. In both cases there is no error to alert on, and every step in the execution history looks the same as a perfect run.
Where each API puts the failed records
Each vendor reports a partial failure in a different place and links the failure back to your input in a different way. The second column is what your workflow sees. The last column is the part most implementations get wrong.
| Endpoint | Status on partial failure | Where the failures are | How a failure maps back to your record |
|---|---|---|---|
| Salesforce sObject Collections (up to 200 records) | 200 OK | Array of SaveResults, success: false plus errors[] with statusCode and fields | By position only. Results come back in request order, and a failed item has no id |
| Salesforce Bulk API 2.0 ingest job | Job state Failed, numberRecordsFailed above 0, good rows kept | Separate failedResults CSV with sf__Error and sf__Id | By your own columns, which the CSV echoes back. Row order is not guaranteed |
| HubSpot CRM batch create | 207 Multi-Status | results[], numErrors, errors[] | By objectWriteTraceId, which you must send on every input |
| Shopify GraphQL Admin API mutation | 200 OK | userErrors with field path and message | Only if your mutation selects userErrors. Shopify returns them only when requested |
Google Sheets spreadsheets.batchUpdate | Error status | The whole request | Nothing to map. If any request is invalid, nothing is applied |
Sources: vendor documentation for each endpoint, checked October 2026.
Two rows deserve a closer look.
Salesforce's position rule is the one that bites. A failed SaveResult in the documented example has no id field at all, just success: false and the error. If your workflow splits the response into items and filters for failures, you now hold an error with nothing that says which of your records it belongs to. You have to zip the response array against the request array by index before you filter, while both are still in the same order.
Shopify's rule is the quiet one. The Admin API documentation says many errors that would be a 4xx or 5xx in REST come back as an HTTP 200 instead, and that to see mutation validation details you must request userErrors. A mutation copied from a tutorial that selects only the created object's id gets back null for that object and no explanation. Nothing failed, as far as the workflow knows.
Google Sheets is in the table as the contrast. Its batchUpdate is all or nothing, which means the failure is loud. Loud failures are the easy kind.
What it looks like in a real run
Here is an illustrative case, the shape we see often, not a measured one. A nightly job pushes 200 changed contacts from a booking system into Salesforce through sObject Collections. Three of them have an email that a duplicate rule rejects, and one has an ID copied from the wrong object.
The response is 200 OK with 200 entries. 196 say success: true. Three say DUPLICATES_DETECTED. One says MALFORMED_ID. The n8n execution is green, the step output shows 200 items, and anyone glancing at it reads "200 synced."
The next night, those four contacts have not changed in the source, so the incremental job does not pick them up again. They stay stale in Salesforce until someone edits them by hand. Batch failures do not repeat on their own, which is why they last. The record that failed once falls out of every later "changed since" window.
The check to put after every batch write
The fix is one step, placed right after the batch call, before anything downstream runs. It does four things.
First, normalize the response into one row per record you sent, carrying your own key. For Salesforce collections, zip by index. For HubSpot, set objectWriteTraceId to your source record ID when you build the request, so the error's context hands it straight back. For Bulk API 2.0, include your external ID as a column in the upload, because the failed-results CSV echoes your original fields. For Shopify, select userErrors on every mutation and keep your key alongside each call.
Second, count. Records sent must equal records succeeded plus records failed. If the numbers do not add up, the response is truncated or malformed, and the run should fail outright. This is the same discipline as reconciling counts on a read, applied to a write.
Third, split the failures by whether a retry can help. A row lock or timeout can succeed on a second attempt. DUPLICATES_DETECTED, MALFORMED_ID, a required field left blank, or a picklist value the destination does not accept will fail identically every time. Retry the first group once. Send the second group to a review queue with the error message and the source record link, as covered in when an automation should retry.
Fourth, write the outcome to the run record: sent, succeeded, failed, and the failed keys. If you already keep a run record for every automation, these are three numbers and a list. Then make the run end in a warning state, not success, whenever failed is above zero. In n8n that can be a Stop and Error node behind an IF on the failed count; in Make, a filtered route on the failed count that ends in an error after the good records are handled.
That last part matters more than it sounds. A run that partly failed and says so gets looked at. A run that partly failed and says success never does.
When to make the batch all or nothing instead
Salesforce lets you flip allOrNone to true, and the Bulk API 2.0 docs are explicit that successfully processed job data is not rolled back, so the choice is real and it is yours to make per flow.
Use all or nothing when the records in one request depend on each other. An order header and its line items, an account and the contacts that point at it, a parent and children written in one call. A half-written group is worse than none, the same reasoning as writing line items in one request.
Keep it off for independent records. A contact sync where one bad email blocks 199 good updates trades a small, visible problem for a large one.
Either way, read the body. With allOrNone: true, Salesforce still returns 200 OK, and every otherwise valid record comes back marked ALL_OR_NONE_OPERATION_ROLLED_BACK. A workflow that does not parse the results will report that nothing went wrong on a run that wrote nothing at all.
What to do next
Open the last successful run of each automation that writes in batches and look at the raw response body of the batch step, not the step's status. If you find a success: false, a numErrors above zero, or a non-empty userErrors, you have records that never arrived, and the check above is the next thing to build. If you would rather have someone audit every batch write in your stack and wire the reconciliation once, that is part of how we build workflow automation systems, and you can tell us what your sync looks like.
Frequently Asked Questions
SOURCES & CITATIONS
- Update Records Using sObject Collections, REST API Developer Guide — Salesforce Developershttps://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_sobjects_collections_update.htm
- Get Job Failed Record Results, Bulk API 2.0 Developer Guide — Salesforce Developershttps://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/get_job_failed_results.htm
- Error handling, HubSpot API reference — HubSpot Developershttps://developers.hubspot.com/docs/api-reference/latest/error-handling
- GraphQL Admin API reference, status and error codes — Shopifyhttps://shopify.dev/docs/api/admin-graphql
About Alexey Yushkin
Alexey is the founder of GENERAL INFORMATICS LLC. He designs and ships AI and automation systems for businesses and operators across the US.
Related reading
Want this kind of system in your business?
We build practical AI and automation systems for operators. Send us your current workflow and we will show you what to automate first.
