---
id: request-format
title: 'Request and Response Format'
description: 'Content types, the JSON-array and YAML-multi-document forms, the response envelope, resultMetaData, and config counts-only.'
sidebar_label: 'Request and Response Format'
sidebar_position: 2
tags: ['api', 'bulk', 'asset', 'reference']
---

## Endpoint and Content Type

```text
POST ~/api/asset
```

The endpoint accepts two content types, and every example in this section is
proven in both:

| Content-Type | Body |
| --- | --- |
| `application/json` | A single command object, or an array of command objects |
| `text/yaml` | A single YAML document, or several documents separated by `---` |

The two are exactly equivalent. A JSON array of _n_ commands and a YAML stream
of _n_ documents produce identical results.

```json tab title="Two commands"
[
  { "AssetType": "Story", "Name": "My First Story", "Scope": "System (All Projects)" },
  { "AssetType": "Story", "Name": "My Second Story", "Scope": "Scope:0" }
]
```

```yaml tab title="The same two commands"
AssetType: Story
Name: My First Story
Scope: System (All Projects)
---
AssetType: Story
Name: My Second Story
Scope: Scope:0
```


A single command does **not** need to be wrapped in an array. Sending one bare
JSON object is equivalent to sending a one-element array; the projection phase
normalises it. See
[Projection, processing and aliases](/apis/asset-bulk/aliases-and-projection).

### Query-String Parameters

