Projection, Processing and Aliases
Every api/asset request is fulfilled in two phases. Almost everything that
looks surprising about the Bulk API follows from this split.
| Phase | What happens |
|---|---|
| Projection | The request is normalised into a flat, ordered list of single-purpose commands, each labelled with an alias. Hierarchies are flattened, shorthand syntaxes are expanded, and queries are left in place. Nothing is written. |
| Processing | Each projected command is executed in order. Aliases are resolved to real OID tokens as the assets they name come into existence. |
The result of the projection phase is a mapping of aliases to commands. You can inspect it directly with preview mode.
Why the Split Matters
- Aliases: a command can refer to an asset that does not exist yet, because resolution happens in the second phase.
- Flattening: nested creates become sibling commands linked by
#ContextOid, which is why a partial failure returns a flat list you can edit and resubmit. - Deferral: user-aliasing a command postpones its projection until processing, so it can see assets created earlier in the same request.
- Preview: stopping after projection tells you exactly what will happen without writing anything.
Auto-Aliasing
If you do not name a command, the server names it for you.
{ "AssetType": "Story", "Scope": "Scope:0", "Name": "My Story" }
[
{
"@e630d6f8-5b95-44b7-8d30-3632549a0121$auto-aliased": {
"AssetType": "Story",
"Scope": "Scope:0",
"Name": "My Story"
}
}
]
Details worth noticing:
- A single object is normalised into a one-element array. Each element is a wrapper object with exactly one property, whose value is the command.
- The property name is a GUID. The
$auto-aliasedsuffix marks it as server-generated rather than user-supplied. - The suffix appears only on auto-aliased asset create commands. An auto-aliased query or update gets a bare GUID with no suffix.
User-Aliasing
Wrap a command in a property whose name starts with @ to name it yourself.
{
"@story": { "AssetType": "Story", "Scope": "Scope:0", "Name": "My Story" }
}
[
{
"@story": { "AssetType": "Story", "Scope": "Scope:0", "Name": "My Story" }
}
]
The alias is then usable anywhere a reference is accepted:
| Position | Example |
|---|---|
| Relation value | { "AssetType": "Test", "Parent": "@story", "Name": "Test Time" } |
Query from | { "from": "@story", "select": ["Number"] } |
Query where | { "from": "Test", "where": { "Parent": "@story" } } |
Update / execute from | { "@inactivate": { "from": "@test1", "execute": "Inactivate" } } |
| Operation argument | { "Member": "@memberAlias1", "Role": "Role:3" } |
Server-generated values — a Story Number, a Task Number, a default
Status — are only knowable after creation. Alias the create, then query the
alias in the same request:
[
{ "@story": { "AssetType": "Story", "Scope": "Scope:0", "Name": "My Story" } },
{ "from": "@story", "select": ["Number"] }
]
In YAML, quote the alias key so the @ is not misread:
"@story":
AssetType: Story
Name: New Story
Scope: Scope:0
---
from: "@story"
select:
- Name
- Number
Request Hierarchy Flattening
A nested create request is projected into a flat list. Each descendant gains a
system-generated #ContextOid property pointing at its container's alias.
#ContextOid is about new-asset initialization: the server seeds the new
asset with the defaults it would receive if created from that context in the
application, then applies your attributes on top. You can also supply it
yourself on a flat create command — worth doing when the type has required
relations that must be defaulted from a container rather than spelled out.
{
"AssetType": "Story",
"Scope": "Scope:0",
"Name": "My Story with Children",
"Children": [
{ "AssetType": "Task", "Name": "Task 1" },
{ "AssetType": "Task", "Name": "Task 2" }
]
}
[
{
"@0f4bcf21-0f0f-4668-a7ee-a4db84f593bb$auto-aliased": {
"AssetType": "Story", "Scope": "Scope:0", "Name": "My Story with Children"
}
},
{
"@17afb273-11ae-4942-bf46-6fd14eda3472$auto-aliased": {
"AssetType": "Task", "Name": "Task 1",
"#ContextOid": "@0f4bcf21-0f0f-4668-a7ee-a4db84f593bb$auto-aliased"
}
},
{
"@9d182825-e5d4-4bbc-99d4-591e8133e087$auto-aliased": {
"AssetType": "Task", "Name": "Task 2",
"#ContextOid": "@0f4bcf21-0f0f-4668-a7ee-a4db84f593bb$auto-aliased"
}
}
]
Observations:
- The descendants keep their original relative order, immediately following their container.
- During processing,
#ContextOidis replaced with the container's real OID token, and becomes the context the contained asset is created in. assetsCreated.oidTokenscomes back in exactly this flattened order.
Hierarchical User-Aliases
User aliases can be supplied at any depth. The container may be auto-aliased while its children are user-aliased, or the reverse.
{
"AssetType": "Story",
"Scope": "Scope:0",
"Name": "My Story with Children",
"Children": [
{ "@task1": { "AssetType": "Task", "Name": "Task 1" } },
{ "@task2": { "AssetType": "Task", "Name": "Task 2" } }
]
}
[
{
"@8f49972e-e9ce-4465-8d6e-4dc91671d0fb$auto-aliased": {
"AssetType": "Story", "Scope": "Scope:0", "Name": "My Story with Children"
}
},
{
"@task1": {
"AssetType": "Task", "Name": "Task 1",
"#ContextOid": "@8f49972e-e9ce-4465-8d6e-4dc91671d0fb$auto-aliased"
}
},
{
"@task2": {
"AssetType": "Task", "Name": "Task 2",
"#ContextOid": "@8f49972e-e9ce-4465-8d6e-4dc91671d0fb$auto-aliased"
}
}
]
You can then query each Task by its alias. But note there is usually a simpler route — alias only the parent and use a nested select:
[
{
"@story": {
"AssetType": "Story", "Scope": "Scope:0", "Name": "My Story with Children",
"Children": [
{ "AssetType": "Task", "Name": "Task 1" },
{ "AssetType": "Task", "Name": "Task 2" }
]
}
},
{
"from": "@story",
"select": [{ "from": "Children:Task", "select": ["Number"] }]
}
]
Two aliased queries return two result sets; one nested query returns one result set containing both Tasks.
Deferring Commands Until the Processing Phase
Projection normally happens up front, which is a problem for a command whose target does not exist yet. User-aliasing an update or execute command defers its projection until the processing phase, by which time earlier aliases have resolved to real assets.
[
{
"@story": {
"AssetType": "Story", "Scope": "Scope:0", "Name": "Story Test",
"Children": [
{ "@test1": { "AssetType": "Test", "Name": "My Test 1 (checked off immediately)" } },
{ "AssetType": "Test", "Name": "My Test 2 (still undone)" }
]
}
},
{ "@inactivate": { "from": "@test1", "execute": "Inactivate" } },
{
"from": "Test",
"where": { "Parent": "@story" },
"select": ["Name", "AssetState"]
}
]
[
{
"@story": {
"AssetType": "Story", "Scope": "Scope:0", "Name": "Story Test",
"Children": [
{ "@test1": { "AssetType": "Test", "Name": "My Test 1 (checked off immediately)" } },
{ "AssetType": "Test", "Name": "My Test 2 (still undone)" }
]
}
},
{ "@inactivate": { "from": "@test1", "execute": "Inactivate" } },
{
"@147491a4-ce7f-4f1b-b238-626d6858d25b": {
"from": "Test", "where": { "Parent": "@story" },
"select": ["Name", "AssetState"]
}
}
]
Observations:
- The user-aliased Story is not flattened. Deferring it also defers the
flattening of everything inside it — which is why
@test1remains nested. - The only thing auto-aliased in this projection is the query.
{
"assetsCreated": { "oidTokens": ["Story:1582", "Test:1583", "Test:1584"], "count": 3 },
"assetsOperatedOn": { "oidTokens": ["Test:1583"], "count": 1 },
"queryResult": {
"results": [
[
{ "_oid": "Test:1583", "Name": "My Test 1 (checked off immediately)", "AssetState": "Closed" },
{ "_oid": "Test:1584", "Name": "My Test 2 (still undone)", "AssetState": "Active" }
]
],
"count": 1
}
}
Queries Do Not Need Deferral
Queries are always evaluated in place after the commands preceding them, so a user alias buys nothing. The following request contains the same query twice — once unaliased, once user-aliased — and produces two identical result sets:
[
{ "@story": { "AssetType": "Story", "Scope": "Scope:0", "Name": "My Story" } },
{ "from": "@story", "select": ["Name", "Number"] },
{ "@query": { "from": "@story", "select": ["Name", "Number"] } }
]
Do not reflexively wrap queries in aliases.
Give Every Aliased Command Its Own Object
Each aliased command must be its own element in the array. Aliases are not siblings inside one object, and getting this wrong fails in a way that depends on how many you have.
[
{ "@newEpic": { "AssetType": "Epic", "Name": "Parent", "Scope": "Scope:0" } },
{ "@epic0": { "AssetType": "Epic", "Name": "Child A", "Super": "@newEpic", "Scope": "Scope:0" } },
{ "@epic1": { "AssetType": "Epic", "Name": "Child B", "Super": "@newEpic", "Scope": "Scope:0" } }
]
[
{
"@newEpic": { "AssetType": "Epic", "Name": "Parent", "Scope": "Scope:0" },
"@epic0": { "AssetType": "Epic", "Name": "Child A", "Super": "@newEpic", "Scope": "Scope:0" },
"@epic1": { "AssetType": "Epic", "Name": "Child B", "Super": "@newEpic", "Scope": "Scope:0" }
}
]
A command object is only recognised as aliased when it has fewer than three children. That threshold produces three different behaviours from the same mistake:
| Aliases in one object | What happens |
|---|---|
| 1 | Correct. This is the intended shape. |
| 2 | Accepted, and both are created. It works — but only by falling under the threshold, so it is one added sibling away from failing. |
| 3 or more | 200 with a commandFailure — `Invalid key at node(s): root->@a |
The middle row is the dangerous one. Two siblings work — the request succeeds
and both assets are created — so the shape gets copied and scaled up. It fails
only when a third command is added, and the failure then reads like a query
syntax error rather than a payload-shape problem: with three or more children
the object is no longer recognised as aliased, so it is read as a query
command, and the query parser rejects the @ keys it finds at the root.
Nesting is usually the better answer anyway. If the children belong to the parent, express that with a relation rather than three aliased commands:
[
{
"AssetType": "Epic", "Name": "Parent", "Scope": "Scope:0",
"Subs": [
{ "AssetType": "Epic", "Name": "Child A", "Scope": "Scope:0" },
{ "AssetType": "Epic", "Name": "Child B", "Scope": "Scope:0" }
]
}
]
That avoids aliases entirely, and the hierarchy is flattened for you — see request hierarchy flattening.
What Aliases Cannot Do
Aliases are more capable than they first appear — a user alias can be referenced
from a query's from, a where clause, an update or execute from, a relation
value and an operation argument. That breadth makes the
gaps surprising when you hit them, because the natural next step often turns out
not to be supported.
An Operation's Output Cannot Be Referenced
This is the limitation people hit most. An alias names a command, and for a create command it resolves to the asset that command created. An operation that produces a new asset gives you nothing to name.
The canonical case is copying an asset and then amending the copy:
[
{ "from": "Story:455628", "execute": "Copy" },
{
"from": "<the Story the Copy just produced>",
"update": { "Name": "New Updated Story" }
}
]
There is no expression for the second from. Aliasing the execute command names
the command, not the Story it produced, so it does not help either.
Split it into two requests: run the Copy, read the new OID token from the
response, then send the update.
A Create Command Cannot Project Its Own Result
A create returns OID tokens in assetsCreated and nothing else. To see any
attribute of the asset you just made — a system-generated Number, a defaulted
Status — add a separate query command referencing the create's alias:
[
{ "@story": { "AssetType": "Story", "Scope": "Scope:0", "Name": "My Story" } },
{ "from": "@story", "select": ["Number", "Status.Name"] }
]
That works. What is not available is asking the create command itself to return a projection, which would save a round of commands.
A Subquery Cannot Supply a Single-Value Relation
Multi-value relations
accept a subquery in place of a literal list, in both add and remove. A
single-value relation does not — you cannot write a subquery as the value of
Scope, Parent or Status in a create or an update.
The practical workaround is a moniker, which resolves a relation by name rather than requiring you to know its OID token:
AssetType: Story
Name: My Story
Scope: Offerings\Agility 25.2\Optimize
That covers the common case of "the asset I mean is identified by its name". It does not cover choosing a single-value relation by an arbitrary filter.
No test asserts a negative, so treat these limitations as accurate descriptions of current behaviour rather than as guarantees about future releases.