ConceptsBatch operations
Batch operations
Create, update, delete or restore many records in one call. By default one invalid item fails the whole batch; with rollback-method=1 a batch can succeed in part.
Answer
By default, one invalid item fails the whole batch with HTTP 422 and nothing is written; the error does not say which item failed. With rollback-method=1 the valid items are written, the batch answers HTTP 200 with success: false, and data holds one entry per item in input order: the record's ID or an error.
A batch writes many records in one request. It is faster than one request per record, and it decides what happens to the valid items when one of them is invalid: read that part before you send your first batch.
Batch endpoints
| Endpoint | Body | Does |
|---|---|---|
POST / |
An array of records | Creates the items without an id, updates the items with one |
POST / |
An array of IDs | Soft-deletes the records |
POST / |
An array of IDs | Restores soft-deleted records |
In GraphQL, bulkUpdate<Entity> mutations such as bulkUpdateContact create and update in
one call. Every entity's batch operations are on its reference page, such as
POST /entities/Contacts/batch-modify and
bulkUpdateContact.
The API sets no batch size limit. Send up to 100 records per request for a quick response: a batch takes about 40 ms per record, so 500 records take about 20 s. A larger batch is not refused, but it answers more slowly, and by default one invalid item throws away more work.
Deleting a record leaves its relation records active. Deleting contacts or accounts leaves their
ContactAccountRelations in place, still pointing at the deleted records, and deleting a
CloudObjectRelation leaves its CloudObject, the file. Delete those on their own.
By default, all or nothing
The sample sends three contacts; the third has no owner. Sent without rollback-method, the
batch fails as a whole:
# No --fail-with-body: the 422 is the expected answer here.
curl -sS -w '\nHTTP %{http_code}\n' "$COEVERA_BASE/entities/Contacts/batch-modify" \
-u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
-H "Content-Type: application/json" \
-d "$CONTACTS"const strict = await call("/entities/Contacts/batch-modify", contacts);
console.log(`Default rollback: HTTP ${strict.status}`, JSON.stringify(strict.body, null, 2));strict = session.post(f"{BASE}/entities/Contacts/batch-modify", json=contacts, timeout=60)
print(f"Default rollback: HTTP {strict.status_code}", json.dumps(strict.json(), indent=2))batch ROLLBACK_ALL | gql
echoconst strict = await gql(ADD_CONTACTS, { input: contacts, rollbackMethod: "ROLLBACK_ALL" });
console.log("ROLLBACK_ALL:", JSON.stringify(strict, null, 2));strict = gql(ADD_CONTACTS, {"input": contacts, "rollbackMethod": "ROLLBACK_ALL"})
print("ROLLBACK_ALL:", json.dumps(strict, indent=2))The API answers HTTP 422 with code 40000 ERROR_ENTITY_VALIDATION and writes nothing, not even
the two valid contacts. The error carries entity_index: null and an entity_id the server made
up for the new record, so it does not tell you which input item failed:
{
"code": 40000,
"name": "ERROR_ENTITY_VALIDATION",
"message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
"entity_id": "0c000000-0000-4000-a000-000000000001",
"entity_name": "Contact",
"entity_errors": [],
"field_errors": [
{
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "unit_id",
"name": "ERROR_FIELD_VALIDATION",
"code": 40002,
"errors": [
{
"code": 119,
"name": "ERROR_PARAMETER_MISSING",
"message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.",
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "unit_id"
},
{
"code": 119,
"name": "ERROR_PARAMETER_MISSING",
"message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "unit_id"
}
]
},
{
"field_id": "10000000-0000-4000-a000-000000000002",
"field_name": "owner_id",
"name": "ERROR_FIELD_VALIDATION",
"code": 40002,
"errors": [
{
"code": 119,
"name": "ERROR_PARAMETER_MISSING",
"message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.",
"field_id": "10000000-0000-4000-a000-000000000002",
"field_name": "owner_id"
},
{
"code": 119,
"name": "ERROR_PARAMETER_MISSING",
"message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.",
"field_id": "10000000-0000-4000-a000-000000000002",
"field_name": "owner_id"
}
]
}
],
"step_checklist_errors": [],
"entity_index": null,
"http_status": 422,
"success": false
}Show all 58 linesShow less
{
"errors": [
{
"message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"bulkUpdateContact"
],
"api_error": {
"code": 40000,
"name": "ERROR_ENTITY_VALIDATION",
"message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
"entity_id": "0c000000-0000-4000-a000-000000000001",
"entity_name": "Contact",
"entity_errors": [],
"field_errors": [
{
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "unit_id",
"name": "ERROR_FIELD_VALIDATION",
"code": 40002,
"errors": [
{
"code": 119,
"name": "ERROR_PARAMETER_MISSING",
"message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit' is null or empty.",
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "unit_id"
},
{
"code": 119,
"name": "ERROR_PARAMETER_MISSING",
"message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].unit_id' is null or empty.",
"field_id": "10000000-0000-4000-a000-000000000001",
"field_name": "unit_id"
}
]
},
{
"field_id": "10000000-0000-4000-a000-000000000002",
"field_name": "owner_id",
"name": "ERROR_FIELD_VALIDATION",
"code": 40002,
"errors": [
{
"code": 119,
"name": "ERROR_PARAMETER_MISSING",
"message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner' is null or empty.",
"field_id": "10000000-0000-4000-a000-000000000002",
"field_name": "owner_id"
},
{
"code": 119,
"name": "ERROR_PARAMETER_MISSING",
"message": "Non-null field 'Contact'[0c000000-0000-4000-a000-000000000001].owner_id' is null or empty.",
"field_id": "10000000-0000-4000-a000-000000000002",
"field_name": "owner_id"
}
]
}
],
"step_checklist_errors": [],
"entity_index": null
}
}
],
"data": {
"bulkUpdateContact": null
}
}Show all 75 linesShow less
GraphQL's default, rollbackMethod: ROLLBACK_ALL, behaves the same way, with HTTP 200, the error
in errors and no result. A batch-delete with an ID that does not exist fails the same way: HTTP
404, code 143 ERROR_NO_ENTITY, and nothing is deleted.
Partial failures
The same batch sent with rollback-method=1 writes the two valid contacts and reports the third.
The create an account with contacts recipe adds its
contacts this way:
partial=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Contacts/batch-modify?rollback-method=1" \
-u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
-H "Content-Type: application/json" \
-d "$CONTACTS")
echo "$partial"const partial = await call("/entities/Contacts/batch-modify?rollback-method=1", contacts);
console.log(`rollback-method=1: HTTP ${partial.status}`, JSON.stringify(partial.body, null, 2));
if (partial.status !== 200) throw new Error(`Expected HTTP 200, got ${partial.status}`);partial = session.post(
f"{BASE}/entities/Contacts/batch-modify",
params={"rollback-method": 1},
json=contacts,
timeout=60,
)
print(f"rollback-method=1: HTTP {partial.status_code}", json.dumps(partial.json(), indent=2))
if partial.status_code != 200:
sys.exit(f"Expected HTTP 200, got {partial.status_code}")partial=$(batch ROLLBACK_FAILED | gql)
echo "$partial"
if jq -e '.errors' <<<"$partial" >/dev/null; then
echo "Unexpected GraphQL errors" >&2
exit 1
ficonst partial = await gql(ADD_CONTACTS, { input: contacts, rollbackMethod: "ROLLBACK_FAILED" });
console.log("ROLLBACK_FAILED:", JSON.stringify(partial, null, 2));
if (partial.errors) throw new Error(`Unexpected GraphQL errors: ${JSON.stringify(partial.errors)}`);partial = gql(ADD_CONTACTS, {"input": contacts, "rollbackMethod": "ROLLBACK_FAILED"})
print("ROLLBACK_FAILED:", json.dumps(partial, indent=2))
if partial.get("errors"):
sys.exit(f"Unexpected GraphQL errors: {json.dumps(partial['errors'])}"){
"success": false,
"data": [
"0c000000-0000-4000-a000-000000000002",
"0c000000-0000-4000-a000-000000000003",
{
"code": 40000,
"name": "ERROR_ENTITY_VALIDATION",
"message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].unit_id' is null or empty."
}
]
}{
"data": {
"bulkUpdateContact": {
"result": {
"created": [
{
"index": 0,
"id": "0c000000-0000-4000-a000-000000000002"
},
{
"index": 1,
"id": "0c000000-0000-4000-a000-000000000003"
}
],
"errors": [
{
"index": 2,
"code": 40000,
"message": "\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].owner' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].unit' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].owner_id' is null or empty.\nNon-null field 'Contact'[0c000000-0000-4000-a000-000000000004].unit_id' is null or empty."
}
]
}
}
}
}Show all 25 linesShow less
Read per-item results
data holds one entry per input item, in input order:
| Item | Entry in data |
Meaning |
|---|---|---|
| 0 | "0c000000-…-000000000002" |
Created; the record's ID |
| 1 | "0c000000-…-000000000003" |
Created |
| 2 | { "code": 40000, "name": "ERROR_ |
Failed; nothing was written for this item |
An error entry has only code, name and message, without field_errors. In GraphQL,
rollbackMethod: ROLLBACK_FAILED returns result.created and result.errors, each entry with
the index of its input item.
Checking only the HTTP status loses data
With rollback-method=1, a batch with failed items still answers HTTP 200. An integration that
treats every 200 as success drops the failed items without noticing. Always walk data, and
treat success: false as "some items failed".
rollback-method
| Value | GraphQL | Effect |
|---|---|---|
0 (default) |
ROLLBACK_ |
One invalid item fails the whole batch: HTTP 422 (404 for an unknown ID in batch-delete). Nothing is written, and the error does not say which item failed. |
1 |
ROLLBACK_ |
HTTP 200 with success: false when an item failed. The valid items are written; each failed item has its error at its position in data. |
rollback-method applies to batch-modify, batch-delete and batch-restore.
validation-level applies to batch-modify only: on batch-delete and batch-restore it is
refused with HTTP 400, code 118 ERROR_PARAMETER_UNKNOWN. See validation.
Check every item in code
Walk data after every batch sent with rollback-method=1, and report or retry the failed
items:
# One line per item, in input order: the new ID, or the error in its place.
jq -r '.data | to_entries[]
| if (.value | type) == "string" then "\(.key) created \(.value)"
else "\(.key) FAILED \(.value.code) \(.value.name)" end' <<<"$partial"type ItemError = { code: number; name: string; message: string };
const results = partial.body.data.map((item: string | ItemError, index: number) =>
typeof item === "string"
? { index, ok: true, id: item }
: { index, ok: false, code: item.code, message: item.message },
);
const failed = results.filter((r: { ok: boolean }) => !r.ok);
if (failed.length) console.warn(`${failed.length} of ${results.length} items failed`, failed);results = [
{"index": index, "ok": True, "id": item}
if isinstance(item, str)
else {"index": index, "ok": False, "code": item["code"], "message": item["message"]}
for index, item in enumerate(partial.json()["data"])
]
failed = [result for result in results if not result["ok"]]
if failed:
print(f"{len(failed)} of {len(results)} items failed", failed, file=sys.stderr)jq -r '.data.bulkUpdateContact.result
| (.created[] | "\(.index) created \(.id)"),
(.errors[] | "\(.index) FAILED \(.code)")' <<<"$partial"const { created, errors } = partial.data.bulkUpdateContact.result;
for (const item of created) console.log(`${item.index} created ${item.id}`);
for (const item of errors) console.warn(`${item.index} FAILED ${item.code}: ${item.message.trim()}`);result = partial["data"]["bulkUpdateContact"]["result"]
for item in result["created"]:
print(f"{item['index']} created {item['id']}")
for item in result["errors"]:
print(f"{item['index']} FAILED {item['code']}: {item['message'].strip()}", file=sys.stderr)Limits
| Limit | Value | Details |
|---|---|---|
| Batch size | None set | Up to 100 per request recommended; 500 take about 20 s |
| Which item failed | Not reported by default | Use rollback-method=1 to get per-item results |
validation-level |
batch-modify only |
Refused on batch-delete and batch-restore with HTTP 400, code 118 |
| Relation records | Not deleted with their records | Delete ContactAccountRelations and the CloudObject of a removed attachment on their own |
| Request body size | None documented | A batch of 500 records (42 kB) is accepted |