# Filtering records

_Every view filter operator, its value shape, the field kinds that accept it, and relative date resolution._

Canonical: https://snoze.dev/help/filtering-records/

A filter keeps records that match a condition. Filters live on one view and do
not change or secure the records. Use permissions when someone must not be able
to retrieve the underlying data.

Each filter stores a field key, an operator, and a string value. Conditions at
the top level use `and` or `or`; one level of filter groups may choose its own
conjunction. Filters run before grouping and sorting.

<!-- GENERATED:view-filter-operators:START -->

Canonical contract values:

- `contains`
- `does_not_contain`
- `is`
- `is_not`
- `is_empty`
- `is_not_empty`
- `equals`
- `not_equals`
- `greater_than`
- `less_than`
- `before`
- `after`
- `is_within`
- `is_checked`
- `is_unchecked`

<!-- GENERATED:view-filter-operators:END -->

## Operators

### Contains `contains`

For ordinary fields, performs a case-insensitive substring check on the
formatted value. For Multi-select and Relation, the value is one exact option
key or related record UUID and the operator tests membership instead. A User or
File value is compared through its formatted stored value, not a nested
property.

### Does not contain `does_not_contain`

Negates Contains. For Multi-select and Relation, it passes when the exact
option or record UUID is absent. For text-like values, it passes when the
case-insensitive substring is absent.

### Is `is`

Tests equality. Select and Status values are option keys selected by the
editor. Relation equality means the relation has exactly one target and it is
the supplied record UUID; use Contains for membership in a many-valued
relation. Date `is` compares the local calendar day, not the timestamp's exact
milliseconds.

### Is not `is_not`

Negates Is. On a relation, a value with zero, two, or more links is therefore
“not” any single target. On general formatted values the comparison is
case-insensitive as well as checking the raw stored string.

### Is empty `is_empty`

Needs no value. Empty means `null`, `undefined`, an empty string, or an empty
array. For Date, a range is empty when it has no start. `false`, `0`, and an
empty JSON object are values, not empty. Button has no stored value, so this is
the only practically useful positive filter on that kind.

### Is not empty `is_not_empty`

Needs no value and negates Is empty. It does not mean truthy: unchecked
checkboxes and numeric zero are still not empty.

### Equals `equals`

Compares Number or Time tracking numerically. The filter value is a decimal
number encoded as a string; time-tracking controls convert human minutes to the
stored millisecond number.

### Not equals `not_equals`

Numeric inequality for Number and Time tracking. An invalid numeric filter is
treated as non-restricting rather than matching no records, and the editor
normally prevents such a value.

### Greater than `greater_than`

Keeps numeric or duration values greater than the supplied number. This is a
strict comparison; equal values do not pass.

### Less than `less_than`

Keeps numeric or duration values below the supplied number. This is also
strict.

### Before `before`

Compares the start of Date ranges, Created at, or Updated at with a literal date
or the relative day token `today`, `yesterday`, or `tomorrow`. A timestamp must
be strictly earlier to pass.

### After `after`

The date counterpart to Before. It uses a range's start and requires a strictly
later timestamp.

### Is within `is_within`

Accepts one rolling range token: `past_week`, `past_month`, `past_year`,
`next_week`, `next_month`, or `next_year`. The bounds are inclusive. It does
not accept an arbitrary start/end pair in the current view filter contract.

### Is checked `is_checked`

Needs no value and passes only when a Checkbox stores `true`.

### Is unchecked `is_unchecked`

Needs no value and passes only when a Checkbox stores `false`. Newly created
records normally have the intrinsic unchecked value.

## Operators by field kind

This table is generated by asking the same contract predicate used to validate
view filters. Formula and Rollup currently receive the general formatted-value
set even when their declared result kind is numeric, date, or checkbox.

<!-- GENERATED:field-operator-table:START -->

| Field kind      | Accepted operators                                                              |
| --------------- | ------------------------------------------------------------------------------- |
| `text`          | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `long_text`     | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `number`        | `is_empty`, `is_not_empty`, `equals`, `not_equals`, `greater_than`, `less_than` |
| `rating`        | `is_empty`, `is_not_empty`, `equals`, `not_equals`, `greater_than`, `less_than` |
| `date`          | `is`, `is_empty`, `is_not_empty`, `before`, `after`, `is_within`                |
| `select`        | `is`, `is_not`, `is_empty`, `is_not_empty`                                      |
| `multi_select`  | `contains`, `does_not_contain`, `is_empty`, `is_not_empty`                      |
| `status`        | `is`, `is_not`, `is_empty`, `is_not_empty`                                      |
| `checkbox`      | `is_empty`, `is_not_empty`, `is_checked`, `is_unchecked`                        |
| `url`           | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `email`         | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `phone`         | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `user`          | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `file`          | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `place`         | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `json`          | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `created_at`    | `is`, `is_empty`, `is_not_empty`, `before`, `after`, `is_within`                |
| `created_by`    | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `updated_at`    | `is`, `is_empty`, `is_not_empty`, `before`, `after`, `is_within`                |
| `updated_by`    | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `unique_id`     | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `formula`       | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `relation`      | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `rollup`        | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |
| `time_tracking` | `is_empty`, `is_not_empty`, `equals`, `not_equals`, `greater_than`, `less_than` |
| `button`        | `contains`, `does_not_contain`, `is`, `is_not`, `is_empty`, `is_not_empty`      |

<!-- GENERATED:field-operator-table:END -->

The table describes accepted manifest operators. Acceptance does not guarantee
a useful stored value: Button stores none, for example, so its general text
operators normally see an empty string.

## Relative date resolution

`today`, `yesterday`, and `tomorrow` resolve whenever the filter is evaluated.
The evaluator starts at local midnight and moves by a local calendar day, so
the answer follows the environment doing the evaluation rather than storing a
fixed timestamp in the view.

The range tokens are rolling windows anchored at the current instant:

- week means exactly 7 days;
- month means exactly 30 days;
- year means exactly 365 days;
- `past_*` covers now minus that duration through now;
- `next_*` covers now through now plus that duration.

These are not calendar-week, calendar-month, or calendar-year boundaries. A
view using `next_month` changes continuously as time advances.

## Values and failure behaviour

Option-bearing fields use stable option keys even though the editor displays
names. User, File, and Relation filters use UUIDs. Text operators take literal
text. Date operators accept parseable date strings or the tokens above.

Operators that require a value but receive an empty or invalid value are
non-restricting in the shared engine. A filter for a field key that no longer
exists also passes every record. This keeps a stale view readable, but it means
an API-authored malformed filter does not fail closed; inspect the saved filter
when a view looks unexpectedly broad.