A query says which records, in which order: a filter of conditions in groups of and and or, sorts, and a search. It is plain maps and lists, which a saved view keeps, a URL carries and an agent writes:

Box.list(tasks, %{
  "filter" => %{
    "and" => [
      %{"field" => "Status", "op" => "in_group", "value" => ["todo", "doing"]},
      %{"or" => [
        %{"field" => "Owner", "op" => "eq", "value" => "me"},
        %{"field" => "Owner", "op" => "is_empty"}
      ]}
    ]
  },
  "sort" => [%{"field" => "Due", "dir" => "asc"}],
  "search" => "invoice"
}, me: current_user, limit: 50)

list/3 gives the records, count/3 how many there are, :limit and :offset page them. Both run in your database, which reads only the records it returns.

Conditions

OperatorOfValue
eq, neqevery type but listsa value of the field
contains, not_contains, starts_with, ends_withtextsa text, without case
gt, gte, lt, ltenumbersa number
betweennumbers, dates[from, to], both included
before, after, on_or_before, on_or_afterdatesa day
withindatespast_week, past_month, past_year, next_week, next_month, next_year, this_week, this_month, this_year
in, not_inselects, statuses, peoplea list
in_groupstatusestodo, doing, done, or a list of them
has_any, has_all, has_nonemulti-selects, relations, several peoplea list
is_empty, is_not_emptyevery type that can be emptynone

"Is not", "does not contain", "is none of" and "has none of" take the records with no value too, as a person means them.

Days said in words

A day is "2026-10-25", or today, tomorrow, yesterday, one_week_ago, one_week_from_now, one_month_ago, one_month_from_now. They are read when the query runs, so a saved view of "due within the next week" shows a different week each week. The days are in :time_zone (UTC unless given): on a field of moments, "is today" is from today's midnight there to the next.

Me

A person is their id, or "me": whoever runs the query (:me). One shared view of "my tasks" shows each person theirs. With no :me, "is me" matches nothing.

Choices

A choice is its id or its name; the query keeps its id, so a renamed choice changes no saved view.

Sorts

Texts sort without case; selects and statuses in the order of their choices, as a board shows them; numbers and dates as themselves. Empty values come last in both directions. After the sorts, records keep the collection's own order (move/3).

A search finds the records with each of its words in their texts or in the names of their choices, without case and without accents: "elan" finds "Élan". It reads each record's search text, which Cardbox writes with its values, the same way on PostgreSQL and SQLite. On PostgreSQL, the migration's trigram index (pg_trgm) serves it.

In Elixir

meets?/4 answers whether a record meets a query without the database: for a view that gets a change (Cardbox.Event) and decides whether to show it. It answers as the database does: a property test runs random queries on random records, on both databases, and compares.

Errors

A query that does not check is an error that says where and what to write instead, for a person or an agent to correct:

the collection has no field "Colour"; its fields are "Title", "Status", "Owner", "Due"
"Points" (number) filters by eq, neq, gt, gte, lt, lte, between, is_empty, is_not_empty, got: "contains"
"Due" takes a day as YYYY-MM-DD or one of today, tomorrow, yesterday, ..., got: "next tuesday"

Groups nest four deep, with at most 100 conditions and 10 sorts. Cardbox.Query.parse/3 reads a query alone; to_wire/1 gives it back, with fields by key and choices by id.