# Filters and views

![A pile of notes filtered into an ordered list](/docs/concepts/view.webp)

A filter describes **which notes** you mean: "films rated 8 or more", "articles I haven't read, saved this month", "anything tagged `rust` or `go` that isn't archived". Potion has one filter, and it is used everywhere a set of notes is chosen:

| Where | The filter decides |
| --- | --- |
| a **view** | which notes appear in its feed |
| the **graph** | which notes it shows |
| an **html block** | which notes it reads, through `potion.getNotes()` |
| the **MCP** | which notes a view or lens an AI builds will hold |

So the rules on this page hold in all of them, on the web, on iOS and through the MCP.

This page is also available as plain Markdown at [/docs/filters.md](/docs/filters.md).

::toc

## A filter is a tree

A filter is a **group** of conditions. A group says how its conditions combine:

| Match | Means | Reads as |
| --- | --- | --- |
| `all` | every condition matches | AND |
| `any` | at least one matches | OR |
| `none` | no condition matches | NOT |

A group can hold other groups, which is how "A and (B or C)" is written. In the web and iOS filter builders, **+ Condition** adds a condition and **+ Group** adds a nested group; the ⋯ menu on each row duplicates it, wraps it in a group, or deletes it.

"Films rated 8 or more, or not rated yet":

```text
all of
├─ Tags has all of  #movie
└─ any of
   ├─ rating ≥ 8
   └─ rating is empty
```

An **empty filter matches every note**. Groups nest at most 4 deep, and a filter holds at most 50 conditions.

## Conditions

There are three kinds of condition.

### Tags

| Condition | Matches notes that |
| --- | --- |
| has all of `a`, `b` | carry every listed tag |
| has any of `a`, `b` | carry at least one |
| has none of `a`, `b` | carry none of them |

A filter refers to tags by identity, so **renaming a tag never breaks a filter**. Deleting a tag that a view or lens filters on is refused until they stop using it.

### Properties

What you can ask depends on the property's type:

| Type | Conditions |
| --- | --- |
| text, select | is, is not, contains, does not contain, starts with, ends with, is any of, is none of |
| number | =, ≠, >, ≥, <, ≤, is between |
| date | is on, is before, is after, is on or before, is on or after, is between, is within |
| bool | is |
| json | has key, contains |
| every type | is empty, is not empty |

- **Text matching ignores case** ("contains Rust" finds "rust"). **Is** is exact.
- **Is between** includes both ends.
- **Is empty** means the note has no value for that property at all.

### Title and dates

Every note also has three built-in fields:

| Field | Conditions |
| --- | --- |
| Title | is, is not, contains, does not contain, starts with, ends with, is empty, is not empty |
| Created | is on, is before, is after, is on or before, is on or after, is between, is within |
| Updated | same as Created |

## Rules worth knowing

**Negative conditions include notes with no value.** `status is not done` also matches notes that have no status at all, the way a person means it. If only notes that *have* a status should count, add `status is not empty` beside it. The same goes for *does not contain*, *is none of* and *has none of*.

**Comparisons skip notes with no value.** `rating > 7` never matches an unrated note, and neither does `rating < 7`.

**Dates are days, not moments.** "Created is on 2026-09-16" matches the whole day. Dates can be fixed or relative:

| Write | Means |
| --- | --- |
| a date, `2026-09-16` | that day |
| today, or *n* days / weeks / months / years ago or from now | a day that moves with the calendar |
| is within the last 30 days | from 30 days ago up to and including today |
| is within the next 2 weeks | from today up to 2 weeks ahead |

A relative date is worked out **every time the filter runs**, so "created within the last 7 days" always means the last 7 days, not the week the view was made. "Today" is your own day: the apps record your time zone when you save a filter.

**A filter never drops a condition silently.** If a condition names a tag or property that no longer exists, it matches nothing rather than being ignored, because ignoring it would quietly show more notes than you asked for.

## Views

