Skip to main content

Updating Multi-Value Relations

Multi-value relations — Owners, Subs, Children, Workitems, RegressionSuites — accept a family of syntaxes that all normalise to the same projected form:

{
"update": {
"Owners": { "add": [ /* ... */ ], "remove": [ /* ... */ ] }
}
}

Everything below is a shorthand for filling those two arrays. Pick whichever form reads best for the job.

Adding​

One Reference​

All five of these are equivalent.

Implicit single-add with scalar
{ "from": "Task:1489", "update": { "Owners": "Member:20" } }
Implicit single-add with array
{ "from": "Task:1489", "update": { "Owners": ["Member:20"] } }
Explicit single-add with object scalar
{ "from": "Task:1489", "update": { "Owners": { "add": "Member:20" } } }
Explicit single-add with object array
{ "from": "Task:1489", "update": { "Owners": { "add": ["Member:20"] } } }
Explicit single-add with object keys
{ "from": "Task:1489", "update": { "Owners": { "Member:20": "add" } } }

Each projects to:

[
{
"@bd9598ad-66af-4cec-b03e-bf273d91bfb5": {
"oid": "Task:1489",
"update": { "Owners": { "add": ["Member:20"], "remove": [] } }
}
}
]

Several References​

Implicit multi-add with array
{ "from": "Task:1489", "update": { "Owners": ["Member:20", "Member:1575"] } }
Explicit multi-add with object array
{ "from": "Task:1489", "update": { "Owners": { "add": ["Member:20", "Member:1575"] } } }
Explicit multi-add with object keys
{
"from": "Task:1489",
"update": { "Owners": { "Member:20": "add", "Member:1575": "add" } }
}

Removing​

Swap add for remove. The object-scalar and object-keys forms are the most convenient:

Explicit single-remove with object scalar
{ "from": "Task:1489", "update": { "Owners": { "remove": "Member:20" } } }
Explicit single-remove with object keys
{ "from": "Task:1489", "update": { "Owners": { "Member:20": "remove" } } }

Object keys scale to several removals in one command:

{
"from": "Epic:1600",
"update": {
"Name": "I have zero Subs",
"Subs": { "Story:1601": "remove", "Story:1602": "remove" }
}
}

Adding and Removing in One Command​

Object context pointing to scalars
from: Story:1700
update:
Owners:
remove: Member:20
add: Member:1800
Object context pointing to arrays
from: Story:1700
update:
Owners:
remove:
- Member:20
add:
- Member:1800
Object keys
{
"from": "Story:1700",
"update": {
"Owners": { "Member:20": "remove", "Member:1800": "add" }
}
}

All three project to:

[
{
"@ca414bbc-8dc5-41ef-9626-244b80145059": {
"oid": "Story:1700",
"update": {
"Owners": { "add": ["Member:1800"], "remove": ["Member:20"] }
}
}
}
]
tip

remove is applied together with add, so it is safe to reference the same attribute in both — the result is the set difference plus the additions, not an ordering-sensitive sequence of edits.

Referencing by Subquery​

add and remove also accept a subquery in place of a literal list. The subquery runs during processing and every match participates.

Add every Story matching a filter
from: Epic:1900
update:
Subs:
add:
from: Story
filter:
- ID='Story:1901','Story:1902','Story:1903'
Remove every Story currently under this Epic
from: Epic:1900
update:
Subs:
remove:
from: Story
filter:
- Super='Epic:1900'
Subquery cardinality differs by position

In a relation update, every asset the subquery matches is used. In an operation argument, only the first match is used. Do not assume one behavior from the other.

Mixing Existing References with New Assets​

An add list may contain OID tokens and inline create commands side by side. The new assets are created and related in the same request.

{
"from": "Epic:1850",
"update": {
"Name": "I should now be related to Story:1840 and another new Story via the Subs relation",
"Subs": [
"Story:1840",
{ "AssetType": "Story", "Name": "I am a brand new Story" }
]
}
}

The response reports the Epic in assetsModified and the new Story in assetsCreated.

The inline create is projected once per matched asset. Under a query-scoped update that means one new asset for every asset the from/filter selects, each related to its own parent — the shape to reach for when the same child belongs on many parents.

Adding Only Where It Is Missing​

Because the filter runs before the creates are projected, a filter that excludes the parents already holding an equivalent child makes the whole command idempotent: run it twice and the second run creates nothing. This example puts a Link to a pull request on every listed asset that does not already carry that URL:

{
"from": "BaseAsset",
"filter": ["ID=$oids", "-Links[URL=$url]"],
"with": {
"$oids": ["Story:1227", "Defect:1310"],
"$url": "https://github.com/example/repo/pull/12"
},
"update": {
"Links": [
{
"AssetType": "Link",
"Name": "GitHub PR",
"URL": "https://github.com/example/repo/pull/12",
"OnMenu": true
}
]
}
}

assetsModified names the parents that received a Link; a parent the filter left out is absent from it and from assetsCreated. The check and the write happen inside one command, so there is no window between them for a second client to slip into — the closest thing the Bulk API offers to an idempotency key.

Provisos for inline creates
  • Use the array or add form. A bare mapping directly under the relation — "Links": { "AssetType": "Link", ... } — is half-handled: the child is created and related, and the command is still reported in commandFailures with a StructureMap error naming Relation. Only the array under the relation, or add: with a list, is the supported syntax.
  • Leave the update command unaliased. In an array of commands, an alias on an update that carries an inline create makes every projected create fail with The given key was not present in the dictionary; the same command with no alias, in the same batch, succeeds. Aliases on the other commands in the batch are fine.
  • One level only. A create nested inside an inline create is projected after the child that needs it, so a required parent — an Expression's Conversation, say — is not there yet and the child fails validation. An inline create also cannot reference an alias defined elsewhere in the same request. Create the dependency in its own command first and use its OID.
  • Match exactly in the guard filter. -Links[URL=$url] compares the stored value; ~= is full-text search and its index catches up with writes asynchronously, so a guard written with ~= still sees a child created seconds ago as missing and creates it again.

Replacing One Reference with Another​

To swap a reference only where it already exists, use find and replace rather than a paired add/remove. add/remove applies unconditionally to every matched asset; find/replace only touches assets that already hold the find value.