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.
| Clause | Form | Use it for |
|---|---|---|
where | An object of attribute/value pairs | Simple 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 |
filter | An array of filter-token strings | Operators, OR-lists, and anything where cannot express |
{ "from": "Story", "where": { "Name": "Feature Story 1" } }
{ "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:
{ "from": "Story", "where": { "Name": "My Story", "IsDeleted": true } }
{ "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.
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.
total is null on a paged query unless you ask for itComputing 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"] }]
}
]
}
]
}
{
"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
| Selection | Returned 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 / select | an array, keyed by the from value |
Two consequences worth noting:
- Every row always carries
_oid, even when you did not select it. - A nested
fromon 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
{
"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.
{
"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"] }
]
[
[{ "_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:
| Mistake | error.message |
|---|---|
Unknown asset type in from | Unknown AssetType: Animal |
Unknown attribute in select | Unknown AttributeDefinition: Scope.MuddleBubble |
Unknown attribute in where | Invalid 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.