Filtering
Every List endpoint accepts a filter parameter: a single expression that narrows the results
server-side. qibdo Cloud implements the
Google AIP-160 filter language consistently across all services,
so the same syntax works everywhere.
Pass the expression as the filter query parameter (URL-encoded over REST), alongside
page_size, page_token, and order_by:
curl --get https://api.qibdo.example.com/taxonomy/v1/organisations/$ORG/groups \ --header "Authorization: Bearer $QIBDO_API_TOKEN" \ --data-urlencode "filter=name : 'eng' AND created >= '2026-01-01T00:00:00Z'"An empty filter matches everything.
Grammar
Section titled “Grammar”The filter string is a concrete grammar layered on top of AIP-160: AIP-160 defines the
filter field as the API surface, and qibdo fixes the exact syntax that goes inside it. That
syntax is a strict subset of the
official AIP-160 EBNF. The grammar
qibdo’s parser accepts is:
filter = expression | empty ;
expression = or_expression ;
or_expression = and_expression { OR and_expression } ;
and_expression = unary_expression { AND unary_expression } ;
unary_expression = ( NOT | "-" ) unary_expression | primary ;
primary = "(" expression ")" | function_call | comparison ;
comparison = field comparator value ;
function_call = field "." IDENTIFIER "(" value ")" ; (* exactly one argument *)
field = IDENTIFIER { "." IDENTIFIER } ; (* max 3 segments *)
comparator = "=" | "!=" | ">" | ">=" | "<" | "<=" | ":" ;
value = STRING_LITERAL | NUMBER_LITERAL | BOOLEAN_LITERAL | IDENTIFIER ;
STRING_LITERAL = "'" { char | "''" } "'" (* single-quoted, '' escapes ' *) | '"' { char } '"' ; (* double-quoted *)
NUMBER_LITERAL = [ "-" ] DIGIT { DIGIT } [ "." DIGIT { DIGIT } ] ;
BOOLEAN_LITERAL = "true" | "false" ;
IDENTIFIER = LETTER { LETTER | DIGIT | "_" } ;
AND = case-insensitive "AND" ;OR = case-insensitive "OR" ;NOT = case-insensitive "NOT" ;Whitespace around operators is flexible. The sections below walk through each production.
Operators
Section titled “Operators”Comparison
Section titled “Comparison”| Operator | Meaning | Example |
|---|---|---|
= | equals | status = 'ACTIVE' |
!= | not equals | status != 'DELETED' |
> >= | greater than (or equal) | age > 18 |
< <= | less than (or equal) | progress <= 50 |
: | has / substring (string fields) | name : 'alpha' |
The : operator matches a substring on string fields (name : 'alpha' matches any value
containing alpha). Using it on a numeric or boolean field returns an error.
Logical
Section titled “Logical”Combine terms with AND, OR, and NOT (all case-insensitive, so AND, and, and aNd
are equivalent). A leading - is an alternative to NOT. Group with parentheses.
| Operator | Meaning | Example |
|---|---|---|
AND | both conditions must match | a = '1' AND b = '2' |
OR | either condition must match | a = '1' OR b = '2' |
NOT | negates the following condition | NOT status = 'DELETED' |
- | negation prefix (alternative to NOT) | -status = 'DELETED' |
(status = 'ACTIVE' OR status = 'PENDING') AND region = 'us-east'NOT name.contains('test')-status = 'DELETED'Precedence runs NOT/- (highest), then AND, then OR (lowest), and binary operators are
left-associative. Without parentheses, a = '1' AND b = '2' OR c = '3' is read as
(a AND b) OR c. Use parentheses to override this.
Values
Section titled “Values”- Strings are single- or double-quoted; escape a single quote by doubling it:
name = 'O''Brien'. - Numbers are unquoted, and may be negative or decimal:
temperature > -5,ratio = 3.14. - Booleans are
true/false. - Enums and bare identifiers may be written unquoted:
status = ACTIVE. A bare identifier is resolved against the field’s type: boolean fields readtrue/falseas booleans, numeric fields read digits as numbers, and string fields treat the identifier as text. - Timestamps are quoted RFC-3339 strings:
created >= '2026-01-01T00:00:00Z'.
Wildcards
Section titled “Wildcards”A * inside a string value is a wildcard (it matches any sequence of characters), usable with
= and != on string fields:
| Pattern | Matches |
|---|---|
'alpha*' | values starting with alpha |
'*beta' | values ending with beta |
'*mid*' | values containing mid |
'exact' | exact match (no wildcard) |
name = 'prod-*' # starts with "prod-"name = '*-v2' # ends with "-v2"Wildcards are valid on string fields only; using one on a numeric or boolean field returns an error.
Functions
Section titled “Functions”Three string functions are available (string fields only, exactly one argument, camelCase names):
| Function | Matches | Example |
|---|---|---|
contains() | values containing the argument | name.contains('prod') |
startsWith() | values beginning with it | name.startsWith('api-') |
endsWith() | values ending with it | name.endsWith('-v2') |
name.contains('prod')name.startsWith('api-') AND status = 'ACTIVE'NOT name.contains('test')Field traversal
Section titled “Field traversal”Filter on nested fields with dot notation, up to three segments deep:
cpu.cores > 4cpu.topology.cores > 4 AND memory.size_mib > 1024Which fields are filterable, and which support traversal, depends on the resource; each
service’s reference lists the fields it supports. Field names are the wire names you see in
responses (for example created, not an internal property name). Filtering on an unknown field
returns an INVALID_ARGUMENT error.
Examples
Section titled “Examples”# Exact matchname = 'engineering'
# Comparison with a timestampcreated >= '2026-06-01T00:00:00Z'
# Substring searchname : 'web'
# Combine conditionsstatus = 'ACTIVE' AND region = 'eu-central'
# Group with OR, then AND(status = 'ACTIVE' OR status = 'PENDING') AND -name.contains('temp')
# Prefix wildcardname = 'api-*'
# Nested field comparisoncpu.topology.cores > 4 AND memory.size_mib >= 8192Limits
Section titled “Limits”To keep queries cheap and safe, filters are bounded:
| Limit | Value |
|---|---|
| Filter string length | 2048 characters |
| Field traversal depth | 3 segments |
| Expression nesting (parentheses and negations) | 4 levels |
| Expression count (comparisons and functions) | 100 |
Two notes on the depths:
- Field traversal depth counts dot-separated segments in a single field path:
a.b.cis the deepest accepted,a.b.c.dis rejected. - Expression nesting counts parentheses and negations together. Each
(and eachNOTor-adds one level, so both((((( ... )))))and a chain of fiveNOTs exceed the limit.
Exceeding a limit, using an unsupported operator or function, or supplying a malformed value
returns an INVALID_ARGUMENT error describing the problem (see
Standard responses).
Errors
Section titled “Errors”Every filter error maps to INVALID_ARGUMENT. The common causes:
| You wrote | Why it fails |
|---|---|
name = 'unterminated | invalid syntax (unbalanced quote, (), etc.) |
nonexistent_field = 'x' | unknown field for this resource |
age = 'not-a-number' | value does not match the field’s type |
name.fooBar('x') | unsupported function |
name.contains('a', 'b') | a function takes exactly one argument |
age : 'x' or age.contains('5') | substring/function on a non-string field |
age = '*' | wildcard on a non-string field |
a.b.c.d = 'x' | traversal depth exceeded (max 3) |
(((((a = '1'))))) | nesting depth exceeded (max 4) |
Not supported
Section titled “Not supported”A few AIP-160 features are intentionally left out. Use the listed alternative:
| Feature | Use instead |
|---|---|
IN operator (field IN ('a', 'b')) | field = 'a' OR field = 'b' |
Struct / composite literals ({...}) | dot-notation field traversal |
Cross-resource traversal (author.email) | filter the field on its own resource |
| Duration / timestamp literal types | quote them as strings ('2026-01-01T...') |
| Regex or glob operators | the * wildcard or contains/startsWith/endsWith |
Sorting
Section titled “Sorting”Filtering selects which rows return; order_by controls their order. Pass a
comma-separated list of fields, each optionally suffixed with a direction ( asc or desc,
case-insensitive). The direction defaults to asc, and results fall back to a stable order when
you omit order_by:
order_by=created desc, nameOnly flat fields are sortable. Nested fields (the dot-notation paths you can filter on, such as
cpu.topology.cores) are filterable but not sortable; sort on a top-level field instead.