Errors and Partial Failures
The Bulk API is not transactional. A request that partly succeeds returns
HTTP 200 OK with the successes applied and the failures itemised in
commandFailures.
commandFailures.countA 200 status tells you the request was understood, not that it worked. Check
the failure count on every response.
Before the Failure Model Applies: Unparseable Requests
The graceful partial-failure model below covers commands the server understood. A request it cannot parse never reaches that stage, and fails in a much less helpful way.
Send a body that is not valid JSON and you get:
{
"Exceptions": ["Server Error", "Reference: 76894036-5f86-49fd-bb4f-58970c644e2c"]
}
There is no indication that the problem was your JSON. No parse error, no
line or character position, no 400. The response is indistinguishable from a
genuine server fault, and the reference GUID is only useful to someone with
access to the host's exception log — which on a hosted instance is nobody
outside Digital.ai.
A missing body behaves the same way — see request format.
What to Do About It
Because the server will not tell you, the diagnosis has to happen on your side:
-
Validate the JSON before sending it. Any parser will do. This is worth doing in the client rather than debugging against the API.
-
Treat a
500with nocommandFailuresas "probably my payload". A real command failure comes back as200with an itemisedcommandFailuresentry. If you get a500, the request almost certainly never parsed. -
Suspect the shape, not just the syntax. Structurally valid JSON in the wrong shape fails the same way — the sibling-alias trap is a
500from perfectly well-formed JSON. -
Keep the reference GUID if you need to raise a support case. It is the only handle on the underlying error, and on a hosted instance it is the only thing you can do with it.
On an on-premise instance the matching entry is written to an exception log on the host, recording the real exception type and message in full. That is only reachable if you have access to the host's file store — which the person writing the integration frequently does not. Treat it as a route worth asking an administrator about, not one you can rely on.
Other Agility endpoints are stricter and more informative about payload problems.
api/webhook rejects unknown keys with a 400 and a
message; api/localizations names the offending key.
The Bulk API's tolerance of unknown keys is convenient, but it comes with a
correspondingly blunt response when the body is wrong.
A truncated object and an entirely non-JSON body both return 500 with the
two-element Exceptions array shown above.
The Failure Entry Shape
Each entry in commandFailures.commands is a complete, resubmittable
command with an extra error property.
{
"commandFailures": {
"commands": [
{
"@06b3bf9a-8906-4376-9bf6-672ced28b350$auto-aliased": {
"AssetType": "Test",
"Parent": "Stor:1591",
"Name": "Test Time"
},
"error": {
"message": "Resolution'Unresolved'Test.Parent'Workitem",
"sourceCommandIndex": 1
}
}
],
"count": 1
}
}
| Property | Meaning |
|---|---|
<alias> | The projected command, keyed by its user or auto alias |
error.message | The failure reason |
error.sourceCommandIndex | Zero-based index of the command in your original payload that produced this failure |
error.stackTrace | Development builds only — never depend on it. See below |
stackTrace never appears on customer deploymentserror.stackTrace is not part of the contract. Release builds — which is what every
customer deployment runs, hosted and on-premise alike — never include stack traces or
exception detail in error responses. This is not a configuration default: no setting an
administrator can reach turns it on.
The field exists only on development builds, which are internal to Digital.ai — no customer or partner deployment ships one. That is why an example payload captured on an internal development instance may include the field, and why the property is worth documenting at all. On any deployment you can reach as a customer or partner, it is never present. On-premise administrators who need stack traces for diagnosis have them in the server-side exception logs; that detail is never returned to API callers.
The practical consequence: an example or test captured against an internal development instance can mislead about what any customer deployment returns, with no API change and no version difference. Strip the field before comparing responses, and never parse it or assert on it.
Because the entry echoes the projected command rather than your original
text, an update failure comes back in normalised oid + add/remove form:
{
"@update": {
"oid": "Scope:0",
"update": { "MuddleBubble": { "add": ["BubbleMuddle"], "remove": [] } }
},
"error": {
"message": "Unknown AttributeDefinition: Scope.MuddleBubble",
"sourceCommandIndex": 0
}
}
and an execute failure comes back in op + args form:
{
"@op": { "oid": "Scope:0", "execute": { "op": "NonexistentOperation", "args": {} } },
"error": {
"message": "Unknown Operation 'Scope.NonexistentOperation'",
"sourceCommandIndex": 0
}
}
Cascading Failures in a Hierarchy
When a create command fails, every descendant fails too — because each
descendant's #ContextOid points at an alias that never resolved.
AssetType: Epic
Scope: Scope:9999
Name: My Epic on a Scope that DOES NOT EXIST
Subs:
- AssetType: Story
Name: My Story
Children:
- AssetType: Test
Name: My Test
- AssetType: Task
Name: My Task
One mistake produces four failures:
{
"commands": [
{
"@epic": { "AssetType": "Epic", "Scope": "Scope:9999", "Name": "My Epic on a Scope that DOES NOT EXIST" },
"error": { "message": "Violation'Invalid'Epic.Scope", "sourceCommandIndex": 0 }
},
{
"@story": { "AssetType": "Story", "Name": "My Story", "#ContextOid": "@epic" },
"error": { "message": "Invalid OID token: @epic", "sourceCommandIndex": 0 }
},
{
"@test": { "AssetType": "Test", "Name": "My Test", "#ContextOid": "@story" },
"error": { "message": "Invalid OID token: @story", "sourceCommandIndex": 0 }
},
{
"@task": { "AssetType": "Task", "Name": "My Task", "#ContextOid": "@story" },
"error": { "message": "Invalid OID token: @story", "sourceCommandIndex": 0 }
}
],
"count": 4
}
Read it as follows:
- The root cause is the first entry. The other three are consequences.
- All four share
sourceCommandIndex: 0, because the whole hierarchy came from a single top-level command in the original payload.sourceCommandIndexidentifies the command you sent, not the projected command. - The
#ContextOidchain is intact, so the shape of the original hierarchy survives in the failure list.
Reissuing a Failed Request
Because failure entries are complete commands, the recovery loop is mechanical: fix the value, resubmit the array.
An echoed update is addressed by oid, and that form is only accepted
inside an alias wrapper ({ "@name": { "oid": …, "update": … } }) — the
shape the failure entry already has. Strip the wrapper and the bare
{ "oid": …, "update": … } object is read as a query and rejected with
Invalid key at node(s): root->oid. Echoed operation failures do not
resubmit cleanly in either form on current versions — rewrite them as
from-addressed execute commands instead.
Given a partially failed request:
[
{ "AssetType": "Task", "Parent": "Story:1591", "Name": "Task Time" },
{ "AssetType": "Test", "Parent": "Stor:1591", "Name": "Test Time" }
]
{
"assetsCreated": { "oidTokens": ["Task:1596"], "count": 1 },
"commandFailures": {
"commands": [
{
"@06b3bf9a-8906-4376-9bf6-672ced28b350$auto-aliased": {
"AssetType": "Test", "Parent": "Stor:1591", "Name": "Test Time"
},
"error": {
"message": "Resolution'Unresolved'Test.Parent'Workitem",
"sourceCommandIndex": 1
}
}
],
"count": 1
}
}
Take commandFailures.commands verbatim, correct Stor:1591 to Story:1591,
and post it back:
[
{
"@06b3bf9a-8906-4376-9bf6-672ced28b350$auto-aliased": {
"AssetType": "Test", "Parent": "Story:1591", "Name": "Test Time"
},
"error": {
"message": "Resolution'Unresolved'Test.Parent'Workitem",
"sourceCommandIndex": 1
}
}
]
Three things make this safe:
- You do not have to strip the
errorproperty. The server ignores it. - You do not have to strip the server-generated aliases. They are treated
as user aliases on the way back in, so
#ContextOidreferences between entries still line up. - The successful commands are absent from the list, so nothing is applied twice.
Reissuing the failure list is safe. Reissuing the original payload is not — the commands that already succeeded would run again, creating duplicates.
Error Message Catalogue
error.message | Cause |
|---|---|
Unknown AssetType: Animal | from names an asset type that does not exist |
Unknown AttributeDefinition: Scope.MuddleBubble | select or update names an attribute the type does not have |
Invalid QueryFilter2 token MuddleBubble='BubbleMuddle' | where names an unknown attribute |
Unknown Operation 'Scope.NonexistentOperation' | execute names an operation the type does not support |
Violation'Invalid'Epic.Scope | A relation points at a non-existent asset |
Invalid OID token: @<alias> | An alias never resolved, because the command that would have created it failed |
Resolution'Unresolved'Test.Parent'Workitem | A relation value given by name matched no asset — see monikers |
Resolution'Ambiguous'Story.Timebox'Timebox | A relation value given by name matched more than one asset — same page |
Note the inconsistency worth planning around: an unknown attribute in select
reports Unknown AttributeDefinition, while the same mistake in where
reports a QueryFilter2 parser message. Do not match on message text alone if
you can avoid it.
The Violation'...' and Resolution'...' forms are Agility's apostrophe-
delimited message tokens, shared with other endpoints. See
Error handling.
Related Topics
- Preview mode — catch these before they happen
- Projection, processing and aliases
- Error handling
- HTTP status codes