DocsReference

Filter syntax

The full grammar of the language that API rules and list filters share, PocketBase's own, compiled to SQL.

API rules and the filter of a list use one language: PocketBase's. The compiler turns it into one SQL expression with every value bound as a parameter. How rules apply per action: Rules and filters.

Operands#

operandexample
a field pathstatus, client.name, meta.address.city
text, single or double quoted'open', "done", 'it\'s'; escapes \n \t \r \\ \' \"
a number12, -1.5 (no exponent)
a literaltrue, false, null
a request value@request.auth.id
a macro@now, @todayStart
a functiongeoDistance(lon, lat, 2.35, 48.85)

// starts a comment to the end of the line. Whitespace is space, tab and newline.

Operators#

operatormeaningany of
=equal?=
!=not equal?!=
> >= < <=compare?> ?>= ?< ?<=
~contains (LIKE)?~
!~does not contain?!~
  • Without ?, a multiple value (multi select, multi relation, back relation) must match for ALL its values. With ?, for at least one.
  • = and != treat '' and NULL as equal: x = '' matches a NULL. != is IS NOT.
  • ~ wraps a literal in %…% unless it already holds an unescaped %. _, % and \ in it are escaped. Against a field, it is LIKE ('%' || field || '%'). LIKE folds ASCII case only, never accents.
  • Numbers compare as REAL, like PocketBase: title = 5 does not match the text '5'.

Joins#

&& (and), || (or), parentheses. AND binds before OR, as in SQL. A group must stand where an operand starts: a = (b = 1) is refused.

Paths#

pathreaches
fielda column of the collection
rel.fieldthrough a relation, single or multiple, nested up to 6 levels
rel.idthe same as rel
json.a.0.binside a JSON field
geo.lon, geo.lata geo point
posts_via_author.titlea back relation: the posts whose author points here. Multiple, unless author has a single column unique index

A bare multiple column (tags with no path or modifier) compares its raw JSON text. Write tags:each ?= "x", editors.id ?= "x" or tags ~ "x" instead.

Request values#

pathvalue
@request.auth.<field>the signed in record: id collectionId collectionName email emailVisibility verified read from it, any other field joined, relations included
@request.body.<field>the submitted data. @request.body.<rel>.<field> looks up the records the body names
@request.query.<key>the query string
@request.headers.<key>a header, key snake cased: X-Token is x_token
@request.methodthe HTTP method
@request.contextdefault, expand, realtime, protectedFile, batch, oauth2, otp, password, or mcp
@collection.<name>[:alias].<field>another collection, cross joined. All of, unless ?

A key the request does not carry is NULL. A client filter that is not a superuser's cannot use @request. or @collection..

Modifiers#

modifieronmeaning
:isset@request.*the key was sent
:changed@request.body.<field>sent, and different from the stored value
:lengthselect, file, relation, also in the bodythe number of values
:eachselect, file, relation, also in the bodyeach value, so ?= and = apply per value
:lowerany field or request valuelowercased

Macros#

All in UTC.

macrovalue
@now, @yesterday, @tomorrowa datetime, 2006-01-02 15:04:05.000Z form
@todayStart, @todayEnd, @monthStart, @monthEnd, @yearStart, @yearEnda datetime
@second, @minute, @hour, @day, @month, @weekday, @yearan integer

Functions#

  • geoDistance(lonA, latA, lonB, latB): the distance in kilometers.
  • strftime(format, [time, modifiers…]): SQLite's date formatting.

Parameters#

{:name} in a filter takes the value of name from its parameters, quoted and escaped. Use bindFilter in the client and filter on the server:

bindFilter('status = {:s} && created >= {:since}', { s: 'open', since: new Date('2026-01-01') })
// status = "open" && created >= "2026-01-01 00:00:00.000Z"

Sort#

A comma list of fields and relation paths, - for descending, + or nothing for ascending, @random, @rowid. At most 8 fields.

Limits#

limitvalue
expressions in one filter200
characters in a client filter3500
fields in a sort8

This page as markdown