Platform CLI
Learn
Developer docs User guide Quickstart CLI and AI agents Blog
Company
Services About Contact Links Get started

Filtering and sorting in templates

Updated

Raytha filters content with a small subset of OData's $filter syntax. You write it in the Filter argument of get_content_items, in the ?filter= parameter of a list view, and in the REST API. This page lists everything the engine accepts and what happens to everything it does not.

A first filter

{% assign news = get_content_items(
     ContentType="posts",
     Filter="category eq 'news' and rank ge 5 and not contains(PrimaryField, 'draft')",
     OrderBy="rank desc, CreationTime desc",
     PageSize=10) %}
{% for post in news.Items %}
  <p>{{ post.PrimaryField | escape }}</p>
{% endfor %}

Raytha parses the expression with Microsoft's OData parser, turns it into a tree and compiles that tree to SQL with every value bound as a parameter. Values never become part of the SQL text. Anything the compiler does not recognise is rejected with an error. It is never passed through.

Syntax

ElementSupported forms
Comparisonfield eq 'x', ne, gt, ge, lt, le
Logicand, or, not, parentheses. not binds tightest, then and, then or. Add parentheses when you mix and with or.
Text matchcontains(field, 'x'), startswith(field, 'x'), endswith(field, 'x')
Null checkfield eq null, field ne null
ConstantsA quoted string 'x', a bare number 5 or 4.5, or true and false

The field name must be on the left and a constant on the right. These are all rejected:

  • Comparing a field with another field (a eq b) or putting the constant first ('x' eq a).
  • Any function other than the three above: tolower, length, year, substring and the rest.
  • gt, ge, lt or le against null.
  • in, has, any, all, arithmetic (add, mul) and $ options.

Operators and function names are lowercase. EQ and CONTAINS are syntax errors.

Quote string values with single quotes, even for numbers and dates. rank eq '5' and rank eq 5 do the same thing, and Raytha converts the text to the column's type. A value that does not fit the type is an error (see below).

Which fields you can filter on

FieldNotes
Any custom fieldBy its developer name, exactly as written. Names are case-sensitive.
PrimaryFieldAn alias for the content type's primary field.
IdValue is the id in short form, long GUID form, or either prefixed with guid_.
CreationTime, LastModificationTimeUTC timestamps. CreationTime ge '2026-01-01'.
IsPublished, IsDraftIsPublished eq 'true'. Template calls already limit get_content_items to published items, so you do not need it there.

CreatorUser, LastModifierUser, Template and RoutePath exist on items but cannot be used in a filter. In particular you cannot filter on the route path on the server. To list the items under a prefix such as blog/, loop over the items and test item.RoutePath startswith "blog/" in Liquid (see Template recipes).

Rules by field type

Field typeComparisonText matchEmpty / not empty
Single line text, Long text, Wysiwyg, Dropdown, Radio, Attachment, ColorText comparison. eq and ne are case-sensitive.Case-insensitive. Underscore and percent characters in the value are literal.f eq '' or f eq null / f ne '' and f ne null
NumberNumeric, with two decimal places. The value must be a number.Matches the stored text.f eq null / f ne null
DateBy calendar date. The value is '2026-01-31' or a date in the organization's date format.Matches the stored text.f eq null / f ne null
CheckboxCompare as text: f eq 'true'. For "not ticked" use f ne 'true'.n/an/a
Multiple selectf eq 'x' means "has the choice x"; f ne 'x' means "does not". contains(f, 'x') is the same as eq. Both are exact matches on a choice's developer name. gt, lt and the other range operators are rejected.only containscontains(f, '[]') / not contains(f, '[]')
One-to-one relationshipf eq '<id>' matches the related item by id. Any other value is compared with the related item's primary field text.On the related item's primary field text.f eq '' (the related item has no primary field text, or there is no related item)
RepeaterRejected.Only contains(f, '[]') for "has no rows" (and not for "has rows").As left.

A comparison against a missing value is false, so title eq 'a' and rank gt 3 skip items that have no value. Negations are real negations: title ne 'a' and not contains(title,'a') include items that have no value at all.