A view is a filter you keep. Open it and it runs again, so it always shows what matches now, including notes added after the view was made. A view holds no notes of its own; nothing is "in" a view except by matching.

A view decides three things, and they never affect each other:

| Part | Decides | Never |
| --- | --- | --- |
| **Filter** | which notes appear | — |
| **Sort** | their order | hides a note |
| **Display** | what each card shows | changes which notes appear |

### Sort

A view sorts by one of: **created**, **updated**, **title**, or **a property**, ascending or descending.

- **Notes without a value for the sort property come last**, in either direction. Sorting never hides a note; if you only want notes that have a value, add *is not empty* to the filter.
- A **select** property sorts by the **order of its options** (`unread → reading → done`), not alphabetically.
- A **json** property cannot be sorted by.
- Ties break newest first, so paging through a view is stable.

The property a view sorts by is **shown on its cards automatically**, with an arrow for the direction.

### Display

Beyond the title and preview, a card can show:

- **Up to three property chips**, in the order you choose. A note with no value for one simply shows no chip.
- **Tags**, or not. Hiding them is useful in a view already filtered to one tag, where every card would show the same one.

Most views are best with no chips at all: a rating is useful in "Best films" and noise in "Inbox".

### The rest of a view

- **Cover, colour and icon** give the view its tile.
- **Pinned** views sort to the top of the view list.
- A **note view** is not a filter at all: it is a permanent shortcut to one note, chosen when it is created.

## Examples

| You want | Filter | Sort |
| --- | --- | --- |
| Films worth rewatching | all of: has all of `#movie`, rating ≥ 8 | rating, descending |
| Reading queue | all of: has any of `#article`, `#paper`; status is not `done` | created, ascending |
| This week's captures | Created is within the last 7 days | created, descending |
| What hasn't been rated | all of: has all of `#movie`, rating is empty | title |
| Rust or Go, not archived | all of: has any of `#rust`, `#go`; has none of `#archived` | updated, descending |

## For AI and code

Through the MCP and in html blocks, a filter is written as JSON, using **tag names and property keys**. Potion looks them up in the workspace and refuses any that do not exist, rather than guessing:

```json
{
  "match": "all",
  "children": [
    { "type": "tags", "op": "has_all", "tags": ["movie"] },
    { "type": "group", "match": "any", "children": [
      { "type": "property", "key": "rating", "op": "gte", "value": 8 },
      { "type": "property", "key": "rating", "op": "empty" }
    ] },
    { "type": "field", "field": "created_at", "op": "within",
      "value": { "dir": "last", "n": 30, "unit": "day" } }
  ]
}
```

| In the builder | In JSON |
| --- | --- |
| has all of / any of / none of | `has_all`, `has_any`, `has_none` |
| is, is not | `eq`, `neq` |
| contains, does not contain | `contains`, `not_contains` |
| starts with, ends with | `starts_with`, `ends_with` |
| is any of, is none of | `in`, `not_in`, with a list |
| >, ≥, <, ≤ | `gt`, `gte`, `lt`, `lte` |
| is between | `between`, with `[low, high]` |
| is on, before, after, on or before, on or after | `on`, `before`, `after`, `on_or_before`, `on_or_after` |
| is within | `within`, with `{ "dir": "last" \| "next", "n": 30, "unit": "day" }` |
| has key | `has_key` |
| is empty, is not empty | `empty`, `not_empty`, with no value |

A date value is `"YYYY-MM-DD"` or relative, `{ "relative": { "n": -7, "unit": "day" } }` (units `day`, `week`, `month`, `year`; negative is the past). The root may carry `"tz": "America/Toronto"` to say what "today" means; without it, today is in UTC.

An AI changing an existing view should edit it **one condition at a time** (the MCP's `edit_view_filter`, addressed by node id) rather than resending the whole tree, so the conditions it was not asked about cannot drift. See [The MCP](/docs/mcp). For html blocks, the [Block Data API](/docs/block-data-api) covers the rest: sorting, paging and which fields to read.
