The advanced query syntax accepted by the query argument on datastore_items and profiles: fields, conditions, grouping, and keyword synonyms.
The datastore_items and profiles methods accept a query argument with its own advanced syntax, a small query language, not Liquid. It applies identically to both: a datastore_items query filters on the item's custom fields, and a profiles query filters on the profile's custom settings/attributes. Neither one can target a built-in/reserved property this way.
A query is one or more phrases, and a phrase is one of: • a field on its own: matches any item where that field has a value at all • a field with a condition and a value: [field] [condition] [value] • a parenthesized group of phrases: ( [phrase] ) • an inverted phrase or group: NOT [phrase] • two phrases joined by AND or OR
The same query language is used by the Query filter on a datastoreitem or datastoreitemlist field definition, where it is also written as a string (see the datastoreitem field type).
A query that does not parse (unbalanced parentheses, an unknown condition word, a missing value, a dangling operator) is not an error: it is ignored, so the list comes back unfiltered. An empty query string is also no filter. When a filter seems to do nothing, check first that the query string is well formed.
Fields and Values
How a query's field and value tokens are written, including quoting rules.
A field is the custom field's key (its FieldID), written as an unquoted word (letters, numbers, dashes, and underscores only) or as a quoted string. A value is a quoted string, an unquoted word, or, for the numeric comparisons below, a positive or negative integer. Inside a quoted string, double the quote character to include it literally ("He said ""hi""" or 'It''s here'); quoted whitespace is kept as written, unlike whitespace between tokens, which is ignored.
Fields in a query are custom fields only. The item's built-in name (Name) is not a queryable field, so name = '…' silently matches nothing rather than raising an error. To fetch one specific item by its name or GUID, use {% datastore_item %} (or {% profile %}) instead of filtering a list.
A field name can be any custom FieldID; there is no list of allowed names, and a name that no item defines never matches.
An item that does not have the field counts as missing: only a != (not-equals) condition matches it, every other condition does not, and NOT is applied after that. So NOT region = 'west' also matches an item that has no region. A field on its own matches when the field holds any value, including an empty string; a field with no stored value, or a stored null, is missing.
A query reads each field as one value. A field that stores an object (a fieldset) reads as missing. A field that stores a list (a textlist, labels, a list picker) is not matched element by element: depending on how the list is stored it reads as missing or as one text value, so filter list fields in Liquid after the fetch instead.
On profiles, the query argument matches the profile's custom settings and the attributes argument matches its attributes by key; attributes are free-form and compare as text only.
Conditions
The comparison operators a query phrase can use between a field and a value.
Write a phrase as [field] [condition] [value]: • = or ==: equals • != or <> or ><: not equals • <: less than • <=: less than or equal • >: greater than • >=: greater than or equal • CONTAINS or LIKE: field's text contains the value (exact synonyms, with identical behavior, not two different kinds of match)
CONTAINS and LIKE match the value as a plain substring of the field's text; the characters % _ [ and \ are matched literally, not as wildcards.
Numbers: with <, <=, > and >=, an unquoted whole number (positive or negative) compares as a whole number; decimals are not compared as numbers. A quoted value, or an unquoted value that is not a whole number, compares as text, so '10' sorts before '9'.
Dates: a custom date or datetime field compares as a date, stored in UTC, when the value you give can be read as a date, such as 2026-10-16. Any other field compares as text.
AND, OR, and NOT
Joining and inverting phrases with AND, OR, and NOT, their interchangeable spellings, and operator precedence.
Join phrases with AND or OR, or invert one with NOT. Each has several interchangeable spellings: • AND: and, +, &, or && • OR: or, |, or || • NOT: not, or ! • a bare - immediately before a phrase (not part of a negative number) also means "and not", so a - b is the same as a and not b
When a query mixes AND and OR without parentheses, AND binds tighter than OR: a and b and c or d and e or f means (a and b and c) or (d and e) or f. Use parentheses to make the grouping explicit rather than relying on this precedence; it keeps the query readable even when the default grouping is what you want.
Case Sensitivity
Query keywords, field IDs, and values all match without regard to case.
Query keywords (and, or, not, contains, like, and the comparison operators) are not case-sensitive: AND, and, and And all work. Field IDs and values are also matched without regard to case, but that's not a reason to write them any way you like: use the field's real, correctly-cased FieldID. That's how the field is referenced everywhere else in a template, and consistent casing keeps a template readable and easy to search.
Using a Query
Building a query string in Liquid and passing it to the query argument, with safe escaping for user input.
Build the query string in Liquid, often with capture for anything beyond a literal string, and pass it to the query argument, escaping any value built from user input by doubling embedded quote characters so it can't break out of its quotes.
ExampleBuild a query string with captureCapture a query string that searches two fields with CONTAINS and OR, escaping the search term, then pass it to datastore_items.
Liquid
{%- var escapedTerm = term | replace: '"', '""' -%}
{%- capture var filterQuery -%}description CONTAINS "{{ escapedTerm }}" OR requirements CONTAINS "{{ escapedTerm }}"{%- endcapture -%}
{%- datastore_items items = datastore:entity query:filterQuery -%}
ExampleGet datastore items using an advanced queryBuild a query string from request params and pass it to datastore_items to filter by custom fields (e.g. beds, baths).
Liquid
{%- var query = "movein_ready = true" -%}
{%- if request.query_params.beds is_int -%}
{%- set query = query | append: " and beds = " | append: request.query_params.beds -%}
{%- endif -%}
{%- if request.query_params.baths is_int -%}
{%- set query = query | append: " and baths = " | append: request.query_params.baths -%}
{%- endif -%}
{%- datastore_items collection = datastore:entity query:query -%}
ExampleSample datastore item queriesThree query patterns for datastore_items: a simple numeric range/comparison, a complex nested query with NOT and optional conditions, and a dynamically built query across configurable fields.
Filters the Hotels datastore to items with more than 100 rooms and an average room price of 100 or less, combining two numeric comparisons with AND.
Complex nested query with NOT and an optional condition
Liquid
{%- capture query -%}
is_vegan
OR
(
number_of_ingredients < 6
AND (
NOT ingredients contains chicken
-ingredients contains pork
- ingredients LIKE 'beef'
{%- if other_meat_to_avoid is_valid -%}
AND !(ingredients CONTAINS "{{other_meat_to_avoid | replace: '"', '""' }}")
{%- endif -%}
)
AND NOT (
'contains_dairy'
OR ingredients contains egg
)
)
{%- endcapture -%}
{%- datastore_items items = datastore:'Recipes' query:query -%}
Matches vegan recipes, or recipes with fewer than 6 ingredients that avoid chicken, pork, and beef (using NOT and the bare - shorthand for AND NOT), plus an optional other_meat_to_avoid the caller may supply, and excludes anything marked contains_dairy or containing egg.
Query multiple configurable fields dynamically
Liquid
{%- set query = "" -%}
{%- set fieldvalue = request.query_params['fieldvalue'] | urldecode -%}
{%- if request.query_params['fieldnames'] is_valid and fieldvalue is_valid -%}
{%- var fieldnames = request.query_params['fieldnames'] | urldecode | split: ',' -%}
{%- var queryparts = '' | compact -%}
{%- for field in fieldnames -%}
{%- capture querypart %}{{field}} = "{{fieldvalue | replace: '"', '""'}}"{% endcapture -%}
{%- set queryparts = queryparts | concat: querypart -%}
{%- endfor -%}
{%- set query = queryparts | join: ' OR ' -%}
{%- endif -%}
{%- datastore_items items = datastore:entity query:query -%}
Builds an OR query across a caller-supplied, comma-separated list of field names (fieldnames), matching each one against the same escaped value (fieldvalue) from the request's query string.