Skip to main content

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.

warning
Always check commandFailures.count

A 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.

Malformed JSON returns an opaque 500, not a 400

Send a body that is not valid JSON and you get:

HTTP 500
{
"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 500 with no commandFailures as "probably my payload". A real command failure comes back as 200 with an itemised commandFailures entry. If you get a 500, 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 500 from 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.

Worth knowing when comparing surfaces

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
}
}
PropertyMeaning
<alias>The projected command, keyed by its user or auto alias
error.messageThe failure reason
error.sourceCommandIndexZero-based index of the command in your original payload that produced this failure
error.stackTraceDevelopment builds only — never depend on it. See below
warning
stackTrace never appears on customer deployments

error.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.

Request — the Scope does not exist
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:

commandFailures
{
"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. sourceCommandIndex identifies the command you sent, not the projected command.
  • The #ContextOid chain 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.

Resubmit echoed commands with their alias wrapper intact

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:

Original request — one misspelled OID token
[
{ "AssetType": "Task", "Parent": "Story:1591", "Name": "Task Time" },
{ "AssetType": "Test", "Parent": "Stor:1591", "Name": "Test Time" }
]
Response — one created, one failed
{
"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:

Reissue
[
{
"@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 error property. 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 #ContextOid references between entries still line up.
  • The successful commands are absent from the list, so nothing is applied twice.
Idempotency is your responsibility

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.messageCause
Unknown AssetType: Animalfrom names an asset type that does not exist
Unknown AttributeDefinition: Scope.MuddleBubbleselect 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.ScopeA 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'WorkitemA relation value given by name matched no asset — see monikers
Resolution'Ambiguous'Story.Timebox'TimeboxA 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.