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.
?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
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 |
GET /rest-1.v1/Data/Scope/0/Workitems.@Count
GET /rest-1.v1/Data/Scope/0/Workitems:PrimaryWorkitem.Estimate.@Sum
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
AssetState!='Closed' is narrower than it readsIt 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'
# 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 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,
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:
/Data/Scope/0— start at the root project.ChildrenMeAndDown[AssetState!='Closed']— walk to that project and every project beneath it, keeping only the ones that are not closed.ChildrenMeAndDownis one of Agility's transitive-closure relations; theMeAndDownsuffix indicates it includes the asset itself. (Relation semantics are metadata, not grammar — confirm the exact membership of any…MeAndUp/…MeAndDownrelation through meta.v1.).Workitems:Epic[…]— from each of those projects, take the workitems and downcast toEpic, so non-Epic workitems drop out and Epic attributes become available.AssetState!='Closed'— keep only open Epics.;— AND. See filter expressions;;and&both mean AND.-SuperAndUp[…]— the unary-asserts that the bracketed set is empty.SuperAndUpwalks from an Epic up through its parent Epics, so this reads "has no ancestor Epic matching the inner filter".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$.$— 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. Omittingselgets you the basic set plus relation expansion, which is usually more data than you wanted; see the overview.