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-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
- YAML
[
{ "AssetType": "Story", "Name": "My First Story", "Scope": "System (All Projects)" },
{ "AssetType": "Story", "Name": "My Second Story", "Scope": "Scope:0" }
]
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
| Parameter | Effect |
|---|---|
?deleted=1 | Allow the request to consider deleted assets. See Querying deleted assets |
?previewOnly=true | Stop after projection and return the projection. See Preview mode |
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 }
}
| 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 left them untouched |
assetsOperatedOn | OID tokens of assets an execute command ran against |
commandFailures | One entry per failed command; see Errors |
queryResult | One entry in results per query in the request |
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.
Before sending a large or destructive batch:
- Preview it, and keep the response.
?previewOnly=trueexpands 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 massClose/Deletehitting 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
updatecommand may carry aChangeCommentinside itsupdatenode, 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:executeoperation commands do not accept aChangeComment— only create and update commands do — so a mass-operation batch such asCloseorDeletecannot 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:
| 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 |
total is opt-intotal 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.
- JSON
- YAML
[
{ "config": "counts-only" },
{
"AssetType": "Story",
"Scope": "Scope:0",
"Name": "My Story",
"Children": [{ "AssetType": "Test", "Name": "My Test" }]
}
]
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.
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.