Skip to main content

Query Grammar

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:

SurfaceWhere the syntax appears
rest-1.v1sel, where, sort and the rest, on the query string
query.v1select, filter, sort and the rest, inside a JSON or YAML body
api/assetselect, where, filter and sort on each command in a bulk request
api/webhookselect, 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.

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 for the keyword differences.

The Pages​

PageCovers
Attribute pathsDot notation, downcast :Type, aggregations .@Sum, now-only #, inline [filter]
Filter expressionsComparison operators including ~=, and/or/grouping, value lists, unary existence, variables
Filter contextThe with parameter and variable assignment
Sort and paging+/- ordering, page=size,start, and reading the paging response attributes
Grammar referenceEBNF 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.

ParameterPurposeGrammarNotes
selAttribute selectionAttribute pathsselect is an undocumented alias that takes precedence over sel
selectAlias for selas selNot mentioned by the grammar at all
whereFilter expressionFilter expressionsThis is the filter parameter. There is no filter= on rest-1.v1.
withFilter-context variable assignmentFilter context$VAR=a,b|$VAR2=c — values are not quoted here
sortOrderingSort and paging+ ascending (the default), - descending, comma-separated
pagePagingSort and pagingpage=<size>,<start> — size comes first
findText searchnoneAbsent from the grammar entirely
findinAttributes that find searchesnoneAbsent from the grammar. Optional; defaults to Name + ShortName
asofHistory momentnoneAbsent from the grammar. Accepts an integer moment or a datetime
deletedInclude deleted assetsnoneBoolean flag. Absent from the grammar files. Defaults to off on every instance
needTotalCompute the total attribute on the responsenoneBoolean 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:

FlagDefault when omittedVaries by instance?
deletedOff — hardcodedNo
needTotalfalls back to the ApiDefaultNeedTotal server settingYes

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.

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:

?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 shapeselwherewithsortpagefindasofneedTotal
/Data/{Type} (a list)yesyesyesyesyesyesyesyes
/Data/{Type}/{id} (a single asset)yesnoyesnononoyesyes
/Hist/{Type}[/{id}] (history)yesyesyesyesyesyesyesyes
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.

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.

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…
1The filter root rule is filter2_tokenReads 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
2Selection comes from selAlso accepts select, an alias that wins when both are present
3Nothing about find, findin, asof, deleted or needTotalSupports all five
4Selection is exactly what you asked forExpands every relation selection with up to three conditional attributes — commonly just Name
5Nothing about the default selectionFalls back to a per-type basic set, not to all attributes
6paging_token : page_size (',' page_start)?Agrees — size first, then start
7sort_token has no defaultAdds an implicit ascending Moment sort on history requests when sort is absent
8Filtering, sorting and paging are properties of a queryAre 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​

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 for the fields they do accept.

Aspectrest-1.v1query.v1api/asset
EnvelopeGET, parameters on the query stringPOST, a YAML document in the body (JSON is valid YAML)POST, a JSON command or array of commands
Root asset typeThe URL path — /Data/Storyfrom: Story (an as alias parses but is silently discarded)from — an asset type or an OID token
Selectionsel or select, one comma-separated tokenselect or sel, a list; nested mappings are subqueries (an entry's as alias parses but is silently discarded)select, a list
Filter expressionwhere=<expression>filter: — a scalar or list of expressions in the same languagefilter — an array of expressions, as on query.v1
Equality shorthandnot availablewhere: — a mapping of attribute to a single scalar valuewhere — an object, as on query.v1
Filter context / variableswith=$V=a,b|$W=cwith: — a mapping of name to a scalar or a listuses aliases instead
Sortsort=-Estimate,+Namesort: ['-Estimate', '+Name']sort, a list
Pagingpage=<size>,<start> (positional)page: { size: 50, start: 0 } (named; both keys required)page: { start, size } (named; start defaults to 0)
Text searchfind, findinfind, findinfind is command-dependent — see below
Historyasof, plus the separate /Hist namespaceasofnot applicable
Deleted assetsdeleted=truedeleted: trueIsDeleted per command — the ?deleted=1 URL flag does not surface deleted rows on its own
Total countneedTotal=trueneedTotal: trueneedTotal: true
Groupingnot availablegroup: — a list of expressions, and grouping subqueriesnot available
Several queries per requestnoyes — the document root may be a sequence of query mappingsyes — the body may be an array of commands
Writesop for operations; POST a payload to updatenot available — update and execute are strippedupdate 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 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.

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:

They are transcribed as EBNF on the grammar reference page. Read them as a syntax map, not as an oracle — see the divergences above.