---
id: errors
title: 'Errors and Partial Failures'
description: 'How the Bulk API reports partial failure, how failures cascade through a hierarchy, and how to correct and reissue a failed request safely.'
sidebar_label: 'Errors and Partial Failures'
sidebar_position: 11
tags: ['api', 'bulk', 'asset', 'errors']
---

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.

:::danger Malformed JSON returns an opaque 500, not a 400

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

```json title="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](/apis/asset-bulk/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](/apis/asset-bulk/aliases-and-projection#limitations) 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.

:::note Worth knowing when comparing surfaces

Other Agility endpoints are stricter and more informative about payload problems.
[`api/webhook`](/apis/webhooks/create) rejects unknown keys with a `400` and a
message; [`api/localizations`](/apis/loc-2-v1/overrides) 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.

```json
{
  "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 |

:::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:

```json
{
  "@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:

```json
{
  "@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.

```yaml title="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:

```json title="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**.

:::warning 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:

```json title="Original request — one misspelled OID token"
[
  { "AssetType": "Task", "Parent": "Story:1591", "Name": "Task Time" },
  { "AssetType": "Test", "Parent": "Stor:1591", "Name": "Test Time" }
]
```

```json title="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:

```json title="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.

:::warning 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.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](/apis/asset-bulk/creating-assets#moniker-must-be-unique) |
| `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](/guides/error-handling).

## Related Topics

- [Preview mode](/apis/asset-bulk/preview-mode) — catch these before they happen
- [Projection, processing and aliases](/apis/asset-bulk/aliases-and-projection)
- [Error handling](/guides/error-handling)
- [HTTP status codes](/reference/status-codes)
