Skip to content
qibdo qibdo
Theme
Book a demo

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:

Terminal window
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.

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.

OperatorMeaningExample
=equalsstatus = 'ACTIVE'
!=not equalsstatus != '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.

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.

OperatorMeaningExample
ANDboth conditions must matcha = '1' AND b = '2'
OReither condition must matcha = '1' OR b = '2'
NOTnegates the following conditionNOT 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.

  • 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 read true/false as 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'.

A * inside a string value is a wildcard (it matches any sequence of characters), usable with = and != on string fields:

PatternMatches
'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.

Three string functions are available (string fields only, exactly one argument, camelCase names):

FunctionMatchesExample
contains()values containing the argumentname.contains('prod')
startsWith()values beginning with itname.startsWith('api-')
endsWith()values ending with itname.endsWith('-v2')
name.contains('prod')
name.startsWith('api-') AND status = 'ACTIVE'
NOT name.contains('test')

Filter on nested fields with dot notation, up to three segments deep:

cpu.cores > 4
cpu.topology.cores > 4 AND memory.size_mib > 1024

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

# Exact match
name = 'engineering'
# Comparison with a timestamp
created >= '2026-06-01T00:00:00Z'
# Substring search
name : 'web'
# Combine conditions
status = 'ACTIVE' AND region = 'eu-central'
# Group with OR, then AND
(status = 'ACTIVE' OR status = 'PENDING') AND -name.contains('temp')
# Prefix wildcard
name = 'api-*'
# Nested field comparison
cpu.topology.cores > 4 AND memory.size_mib >= 8192

To keep queries cheap and safe, filters are bounded:

LimitValue
Filter string length2048 characters
Field traversal depth3 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.c is the deepest accepted, a.b.c.d is rejected.
  • Expression nesting counts parentheses and negations together. Each ( and each NOT or - adds one level, so both ((((( ... ))))) and a chain of five NOTs 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).

Every filter error maps to INVALID_ARGUMENT. The common causes:

You wroteWhy it fails
name = 'unterminatedinvalid 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)

A few AIP-160 features are intentionally left out. Use the listed alternative:

FeatureUse 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 typesquote them as strings ('2026-01-01T...')
Regex or glob operatorsthe * wildcard or contains/startsWith/endsWith

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, name

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