---
id: query-grammar
slug: /reference/query-grammar
title: 'Query Grammar'
description: 'Normative reference for the attribute-path and filter syntax shared by rest-1.v1, query.v1 and api/asset: attribute paths, downcasts, aggregations, filter expressions, filter context, sort and paging.'
sidebar_label: 'Overview'
sidebar_position: 1
tags: ['reference', 'query-grammar', 'rest-1', 'query-v1', 'asset-bulk', 'filtering']
---

Agility has a compact, purpose-built syntax for choosing which attributes come
back and for narrowing a result set. It is more expressive than it first looks:
it supports attribute-path traversal across relations, downcasting to subtypes,
aggregation rollups, filters applied part-way along a path, existence
assertions, reusable variables and self-reference. Together these let a single
expression walk the asset graph rather than requiring several round trips.

**This syntax is not specific to one endpoint.** The same attribute paths and
filter tokens turn up across the API:

| Surface | Where the syntax appears |
| --- | --- |
| [`rest-1.v1`](/apis/rest-1) | `sel`, `where`, `sort` and the rest, on the query string |
| [`query.v1`](/apis/query-v1) | `select`, `filter`, `sort` and the rest, inside a JSON or YAML body |
| [`api/asset`](/apis/asset-bulk) | `select`, `where`, `filter` and `sort` on each command in a bulk request |
| [`api/webhook`](/apis/webhooks/event-types) | `select`, `filter` and `with` on each event-type subscription |

The fourth is easy to miss. A **webhook subscription is not a query**, but each
event type it declares uses the same vocabulary to decide which assets it fires
for and what to project into the delivered payload — so everything on these pages
about attribute paths, filter expressions and filter context applies when you are
writing a subscription. See [event types](/apis/webhooks/event-types).

What differs between them is the **envelope** — query string versus request body
— and the spelling of a few keywords. Webhooks take a subset: they filter and
project, but there is nothing to sort or page. The vocabulary itself is common, so an
attribute path such as `Scope.Workitems:Story[ToDo='0'].Estimate.@Sum` means
the same thing wherever it appears. Learn it once.

