---
id: attribute-paths
slug: /reference/query-grammar/attribute-paths
title: 'Attribute Paths'
description: 'Dot notation, downcasting to a subtype, aggregation rollups, the now-only operator, and filters applied part-way along an attribute path.'
sidebar_label: 'Attribute Paths'
sidebar_position: 2
tags: ['reference', 'query-grammar', 'attributes', 'relations', 'aggregation']
---

An **attribute path** names a value relative to an asset. It is the single most
reused construct in the query syntax: the same production appears in `sel`, in
`sort`, on the left-hand side of a term in `where`, and as the last segment of a
`rest-1.v1` URL.

```text
attribute_name       = attribute_name_part , { "." , attribute_name_part }
                     , [ ".@" , aggregation_name ] ;

attribute_name_part  = NAME , [ "#" ] , [ ":" , asset_type ] , [ "[" , filter_expression , "]" ] ;
```


Read that second line carefully — the four pieces of a path part are ordered.
The now-only marker comes before the downcast, and the downcast comes before the
inline filter.

## Dot Notation

Attributes are either **scalars** or **relations** to other assets, and
relations are either single-value or multi-value. When an attribute is a relation, `.` continues the path onto the
related asset.

```text
Name                       # a scalar on this asset
Category.Name              # the Name of the related Category
Scope.Name                 # the Name of the owning project
Workitems.Name             # every Name across a multi-value relation
```

A path may be used as the final segment of a URL as well as inside `sel`:

```text
GET /rest-1.v1/Data/Story/1055/Category.Name
GET /rest-1.v1/Data/Scope/0/Workitems.Name
```


There is no depth limit in the grammar. Each traversal is a join on the server,
so long paths are correct but not free; see the note on cost at the end of this
page.

