Skip to main content

Request and Response Format

Endpoint and Content Type​

POST ~/api/asset

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

Content-TypeBody
application/jsonA single command object, or an array of command objects
text/yamlA 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.

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.

Query-String Parameters​

ParameterEffect
?deleted=1Allow the request to consider deleted assets. See Querying deleted assets
?previewOnly=trueStop after projection and return the projection. See Preview mode
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 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:

{
"assetsCreated": { "oidTokens": [], "count": 0 },
"assetsModified": { "oidTokens": [], "count": 0 },
"assetsOperatedOn": { "oidTokens": [], "count": 0 },
"commandFailures": { "commands": [], "count": 0 },
"queryResult": { "results": [], "count": -1 }
}
PropertyMeaning
assetsCreatedOID tokens of assets created, in the order the create commands appeared
assetsModifiedOID tokens of assets an update command addressed — matched assets count even when a find-and-replace left them untouched
assetsOperatedOnOID tokens of assets an execute command ran against
commandFailuresOne entry per failed command; see Errors
queryResultOne entry in results per query in the request
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:

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.

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

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

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.

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

PropertyMeaning
page.startZero-based index of the first row returned
page.sizePage size; defaults to 2147483647 when unpaged
totalTotal 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.

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.

[
{ "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:

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

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.