The text-matching operators do not look at the value's case or at SQL wildcards. contains(title, '50%') finds the literal text 50%.

Examples

category eq 'news'
category ne 'archive'
rank ge 3 and rank lt 10
featured eq 'true'
published_on ge '2026-01-01' and published_on lt '2027-01-01'
tags eq 'release'
not contains(tags, '[]')
author eq 'Ada Lovelace'
startswith(PrimaryField, 'The ')
(category eq 'news' or category eq 'tips') and featured eq 'true'
summary eq '' or summary eq null
CreationTime ge '2026-06-01'

Sorting

OrderBy is a comma-separated list of field direction, where direction is asc or desc:

PrimaryField asc
rank desc, CreationTime desc
category asc, PrimaryField asc
  • Sortable: PrimaryField, CreationTime, LastModificationTime, CreatorUser and LastModifierUser (by first name), and every custom field except repeaters. Do not sort by Template or RoutePath. Numbers sort numerically, dates by calendar date, relationships by the related item's primary field, text as text.
  • A term with no direction, an unknown field or an unsortable field is skipped without an error. OrderBy="PrimaryField" therefore does nothing. Always write the direction.
  • Raytha adds the item id as a last tie-breaker, so pages stay stable when values repeat.
  • With no OrderBy, get_content_items sorts by CreationTime desc.

Building a filter from user input

Escape a single quote in a value by doubling it: PrimaryField eq 'O''Brien'. When a template puts input into a filter, double the quotes first, and build the string with capture:

{% assign term = QueryParams.q | default: "" | replace: "'", "''" %}
{% capture filter %}contains(PrimaryField, '{{ term }}'){% endcapture %}
{% assign results = get_content_items(ContentType="posts", Filter=filter, PageSize=20) %}

Without the escape, a visitor can close the quote and add their own conditions. They cannot reach SQL, but they can change what the filter means, and a malformed filter takes the whole page down with an HTTP 400. For a number from the URL, force it to a number first, for example {% assign n = QueryParams.min | plus: 0 %}. For today's date use {{ "now" | date: "%Y-%m-%d" }} inside a capture.

Errors

A filter the engine cannot use fails the page. In a template, get_content_items raises it, and a visitor gets HTTP 400 with the message as plain text. The same applies to a bad ?filter= on a list view. The messages are:

MessageCause
The filter expression could not be parsed: ...Syntax error, unterminated quote, unknown operator.
Unknown filter field 'x'.No such developer name on this content type.
Field 'x' cannot be used in a filter.A reserved field like RoutePath, or a relationship that cannot be resolved.
Field 'x' cannot be used with this operator.For example gt on a multiple select or repeater.
Field 'x' can only be filtered with contains. / ... by empty or not empty.startswith or endswith on a multiple select, or any text match except contains(f, '[]') on a repeater.
Operator 'GreaterThan' cannot be used with null.Only eq null and ne null exist.
Field 'x' expects a numeric value / a date / true or false.The constant does not fit the column.
Unsupported filter expression of kind 'In'. / Unsupported function 'x'.Operators and functions outside the table at the top.
A filter comparison must reference a field.The constant is on the left, or the left side is a function such as tolower(a).
A filter value must be a constant.Field-to-field comparison.
Function 'startswith' requires a non-null value.startswith(a, null) and the like.

Gotchas

  • Checkbox filters are text filters. featured eq true becomes the text True, which does not match the stored true. Write featured eq 'true'.
  • The engine reads a missing text value as an empty string, so summary eq null never matches a text, dropdown, checkbox or relationship field. Use summary eq '', or both as in the example above. eq null works on number, date and the built-in time fields.
  • A dropdown stores the choice's developer name, so filter on that, not on the label.
  • Numbers are compared as decimal(18,2), and only plain non-negative numbers such as 12 or 3.5 count. A stored negative number, or one with an exponent, reads as empty: it matches nothing, and it sorts as if it had no value.
  • Filtering the related item's own fields through a relationship is not possible. author eq only compares with its id or primary field text.

Next steps