:::info Selecting a relation selects more than you asked for
`?sel=Status` also returns `Status.Name`, because the server expands every
relation in your selection with up to three conditional attributes — the
related type's designated name and short-name attributes and the relation's
`DisplayBy`, which usually coincide. This is
[documented in the overview](/reference/query-grammar#selection-defaults-and-why-you-get-attributes-you-did-not-ask-for).
:::

## Downcast

Agility's asset types form a hierarchy: `Workitem` is abstract, and `Story`,
`Defect`, `Task` and `Test` are among its concrete subtypes. **An abstract type
only exposes the attributes defined at its own level.** `Workitem` has no
`Category` attribute; `Story` does.

The downcast operator `:` narrows a path part to a subtype so the subtype's
attributes become reachable:

```text
Workitems:Story.Category
```

```text
GET /rest-1.v1/Data/Scope/0/Workitems:Story.Category
```


:::warning Without the downcast, the request fails
Reaching for a subtype attribute through an abstract relation is not a silent
no-op — it is an error. `?sel=Workitems.Category` returns `400 Bad Request` with
`Invalid SEL parameter` and an inner `Unknown AttributeDefinition` message. This
is the top-tier gotcha when traversing `Workitem`, `BaseAsset` or `Scope`
relations, because those relations are typed at the abstract level far more
often than people expect.
:::

The downcast also restricts: `Workitems:Story` addresses the `Story` members of
the relation, so `Workitems:Story.Category` yields one value per Story and
nothing for the Defects and Tasks in the same relation. (Inferred from the
purpose of the operator as described in `V1Query.g`; the grammar does not state
the set semantics explicitly and no shipped example demonstrates it, so treat
the restriction as unverified.)

## Aggregations

An aggregation rolls a set of values up to a single value. It is written `.@`
followed by the aggregation name, and it may only appear once, at the very end
of a path.

| Aggregation     | Meaning                                                   |
| --------------- | --------------------------------------------------------- |
| `Sum`           | Sum the returned values                                   |
| `Any`           | True if the set is non-empty (a count-greater-than-zero test on a relation) |
| `Count`         | Count the returned assets                                 |
| `DistinctCount` | Count the returned assets, counting each asset only once  |
| `MinDate`       | The oldest date among the returned values                 |
| `MaxDate`       | The newest date among the returned values                 |
| `And`           | True if all the returned values are true                  |
| `Or`            | True if any of the returned values is true                |
| `MaxState`      | The highest state value in the set                        |


```text
GET /rest-1.v1/Data/Scope/0/Workitems.@Count
GET /rest-1.v1/Data/Scope/0/Workitems:PrimaryWorkitem.Estimate.@Sum
```

:::warning Aggregating through an abstract relation needs a downcast
`Workitems.Estimate.@Sum` is a `400` — `Unknown AttributeDefinition:
Workitem.Estimate`. The `Workitems` relation targets the abstract `Workitem`
type, and `Estimate` is defined on its subtypes (`PrimaryWorkitem`, `Task`,
`Test`), not on `Workitem` itself. [Downcast](#downcast) to the subtype whose
attribute you are rolling up, as the second example does.
:::

Note what the two forms aggregate. `Workitems.@Count` counts assets;
`Workitems:PrimaryWorkitem.Estimate.@Sum` sums a scalar reached through the relation. `Count`
and `DistinctCount` differ only when the path can arrive at the same asset by
more than one route. `Any` tests a relation for emptiness — unlike `And` and
`Or`, which aggregate boolean values reached through the path:

```text
GET /rest-1.v1/Data/Scope/0?sel=Workitems.@Any
```

Aggregations compose with everything else on this page, and the aggregation
always goes last:

```text
Workitems:Story[AssetState!='Closed'].Estimate.@Sum
```

:::note `AssetState!='Closed'` is narrower than it reads
It compiles to `< 128`, so it excludes **Dead** and **Deleted** assets as well as
Closed ones — it means "before Closed", not "not Closed". See
[State attributes](/reference/query-grammar/filter-expressions#state-attributes).
:::

## Now-Only {#now-only}

In a history request, every referenced asset is resolved *as it was at that
moment*. The now-only operator `#` overrides that for one path part, so the
value is taken from the asset's **current** state instead.

```text
Parent#.Name
```

The canonical use is "find all the stories that belong to the project *currently*
named Website", which is not the same question as "find all the stories that
belonged to a project named Website at the time":

```text
GET /rest-1.v1/Hist/Story?where=Parent%23.Name='Website'
```


:::danger `#` must be sent as `%23` — the failure happens in your client, not on the server
`#` is the URI fragment delimiter ([RFC 3986 §3.5](https://www.rfc-editor.org/rfc/rfc3986#section-3.5)).
A compliant HTTP client splits the URL there and **transmits neither the `#` nor
anything after it**. The server never sees a `#` at all — it sees a **truncated
query string** and answers whatever that truncated query happens to mean.

This matters when you debug it: nothing is wrong on the server, so no amount of
server-side investigation will explain the result. The request the server
received is not the request you wrote.

**What you observe depends on where the truncation lands:**

| Truncation leaves… | Result |
| --- | --- |
| A still-valid query string | `200` with a silently wrong result set |
| A half-finished token | `400` with an `Invalid …` error |

So `?where=Parent#.Name='Website'` is transmitted as `?where=Parent` — a
[bare attribute name](/reference/query-grammar/filter-expressions#bare-attribute-names),
which is syntactically legal — and returns `200` full of the wrong assets. Move
the `#` one character later, into the middle of a token, and the same mistake
becomes a `400` instead. Neither outcome tells you the truth, and which one you
get is an accident of string position.

Percent-encode it: `?where=Parent%23.Name='Website'`.

Most HTTP client libraries will **not** do this for you when you build the URL
as a string, because `#` is legal in a URL. Encode it yourself, or pass the
query string through a parameter-encoding API.
:::

The grammar states the same warning in the comment above `now_only`: "the
encoding of `#` (as `%23`) is not optional… using `#` instead of the encoded
value may return a confusing result."

## Inline Filters

A filter in square brackets may be attached to any path part, not just the last
one. It reduces the set of assets traversed at that point, before the rest of the
path is evaluated.

```text
GET /rest-1.v1/Data/Scope/0/Workitems[ToDo='0']
```


The contents of the brackets are a full
[filter expression](/reference/query-grammar/filter-expressions) — the same
language as `where`, including boolean operators, grouping, value lists, unary
existence tests and variables. Inline filters may be nested inside one another
to arbitrary depth, because a filter expression can itself contain attribute
paths that carry filters.

Position matters. These two are different questions:

```text
Workitems:PrimaryWorkitem[ToDo='0'].Estimate.@Sum     # sum Estimate over the primary workitems with no ToDo
Workitems:PrimaryWorkitem.Estimate.@Sum               # sum Estimate over all primary workitems
```

(The downcast is required here for the reason the
[aggregation warning](#aggregations) gives: `Estimate` is not defined on the
abstract `Workitem`.)

## Ordering of the Operators

All four modifiers can appear on the same path part, and the grammar fixes their
order: name, then `#`, then `:Type`, then `[filter]`.

```text
Workitems#:Story[AssetState!='Closed'].Category.Name
```

Writing them in any other order is a syntax error.

## Worked Example: the Capstone Query

`V1Query.g` carries this example against its `variable` rule. It is worth
walking through because it uses downcast, inline filters, nested inline filters
and self-reference at once.

```text
GET /rest-1.v1/Data/Scope/0
    ?sel=ChildrenMeAndDown[AssetState!='Closed']
         .Workitems:Epic[AssetState!='Closed';-SuperAndUp[Scope.ParentMeAndUp[AssetState!='Closed']=$]]
```

(Wrapped for readability; on the wire it is one unbroken value.)

Reading it outside-in:

1. **`/Data/Scope/0`** — start at the root project.
2. **`ChildrenMeAndDown[AssetState!='Closed']`** — walk to that project and every
   project beneath it, keeping only the ones that are not closed.
   `ChildrenMeAndDown` is one of Agility's transitive-closure relations; the
   `MeAndDown` suffix indicates it includes the asset itself. (Relation
   semantics are metadata, not grammar — confirm the exact membership of any
   `…MeAndUp` / `…MeAndDown` relation through [meta.v1](/apis/meta-v1).)
3. **`.Workitems:Epic[…]`** — from each of those projects, take the workitems and
   downcast to `Epic`, so non-Epic workitems drop out and Epic attributes become
   available.
4. **`AssetState!='Closed'`** — keep only open Epics.
5. **`;`** — AND. See [filter expressions](/reference/query-grammar/filter-expressions#boolean-operators);
   `;` and `&` both mean AND.
6. **`-SuperAndUp[…]`** — the unary `-` asserts that the bracketed set is
   **empty**. `SuperAndUp` walks from an Epic up through its parent Epics, so
   this reads "has no ancestor Epic matching the inner filter".
7. **`Scope.ParentMeAndUp[AssetState!='Closed']=$`** — the inner filter. For each
   ancestor Epic, take its project, walk that project and its ancestors, keep the
   open ones, and compare to `$`.
8. **`$`** — the context asset, meaning "self". Here that is the Scope the
   selection is being evaluated against.

Net result: **open Epics in open projects at or below the root project, that are
not contained by another open Epic in a still-open project.** In other words the
top-level Epics of the open portfolio — a query that would be tedious to express
as several round trips and impossible to express as a flat filter.

## A Note on Cost

Every dot is a traversal and every bracket is a sub-select. The syntax makes it
easy to write a single URL that is expensive to answer. Two practical habits:

- Prefer aggregations to fetching a list and summing it client-side — the rollup
  happens server-side and the payload is one value.
- Name the attributes you need in `sel`. Omitting `sel` gets you the basic set
  plus relation expansion, which is usually more data than you wanted; see the
  [overview](/reference/query-grammar#selection-defaults-and-why-you-get-attributes-you-did-not-ask-for).

## Related Topics

- [Filter expressions](/reference/query-grammar/filter-expressions)
- [Filter context](/reference/query-grammar/filter-context)
- [Grammar reference](/reference/query-grammar/ebnf)
- [Attributes](/concepts/attributes) and [Relations](/concepts/relations)
