Skip to main content

Attribute Paths

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.

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.

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:

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.

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.

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:

Workitems:Story.Category
GET /rest-1.v1/Data/Scope/0/Workitems:Story.Category
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.

AggregationMeaning
SumSum the returned values
AnyTrue if the set is non-empty (a count-greater-than-zero test on a relation)
CountCount the returned assets
DistinctCountCount the returned assets, counting each asset only once
MinDateThe oldest date among the returned values
MaxDateThe newest date among the returned values
AndTrue if all the returned values are true
OrTrue if any of the returned values is true
MaxStateThe highest state value in the set
GET /rest-1.v1/Data/Scope/0/Workitems.@Count
GET /rest-1.v1/Data/Scope/0/Workitems:PrimaryWorkitem.Estimate.@Sum
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 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:

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

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

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.

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.

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":

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). 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 string200 with a silently wrong result set
A half-finished token400 with an Invalid … error

So ?where=Parent#.Name='Website' is transmitted as ?where=Parent — a bare attribute name, 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.

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

The contents of the brackets are a full filter expression — 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:

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

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.

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.)
  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; ; 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.