Skip to main content

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.

PhaseWhat happens
ProjectionThe 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.
ProcessingEach 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.

Request
{ "AssetType": "Story", "Scope": "Scope:0", "Name": "My Story" }
Projection
[
{
"@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-aliased suffix 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.

Request
{
"@story": { "AssetType": "Story", "Scope": "Scope:0", "Name": "My Story" }
}
Projection
[
{
"@story": { "AssetType": "Story", "Scope": "Scope:0", "Name": "My Story" }
}
]

The alias is then usable anywhere a reference is accepted:

PositionExample
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" }
The most common reason to user-alias

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.

Request
{
"AssetType": "Story",
"Scope": "Scope:0",
"Name": "My Story with Children",
"Children": [
{ "AssetType": "Task", "Name": "Task 1" },
{ "AssetType": "Task", "Name": "Task 2" }
]
}
Projection
[
{
"@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, #ContextOid is replaced with the container's real OID token, and becomes the context the contained asset is created in.
  • assetsCreated.oidTokens comes 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.

Request
{
"AssetType": "Story",
"Scope": "Scope:0",
"Name": "My Story with Children",
"Children": [
{ "@task1": { "AssetType": "Task", "Name": "Task 1" } },
{ "@task2": { "AssetType": "Task", "Name": "Task 2" } }
]
}
Projection
[
{
"@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"]
}
]
Projection
[
{
"@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 @test1 remains nested.
  • The only thing auto-aliased in this projection is the query.
Processing response
{
"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.

Correct — one object per command
[
{ "@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" } }
]
Wrong — aliases as siblings in one object
[
{
"@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" }
}
]
The failure mode changes with the number of siblings

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 objectWhat happens
1Correct. This is the intended shape.
2Accepted, and both are created. It works — but only by falling under the threshold, so it is one added sibling away from failing.
3 or more200 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:

Does not work
[
{ "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.