Skip to main content

Queries

A query command is a command object with a from clause and no update, execute, or AssetType. Query results are returned in queryResult, separately from the create/update/operate counters.

Query syntax is the same SQL-like grammar as query.v1: from, select, where, filter, sort, page, asof.

where And filter​

There are two ways to narrow a query, and they are not interchangeable.

ClauseFormUse it for
whereAn object of attribute/value pairsSimple equality on one or more attributes — operators are not accepted here; use filter. Both clauses on one command combine as AND, but prefer one style per query
filterAn array of filter-token stringsOperators, OR-lists, and anything where cannot express
where — equality only
{ "from": "Story", "where": { "Name": "Feature Story 1" } }
filter — tokens
{ "from": "Workitem", "filter": ["Owners.@Count>'0'"] }

A filter token is Attribute + operator + 'value'. Repeated tokens are ANDed; comma-separated values inside one token are ORed:

{ "from": "TeamRoom", "filter": ["Name='Team A'", "IsDeleted='True','False'"] }

Querying Deleted Assets​

Deleted assets are excluded automatically. The switch that includes them is IsDeleted per command — set it on each command that should see deleted assets. The ?deleted=1 URL parameter does not surface deleted rows on its own: a query command without an IsDeleted clause returns no deleted assets whether or not the flag is present.

IsDeleted On the Command​

Say so per command, in whichever clause style you are using:

where form — boolean
{ "from": "Story", "where": { "Name": "My Story", "IsDeleted": true } }
filter form — string token
{ "from": "TeamRoom", "filter": ["Name='Team A'", "IsDeleted='True'"] }

The OR-form IsDeleted='True','False' returns deleted and active assets in one result set. This is the conventional idiom for integrations that need a complete picture, and Core's own test suite refers to it as the standard filter used by query.v1 and api/asset integrations.

warning
IsDeleted selects back as a string

"select": ["IsDeleted"] returns "True" or "False" — the strings, not JSON booleans. The boolean true is only accepted on the input side, in a where object.

See also Updating deleted assets and Undelete, both of which need IsDeleted for the same reason.

Paging and Totals​

A query command accepts page and needTotal:

{
"from": "StoryStatus",
"needTotal": true,
"page": { "start": 0, "size": 5 }
}

page is an object with start and size — unlike rest-1.v1, which takes a positional page=size,start query-string pair. See the REST-1 API for that form.

warning
total is null on a paged query unless you ask for it

Computing the total of a paged query is a second database round trip, so it is opt-in. Without needTotal: true, a paged query's resultMetaData[n].total is null — meaning "not computed", not a count of zero. An unpaged query's total is always a real count, because the server counts what it returns.

Unlike rest-1.v1, this surface does not consult the ApiDefaultNeedTotal instance setting: without needTotal in the command, the count is simply not computed. Set needTotal: true whenever you depend on total on a paged query.

resultMetaData itself is newer than the API: versions before 24.3 do not return it at all.

A Single Query​

{
"from": "Epic:1589",
"select": [
"Name",
{
"from": "Subs",
"select": [
"Name",
{
"from": "Subs",
"select": ["Name", { "from": "Children", "select": ["Name"] }]
}
]
}
]
}
Response (abridged)
{
"queryResult": {
"results": [
[
{
"_oid": "Epic:1589",
"Name": "Epic with Subs",
"Subs": [
{
"_oid": "Epic:1590",
"Name": "Feature Epic",
"Subs": [
{
"_oid": "Story:1591",
"Name": "Feature Story 1",
"Children": [
{ "_oid": "Task:1596", "Name": "Task Time" },
{ "_oid": "Test:1598", "Name": "Test Time" }
]
}
]
}
]
}
]
],
"count": 1
}
}

queryResult.count is 1 because the request contained one query — not because one Epic matched.

Result Shapes​

