Skip to content

Core Filters

This annex is the core filter set. It is closed within a spec version. Implementations MUST provide every filter here, and MUST NOT add a filter with a bare name. A bracketed argument is optional. T? denotes a value that can be null.

Filter behavior is defined by this annex together with the conformance suite fixtures. Where a description defers to fixtures, the fixtures are normative.

The filter rules govern these four filters, including the current binding and the closed operator set of where.

Filter Signature
where(path, op, value) list[object] → list[object] — keeps elements whose path satisfies the comparison
keep(expr) list[T] → list[T] — keeps elements for which expr is true, with current bound to the element
each(expr) list[T] → list[U] — evaluates expr once per element, with current bound to the element
has(path) object → bool — the path exists and is non-null
Filter Signature and behavior
length list | map | string → int — elements, entries, or code points
default(v) T? → T — yields v when the value is null
first / last list[T] → T?null for an empty list
slice(a, b) list → list — half-open, and a negative index counts from the end
reverse list → list
keys / values map[string, T] → list — in the map’s key order
items map[string, T] → list[object] — each element is { key: string, value: T }
flatten list[list[T]] → list[T] — one level
batch(n) list[T] → list[list[T]] — chunks of at most n; n MUST be a positive integer
group_by(path) list[object] → map[string, list[object]] — the value at path MUST be a string
sort list → list — by the standard ordering
sort_by(path) list[object] → list[object] — by the standard ordering, stable
unique list → list — keeps the first occurrence of each value, in order
sum / min / max list[number] → numbersum of an empty list is 0; min and max of an empty list yield null
join(sep) list[string] → string
Filter Signature and behavior
split(sep, [limit]) string → list[string]limit is the maximum number of elements, and the last holds the unsplit remainder; it MUST be a positive integer
lower / upper string → string — Unicode simple case mapping, independent of locale
trim string → string — removes leading and trailing Unicode whitespace
capitalize string → string — uppercases the first cased character, and lowercases every other cased character
title string → string — uppercases the first cased character of each word, and lowercases every other cased character; a word is a maximal run of characters that are not Unicode whitespace
starts_with(s) / ends_with(s) string → bool
matches(re) string → bool
extract(re) string → string? — the first capture group, or the whole match when the pattern has no group; null when unmatched
extract_all(re) string → list[string] — every non-overlapping match, taking the first capture group of each, or the whole match when the pattern has no group; an empty list when unmatched
replace(a, b) string → string — every occurrence; a is a literal, not a pattern
format(fmt) list → string — each {} in fmt consumes the next element; {{ and }} are literal braces; an arity mismatch is an evaluation fault
truncate(n) string → string — at most n code points; no marker is appended
indent(n) string → string — prefixes every line with n spaces
wordwrap(n) string → string — breaks lines at whitespace so that no line exceeds n code points; a word longer than n occupies its own line and is not broken
pluralize(one, many) int → string — yields one when the value is 1, and many otherwise
human_size int → string — a byte count as text; see below
strip_html string → string — see below

matches, extract, and extract_all MUST use RE2 syntax: no backreferences, no lookaround, and linear-time matching. Security considerations states why.

human_size scales by 1000, with the units B, kB, MB, GB, TB, and PB. It selects the largest unit for which the magnitude is at least 1. It formats with one decimal place, except for B, which has none. One space separates the number from the unit. 1500 yields 1.5 kB, and 900 yields 900 B.

strip_html removes every tag, and removes the content of script and style elements. It decodes named and numeric character references. It then replaces each run of whitespace with one space, and trims.

Filter Signature and behavior
abs number → number
round(n) number → number — to n decimal places, half away from zero
int T → int — from a decimal string with an optional sign, or from a float whose fractional part is zero; every other input is an evaluation fault
float T → float — from an int, or from a decimal string
string T → string — from a scalar: null yields the empty string, a bool yields true or false, a timestamp yields RFC 3339 in UTC, and a duration yields its canonical literal; a list or a map is an evaluation fault
bool T → bool — from a bool, or from the strings true and false; every other input is an evaluation fault
timestamp string → timestamp — from RFC 3339 with a mandatory offset; the display zone is the literal’s offset; every other input is an evaluation fault
duration string → duration — from the duration literal grammar; every other input is an evaluation fault
json any → string — canonical serialization: sorted keys, no insignificant whitespace, UTF-8
from_json string → json — parses one JSON document (RFC 8259); the result is typed json; an input that is not valid JSON, or an object with a repeated member name, is an evaluation fault

timestamp and duration are the inverses of string on those types, and from_json is the inverse of json, so a value survives a round trip through text. The grammar states the separate rule that a position which declares the type timestamp accepts an RFC 3339 string without a filter.

Filter Signature and behavior
plus(duration) / minus(duration) timestamp → timestamp — preserves the display zone
diff(timestamp) timestamp → duration — the receiver minus the argument; the result can be negative
format_time(fmt) timestamp → string — in the display zone; see the directive set below
to_timezone(tz) timestamp → timestamp — changes the display zone only; tz is an IANA identifier or a fixed offset

format_time directives. The set is closed. A character outside a directive is copied literally. An unrecognized directive is a validation error where fmt is a literal, and an evaluation fault otherwise.

Directive Yields
%Y Year, at least four digits
%m / %d Month 0112 / day of month 0131
%j Day of year, 001366
%H / %M / %S Hour 0023 / minute 0059 / second 0059
%I / %p Hour 0112 / AM or PM
%L Millisecond, 000999
%z / %:z Offset from UTC, as +HHMM / as +HH:MM
%b / %B Month name, abbreviated / full
%a / %A Weekday name, abbreviated / full
%% A literal %

Month and weekday names are English: January to December, Monday to Sunday, and their first three characters as the abbreviation. A name in another language belongs in an extension package, where fixtures verify it.

These filters exist so that an author does not write escaping by hand.

Filter Signature
html_escape string → string
json_escape string → string
url_encode string → string
shell_quote string → string
md5 / sha256 string → string — lowercase hexadecimal
base64 string → string — the standard alphabet, with padding

now, random, uuid, and every other source of an ambient or nondeterministic value are not filters, and MUST NOT be added as extensions, because they violate purity. The current instant is run.started_at, a recorded binding rather than a value synthesized inside an expression.

There is no count, no drop, no where_not, and no two-argument equality form of where. length, keep(not …), and where('status', '!=', 'open') are the one representation of each.

Template-autoescaping machinery such as safe and force_escape has no analogue here. In expressions there is no output document to autoescape, and in local templates escaping is contextual and mandatory.

There is no hmac and no other signing filter, and no canonical package will hold one. A signature requires a key, a key is a credential, and no expression can read a credential. An action that requires a signed request receives the signature from its connector’s executor.

slugify is left out of core, because implementations diverge silently on its Unicode behavior. The canonical text package defines it, with fixtures that pin the behavior.

relative_to is left out of core for the same reason: a human-readable interval is wording, and wording is a locale. The canonical human package defines it, with the locale as an explicit argument.

Format-directed time parsing is left out of core and out of the current library. The timestamp filter converts RFC 3339, which is unambiguous. A parse directed by a pattern such as %d/%m/%Y needs a locale, a century rule for a two-digit year, and a policy for an impossible date. Each is a place where implementations differ without saying so.