| Parameter | Effect |
| --- | --- |
| `?deleted=1` | Allow the request to consider deleted assets. See [Querying deleted assets](/apis/asset-bulk/queries#querying-deleted-assets) |
| `?previewOnly=true` | Stop after projection and return the projection. See [Preview mode](/apis/asset-bulk/preview-mode) |

:::warning A missing body is a hard 500
`POST ~/api/asset` with no body returns **HTTP 500**, not a `commandFailures`
entry. The graceful partial-failure model described in
[Errors and partial failures](/apis/asset-bulk/errors) applies to commands the
server was able to parse; a request it cannot parse at all fails outright.
:::

## The Response Envelope

Every successful request returns HTTP `200 OK` with the same top-level
envelope, regardless of which command kinds it contained:

```json
{
  "assetsCreated": { "oidTokens": [], "count": 0 },
  "assetsModified": { "oidTokens": [], "count": 0 },
  "assetsOperatedOn": { "oidTokens": [], "count": 0 },
  "commandFailures": { "commands": [], "count": 0 },
  "queryResult": { "results": [], "count": -1 }
}
```

| Property | Meaning |
| --- | --- |
| `assetsCreated` | OID tokens of assets created, in the order the create commands appeared |
| `assetsModified` | OID tokens of assets an `update` command addressed — matched assets count even when a [find-and-replace](/apis/asset-bulk/find-and-replace) left them untouched |
| `assetsOperatedOn` | OID tokens of assets an `execute` command ran against |
| `commandFailures` | One entry per failed command; see [Errors](/apis/asset-bulk/errors) |
| `queryResult` | One entry in `results` per query in the request |

:::warning A 200 does not mean everything succeeded
The Bulk API returns `200 OK` even when individual commands fail. Partial
failure is reported in `commandFailures`, not in the HTTP status code. Always
check `commandFailures.count`. Every error test in the suite asserts
`res.status === 200` alongside a non-empty `commandFailures`.
:::

### Ordering Guarantees

`assetsCreated.oidTokens` is ordered to match the order of the create commands
after hierarchy flattening — parent first, then its children in the order they
were declared. Tests rely on this by destructuring the array positionally, for
example:

```ts
const [scopeOidToken, epicOidToken, story1, story2, story3, task, test] =
  res.data.assetsCreated.oidTokens;
```


### The Same Token Can Appear Twice

`assetsOperatedOn.oidTokens` may contain the same OID token more than once
when an operation is invoked repeatedly against a single asset — for example
when using the `list` form of `execute`.


An OID token can also appear in **both** `assetsCreated` and
`assetsOperatedOn` when a request creates an asset and then invokes an
operation on it. See
[Compound requests](/apis/asset-bulk/compound-requests).

## Processing Model: Large Batches Run to Completion

A single `api/asset` request can carry many commands — hundreds or thousands of
asset updates in one array. How the server processes that batch has consequences
worth understanding before you send a large one: it applies immediately, one
command at a time, and no single action undoes the batch once it has run.

The "bulk" in the Bulk API refers to how many operations you can submit in one
request, not to how they run. The server executes them one at a time, each as
its own short transaction. That is a deliberate choice: many small transactions
keep the server's footprint low while the batch runs — no long-held, monolithic
transaction holding locks across thousands of changes — at the cost of atomicity
and raw throughput. So a bulk request saves you round trips and stays gentle on
the server, but gives you none of the all-or-nothing guarantee a single batched
transaction would.

**There is no enforced time limit on processing a request.** A batch of a
thousand updates takes as long as it takes; the server does not cap the work or
abort a long-running batch. Set a generous **client** timeout accordingly — but
understand what a client timeout does and does not do (see below).

**Each command is its own database transaction — the request is not atomic.**
The Bulk API applies commands one at a time, committing each before moving to the
next; it is [not transactional](/apis/asset-bulk/errors). A batch that fails
partway leaves the already-processed commands applied and reports the rest in
`commandFailures`. There is no single enclosing transaction to roll back. (This
per-command commit is a deliberate trade: it is gentler on the server than
applying an entire large batch in one long-held transaction would be, at the cost
of atomicity — a partly-applied batch is a state you must be prepared to handle,
not one the server will undo for you.)

**Once submitted, a batch cannot be cancelled — and a timeout does not stop it.**
There is no cancel endpoint. A timeout you observe is almost always at a proxy or
gateway in front of the server: once the request reaches the point where the
server has read the body, processing runs to completion regardless of whether
anything is still listening for the response. A client or gateway timeout
therefore tells you nothing about how much was applied — you can be left with a
fully- or partly-applied mutation and **no response to say which**. After a
timeout on a mutating batch, re-query to discover the actual state rather than
assuming the request did nothing, and **never blindly resubmit the original
payload** — the commands that already committed would run again.

**Individual changes are reversible, but there is no batch-level undo.** Every
update and every operation Agility applies is recorded in the asset's
[history as a moment](/concepts/moments-and-history), so a change can be traced
and reverted one asset at a time. What you do not get is a single "undo this
batch" action: unwinding a large accidental batch means finding every asset it
touched and reversing each. That is exactly the reconciliation you want to avoid
having to do — so the discipline is to prevent the bad batch, and to make its
footprint findable if one slips through.

:::warning A large mutating batch is not atomic and has no batch undo — preview and stamp it
Before sending a large or destructive batch:

- **Preview it, and keep the response.** [`?previewOnly=true`](/apis/asset-bulk/preview-mode)
  expands and validates the whole request without applying anything — the one safe
  dry run you get, and the front-line defense against a mis-scoped update or a mass
  `Close`/`Delete` hitting more assets than you intended. Because the API returns no
  durable receipt of what a live run committed, that preview response is the best
  record you have of what the batch _would_ do — and since the projection
  [round-trips](/apis/asset-bulk/preview-mode#the-projection-round-trips) as a valid
  request body, a saved preview doubles as your reissue payload. Treat it as a record
  of intent, not proof of effect: state can change between preview and commit, so
  reconcile against a re-query afterward.
- **Stamp updates with a change comment so you can find them later.** An `update`
  command may carry a [`ChangeComment`](/apis/asset-bulk/updating-assets#recording-a-change-comment)
  inside its `update` node, which becomes the audit comment on every moment the
  command creates. A distinctive comment turns "find everything that batch touched"
  into a single history query. (Note: `execute` operation commands do **not**
  accept a `ChangeComment` — only create and update commands do — so a
  mass-operation batch such as `Close` or `Delete` cannot be tagged this way.
  Previewing those matters all the more.)
- **Size batches for recoverability, not just throughput.** Smaller batches mean a
  partial failure or a timeout leaves less to reconcile, and the
  [failure list is directly resubmittable](/apis/asset-bulk/errors#reissuing-a-failed-request).
:::

## `queryResult`

`queryResult.count` is the number of **result sets**, not the number of assets.
A request with no queries returns `"count": -1`. A request with two queries
returns `"count": 2`, even if both are empty.

```json
"queryResult": {
  "resultMetaData": [
    { "page": { "start": 0, "size": 2147483647 }, "total": 1 }
  ],
  "results": [
    [{ "_oid": "Story:1585", "Name": "First Story" }]
  ],
  "count": 1
}
```

### `resultMetaData`

Alongside `results`, the response carries a parallel `resultMetaData` array
with one entry per result set:

| Property | Meaning |
| --- | --- |
| `page.start` | Zero-based index of the first row returned |
| `page.size` | Page size; defaults to `2147483647` when unpaged |
| `total` | Total matching assets ignoring paging — but `-1` unless `needTotal` is set |

:::warning `total` is opt-in
`total` is only computed when the query command sets `needTotal: true` (or the
instance's `ApiDefaultNeedTotal` setting turns it on). Otherwise it is `-1`,
meaning "not computed". See
[Paging and totals](/apis/asset-bulk/queries#paging-and-totals).
:::


## `config counts-only`

A request can suppress the OID token arrays and return only counts by placing a
`config` command in the request. This is a request-level setting expressed as
its own command, so it occupies a position in the command list.

```json tab
[
  { "config": "counts-only" },
  {
    "AssetType": "Story",
    "Scope": "Scope:0",
    "Name": "My Story",
    "Children": [{ "AssetType": "Test", "Name": "My Test" }]
  }
]
```

```yaml tab
config: counts-only
---
AssetType: Story
Scope: Scope:0
Name: My Story
Children:
- AssetType: Test
  Name: My Test
```

The affected response properties then omit `oidTokens` entirely rather than
returning it empty:

```json
"assetsCreated": { "count": 2 }
```

Use this when you are applying a very large batch and do not need to correlate
the results — it keeps the response small.


:::warning Not covered by tests
The tests only exercise the literal value `counts-only`. Whether other `config`
values exist, and whether `counts-only` also suppresses `queryResult` detail,
is not verified here.
:::

## Related Topics

- [Creating assets](/apis/asset-bulk/creating-assets)
- [Queries](/apis/asset-bulk/queries)
- [Errors and partial failures](/apis/asset-bulk/errors)
- [HTTP status codes](/reference/status-codes)