SelectionReturned as
No select at all{ "_oid": "Story:1585" } — the OID token only
A scalar attribute"Name": "First Story"
A single-value relation"Parent": { "_oid": "Story:1591" }
A multi-value relation"Owners": [{ "_oid": "Member:20" }]
A dotted path"Scope.Name": "System (All Projects)"
A nested from / selectan array, keyed by the from value

Two consequences worth noting:

  • Every row always carries _oid, even when you did not select it.
  • A nested from on a single-value relation still returns an array of one. { "from": "RegressionSuite", "select": ["RegressionPlan"] } yields "RegressionSuite": [ { ... } ], whereas selecting "RegressionSuite" as a plain attribute would yield { "_oid": ... }.

Several Independent Queries​

Multiple queries in one request produce multiple result sets, in request order.

from: Story:1585
---
from: Story:1586
---
from: Story:1587
Response
{
"queryResult": {
"resultMetaData": [
{ "page": { "start": 0, "size": 2147483647 }, "total": 1 },
{ "page": { "start": 0, "size": 2147483647 }, "total": 1 },
{ "page": { "start": 0, "size": 2147483647 }, "total": 1 }
],
"results": [
[{ "_oid": "Story:1585" }],
[{ "_oid": "Story:1586" }],
[{ "_oid": "Story:1587" }]
],
"count": 3
}
}

Queries are genuinely independent — they need not share an asset type, a scope, or anything else:

[
{ "from": "Workitem", "filter": ["Owners.@Count>'0'"], "select": ["Owners"] },
{
"from": "Member:20",
"select": ["Name", { "from": "OwnedWorkitems", "select": ["Name"] }]
}
]

Empty Result Sets Are Preserved​

A query that matches nothing still occupies its slot, as an empty array with total: 0 in resultMetaData. Positional correlation between your queries and results is therefore always safe.

Two queries, neither matching
{
"queryResult": {
"resultMetaData": [
{ "page": { "start": 0, "size": 2147483647 }, "total": 0 },
{ "page": { "start": 0, "size": 2147483647 }, "total": 0 }
],
"results": [[], []],
"count": 2
}
}

Querying an Alias​

A query can use an alias defined earlier in the same request as its from target, which is how you retrieve server-generated values — a Number, a default Status — for assets you just created.

[
{ "@story": { "AssetType": "Story", "Name": "New Story", "Scope": "Scope:0" } },
{ "from": "@story", "select": ["Name", "Number"] }
]

An alias also works inside a where clause:

{ "from": "Test", "where": { "Parent": "@story" }, "select": ["Name", "AssetState"] }

Query Ordering Within a Request​

Queries are evaluated in place, in request order, relative to the other commands. A query positioned before a deferred update or operation observes the pre-change state; the same query positioned after it observes the post-change state. Both can appear in the same request:

[
{
"AssetType": "Story", "Name": "New Story", "Scope": "Scope:0",
"Children": [{ "@task": { "AssetType": "Task", "Name": "Do it now" } }]
},
{ "from": "@task", "select": ["Name", "Parent"] },
{ "@update": { "from": "@task", "update": { "Name": "It is done!" } } },
{ "from": "@task", "select": ["Name", "Parent"] }
]
Response queryResult.results
[
[{ "_oid": "Task:1490", "Name": "Do it now", "Parent": { "_oid": "Story:1489" } }],
[{ "_oid": "Task:1490", "Name": "It is done!", "Parent": { "_oid": "Story:1489" } }]
]

Query Errors​

Query mistakes surface as command failures, not HTTP errors:

Mistakeerror.message
Unknown asset type in fromUnknown AssetType: Animal
Unknown attribute in selectUnknown AttributeDefinition: Scope.MuddleBubble
Unknown attribute in whereInvalid QueryFilter2 token MuddleBubble='BubbleMuddle'

Note that the where failure reports a parser-level message rather than the attribute-definition message the select failure gives, even though the underlying mistake is the same. See Errors and partial failures.