This section is the normative, human-readable description of that syntax. See
[which surface uses what](#which-surface-uses-what) for the keyword differences.


## The Pages

| Page                                                          | Covers                                                                                |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| [Attribute paths](/reference/query-grammar/attribute-paths)   | Dot notation, downcast `:Type`, aggregations `.@Sum`, now-only `#`, inline `[filter]`  |
| [Filter expressions](/reference/query-grammar/filter-expressions) | Comparison operators including `~=`, `and`/`or`/grouping, value lists, unary existence, variables |
| [Filter context](/reference/query-grammar/filter-context)     | The `with` parameter and variable assignment                                          |
| [Sort and paging](/reference/query-grammar/sort-and-paging)   | `+`/`-` ordering, `page=size,start`, and reading the paging response attributes        |
| [Grammar reference](/reference/query-grammar/ebnf)            | EBNF rendering of all three grammars, plus the raw `.g` files to download              |

## Parameter Map

Every parameter below is read from the query string of a `rest-1.v1`
request.

| Parameter   | Purpose                                       | Grammar                                                              | Notes                                                                    |
| ----------- | --------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `sel`       | Attribute selection                           | [Attribute paths](/reference/query-grammar/attribute-paths)          | `select` is an undocumented alias that **takes precedence** over `sel`    |
| `select`    | Alias for `sel`                               | as `sel`                                                              | Not mentioned by the grammar at all                                       |
| `where`     | Filter expression                             | [Filter expressions](/reference/query-grammar/filter-expressions)    | **This is the filter parameter. There is no `filter=` on `rest-1.v1`.**   |
| `with`      | Filter-context variable assignment            | [Filter context](/reference/query-grammar/filter-context)            | `$VAR=a,b\|$VAR2=c` — values are _not_ quoted here                        |
| `sort`      | Ordering                                      | [Sort and paging](/reference/query-grammar/sort-and-paging)          | `+` ascending (the default), `-` descending, comma-separated              |
| `page`      | Paging                                        | [Sort and paging](/reference/query-grammar/sort-and-paging)          | `page=<size>,<start>` — **size comes first**                              |
| `find`      | Text search                                   | none                                                                  | Absent from the grammar entirely                                          |
| `findin`    | Attributes that `find` searches               | none                                                                  | Absent from the grammar. Optional; defaults to Name + ShortName           |
| `asof`      | History moment                                | none                                                                  | Absent from the grammar. Accepts an **integer moment** _or_ a datetime    |
| `deleted`   | Include deleted assets                        | none                                                                  | Boolean flag. Absent from the grammar files. Defaults to off on **every** instance |
| `needTotal` | Compute the `total` attribute on the response | none                                                                  | Boolean flag. Without it a **paged** response omits `total` entirely (unpaged responses always carry a real one). Its default is **per-instance** |

The two flags look alike but their defaults do not behave alike, which is worth
knowing before you rely on either:

| Flag        | Default when omitted                                       | Varies by instance? |
| ----------- | ---------------------------------------------------------- | ------------------- |
| `deleted`   | Off — hardcoded                                              | No                  |
| `needTotal` | falls back to the `ApiDefaultNeedTotal` server setting        | **Yes**             |

So `deleted` behaves identically everywhere, and `needTotal` does not.

:::warning A paged response may have no `total` at all
The default for `needTotal` comes from the instance-configurable
`ApiDefaultNeedTotal` setting, so a client that pages off `total` can work
against one instance and break against another. Send `needTotal=true`
explicitly, and treat a missing `total` as "not computed", not as zero. See
[Sort and paging](/reference/query-grammar/sort-and-paging#reading-the-response).
:::

### `asof` Accepts Two Different Things

`asof` accepts either an **integer moment number** or a **datetime**. The
datetime is parsed with invariant culture, so use ISO 8601 rather than a
localized format. Both of these are valid, and they mean different things:

```text
?asof=113                        # moment number 113
?asof=2005-08-03T17:30:00.00     # the state as of that instant
```

A datetime `asof` returns the last historical record **on or before** the
instant given. A value that parses as neither
raises `400 Bad Request`.

## Which Parameters Apply to Which Request Shape

This is not stated by the grammar, and it is a common source of "my filter was
ignored" reports. The three request shapes build different queries:

| Request shape                          | `sel` | `where` | `with` | `sort` | `page` | `find` | `asof` | `needTotal` |
| -------------------------------------- | ----- | ------- | ------ | ------ | ------ | ------ | ------ | ----------- |
| `/Data/{Type}` (a list)                | yes   | yes     | yes    | yes    | yes    | yes    | yes    | yes         |
| `/Data/{Type}/{id}` (a single asset)    | yes   | **no**  | yes    | **no** | **no** | **no** | yes    | yes         |
| `/Hist/{Type}[/{id}]` (history)         | yes   | yes     | yes    | yes    | yes    | yes    | yes    | yes         |

:::danger On a single-asset URL, some parameters work and others are silently dropped
Read the middle row carefully. `/Data/Story/1364` is **not** a URL where "the
list parameters don't apply" — it is a URL where `deleted`, `needTotal`, `asof`,
`sel` and `with` all take effect, while `where`, `sort`, `page` and `find` are
discarded without comment. Same URL, same query string, two different fates, no
error either way.

The single-asset query is built from the OID with only the deleted flag, the
total flag and a moment slice. No filter is ever attached, and nothing anywhere
reports that one was supplied and ignored. `?where=` on this URL is not an
error, not a warning, and not applied — it is simply as though you never sent
it.
:::

Two further consequences worth knowing:

- The grammar agrees about filtering — "only valid when asking for a list of
  assets" — but says nothing about the
  parameters that _do_ survive.
- On a history request with no `sort`, the server adds an implicit ascending
  sort on `Moment`. Supplying your own
  `sort` replaces it, so a history response is not guaranteed chronological once
  you sort by something else.

If the URL addresses a single attribute — `/Data/Member/20/Name` — the selection
is that attribute and `sel` is not consulted at all.

:::danger The `#` operator must be percent-encoded
The now-only operator in an attribute path is `#`, and it **must** be sent as
`%23`. Unencoded, it is the URI fragment delimiter: your client truncates the URL
there and never puts the rest on the wire, so the server answers a query you did
not write. Depending on where the truncation lands you get either a `200` with a
silently wrong result set or a `400`. The fault is client-side, so debugging the
server will not explain it. See
[Attribute paths](/reference/query-grammar/attribute-paths#now-only).
:::

## Selection Defaults, and Why You Get Attributes You Did Not Ask For

Two selection defaults explain most surprises about response size.

**Omitting `sel` does not return everything.** It returns
`assettype.GetBasicAttributeDefinitions()` — a per-asset-type basic set. Never
assume an unfiltered request is exhaustive; if you need a specific attribute,
name it.

**Selecting a relation implicitly selects more.** After your selection is
parsed, every attribute whose type is `Relation` additionally selects, on the
related asset:

- its designated **name** attribute;
- its designated **short-name** attribute, where the type declares one — and
  the designation names a real attribute, so it surfaces as `Owners.Nickname`
  or `Workitems.Number`, never as a literal `ShortName`;
- the relation's **DisplayBy** attribute, where the relation declares one.

Each of the three is conditional and they frequently coincide — `DisplayBy` and
the short-name designation are usually the name attribute again — so the common
case is exactly one extra attribute: `?sel=Status` really returns `Status` plus
`Status.Name`. This is why responses contain attributes you never
requested, and it is the first thing to look at when tuning payload size — the
expansion is per relation attribute selected, so a selection with many relations
grows faster than it looks.

Combined with the absence of a default page size, these two defaults are the main
reason Agility integrations pull more data than intended. See
[Query size and performance](/guides/query-size-and-performance).

## Grammar Versus Implementation

The published grammar files were last revised in 2012 and describe the intended
syntax rather than the shipped implementation. **Where the grammar and the
server disagree, the server wins.** These are the known disagreements:

| # | The grammar says…                                                     | The server actually…                                                                                          |
| - | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| 1 | The filter root rule is `filter2_token`                               | Reads the query parameter **`where`**. `Filter2` is an internal type name that never appears on the wire; there is **no `filter=` parameter** on `rest-1.v1` |
| 2 | Selection comes from `sel`                                            | Also accepts `select`, an alias that **wins** when both are present                                            |
| 3 | Nothing about `find`, `findin`, `asof`, `deleted` or `needTotal`      | Supports all five                                                                                              |
| 4 | Selection is exactly what you asked for                               | Expands every relation selection with up to three conditional attributes — commonly just Name                  |
| 5 | Nothing about the default selection                                   | Falls back to a per-type basic set, not to all attributes                                                      |
| 6 | `paging_token : page_size (',' page_start)?`                          | Agrees — size first, then start                                                                                |
| 7 | `sort_token` has no default                                           | Adds an implicit ascending `Moment` sort on history requests when `sort` is absent                             |
| 8 | Filtering, sorting and paging are properties of a query               | Are not applied at all on single-asset `/Data/{Type}/{id}` requests, while `deleted` and `needTotal` are |

Item 6 is listed because it is frequently gotten backwards, not because it is a
disagreement — all three sources agree that size comes first.

## Which Surface Uses What \{#which-surface-uses-what\}

These three surfaces **share the attribute-path and filter-expression vocabulary**
and differ in envelope and keyword spelling. Webhook subscriptions are left out of
the table because they are not a query surface — they support `select`, `filter`
and `with` but have no `from`-and-return semantics, no sort and no paging. See
[event types](/apis/webhooks/event-types) for the fields they do accept.

| Aspect                      | `rest-1.v1`                                     | `query.v1`                                                                     | `api/asset` |
| --------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ | --- |
| Envelope                    | GET, parameters on the query string             | POST, a YAML document in the body (JSON is valid YAML)                          | POST, a JSON command or array of commands |
| Root asset type             | The URL path — `/Data/Story`                    | `from: Story` (an `as` alias parses but is silently discarded)                  | `from` — an asset type or an OID token |
| Selection                   | `sel` or `select`, one comma-separated token    | `select` or `sel`, a list; nested mappings are subqueries (an entry's `as` alias parses but is silently discarded) | `select`, a list |
| Filter expression           | `where=<expression>`                            | **`filter:`** — a scalar or list of expressions in the same language            | **`filter`** — an array of expressions, as on `query.v1` |
| Equality shorthand          | not available                                   | **`where:`** — a mapping of attribute to a single scalar value                  | **`where`** — an object, as on `query.v1` |
| Filter context / variables  | `with=$V=a,b\|$W=c`                             | `with:` — a mapping of name to a scalar or a list                               | uses [aliases](/apis/asset-bulk/aliases-and-projection) instead |
| Sort                        | `sort=-Estimate,+Name`                          | `sort: ['-Estimate', '+Name']`                                                  | `sort`, a list |
| Paging                      | `page=<size>,<start>` (positional)              | `page: { size: 50, start: 0 }` (named; **both keys required**)                   | `page: { start, size }` (named; `start` defaults to 0) |
| Text search                 | `find`, `findin`                                | `find`, `findin`                                                                | `find` is command-dependent — see below |
| History                     | `asof`, plus the separate `/Hist` namespace     | `asof`                                                                          | not applicable |
| Deleted assets              | `deleted=true`                                  | `deleted: true`                                                                 | `IsDeleted` per command — the `?deleted=1` URL flag does not surface deleted rows on its own |
| Total count                 | `needTotal=true`                                | `needTotal: true`                                                               | `needTotal: true` |
| Grouping                    | not available                                   | `group:` — a list of expressions, and grouping subqueries                        | not available |
| Several queries per request | no                                              | yes — the document root may be a sequence of query mappings                      | yes — the body may be an array of commands |
| Writes                      | `op` for operations; POST a payload to update   | not available — `update` and `execute` are stripped                              | `update` and `execute` on any command |

:::warning `find` does not always mean text search

On `rest-1.v1` and `query.v1`, `find` searches text attributes and pairs with
`findin`.

On `api/asset` its meaning depends on the command it appears in. In an **update**
command, `find` is half of the
[find-and-replace](/apis/asset-bulk/find-and-replace) form — it names the
substring to be replaced and pairs with `replace`, not with `findin`.

In an `api/asset` **query** command, `find` is a text search, as on
`query.v1`: it pairs with `findin`.

The practical caution: a query carrying `find` that is ported into a Bulk API
update command will be read as a replacement instruction, not a search.

:::

:::warning `where` does not mean the same thing in both APIs
On `rest-1.v1`, `where` is the full filter language. On `query.v1`, `where` is a
plain equality mapping and the full filter language lives under `filter`. So
`?where=ToDo='0'` and `filter: ["ToDo='0'"]` are equivalent — but
`where: { ToDo: '0' }` on `query.v1` is the shorthand, not the expression.
Conflating the two is the single most common mistake when porting a query from
one surface to the other.
:::

Two further `query.v1` details worth knowing:

- The keys `update` and `execute` are stripped from the document before
  validation, so including them is neither honoured nor rejected.
- Any other unrecognised key is rejected with `Invalid key at node(s): …` when
  key validation is enabled.

## Errors

A malformed value for any of these parameters produces `400 Bad Request` with an
`Error` payload whose `Message` names the parameter — for example
`Invalid SEL parameter`, with a nested `Exception` element carrying the detail
such as `Unknown AttributeDefinition: Story.Bogus`. The full list is in
[HTTP status codes](/reference/status-codes#query-parameter-errors).

## Machine-Readable Grammar

The three ANTLR grammar files are published verbatim, license headers intact, at
stable URLs — fetch them if you are building a parser, linter or query builder:

- [`V1Query.g`](pathname:///grammars/V1Query.g) — selection, attribute paths, filter expressions, sort
- [`V1FilterContext.g`](pathname:///grammars/V1FilterContext.g) — the `with` parameter
- [`V1Paging.g`](pathname:///grammars/V1Paging.g) — the `page` parameter

They are transcribed as EBNF on the
[grammar reference](/reference/query-grammar/ebnf) page. Read them as a syntax
map, not as an oracle — see the divergences above.

## Related Topics

- [REST API (`rest-1.v1`)](/apis/rest-1)
- [Query API (`query.v1`)](/apis/query-v1)
- [Assets and attributes](/concepts/attributes)
