---
id: queries
title: 'Queries'
description: 'Issue one or many independent queries in a Bulk API request and read the multi-result-set response.'
sidebar_label: 'Queries'
sidebar_position: 8
tags: ['api', 'bulk', 'asset', 'query']
---

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`](/apis/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 |

```json title="where — equality only"
{ "from": "Story", "where": { "Name": "Feature Story 1" } }
```

```json title="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:

```json
{ "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:

```json title="where form — boolean"
{ "from": "Story", "where": { "Name": "My Story", "IsDeleted": true } }
```

```json title="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](/apis/asset-bulk/updating-assets#updating-a-deleted-asset)
and [Undelete](/apis/asset-bulk/operations#undelete), both of which need
`IsDeleted` for the same reason.

## Paging and Totals

A query command accepts `page` and `needTotal`:

```json
{
  "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](/apis/rest-1) 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

```json
{
  "from": "Epic:1589",
  "select": [
    "Name",
    {
      "from": "Subs",
      "select": [
        "Name",
        {
          "from": "Subs",
          "select": ["Name", { "from": "Children", "select": ["Name"] }]
        }
      ]
    }
  ]
}
```

```json title="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

| 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 `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.

```yaml
from: Story:1585
---
from: Story:1586
---
from: Story:1587
```

```json title="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:

```json
[
  { "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.

```json title="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.

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


An alias also works inside a `where` clause:

```json
{ "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:

```json
[
  {
    "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"] }
]
```

```json title="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:

| 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](/apis/asset-bulk/errors).

## Related Topics

- [Query API (query.v1)](/apis/query-v1)
- [Request and response format](/apis/asset-bulk/request-format)
- [Projection, processing and aliases](/apis/asset-bulk/aliases-and-projection)
