Saved views
A view is data — a filter tree, sorts and a group-by — compiled to SQL in one place. What it selects, and what it only changes about looking at it.
A view is a saved way of looking at a collection. It is made of two halves that are deliberately kept apart:
definition— what the view selects: a filter tree, sorts, an optional group-by, and whether to follow secondary list attachments;display— how it is looked at: the order of board columns, and which fields the view shows (columns) — a table's column order, widths and hidden columns, and the same order and hidden fields for the cards of a board or a list. One setting per view, whatever itskind.
Moving, widening or hiding a column changes display. It cannot make a single
card enter or leave the result, because it never reaches the part that selects.
`display` is replaced wholesale
Saving one half means sending the other back. A client that saves column widths
without returning groupOrder erases the board's column order.
A definition is data, not SQL
{
"filter": {
"and": [
{ "op": "eq", "field": { "fieldId": "sys:status" }, "value": "<statusId>" },
{ "op": "contains", "field": { "fieldId": "<fieldId>" }, "value": "draft" }
]
},
"sort": [{ "field": { "fieldId": "sys:order" }, "direction": "asc" }],
"groupBy": { "fieldId": "sys:status" },
"includeSecondaryLists": true
}No SQL fragment can enter a view — not a snippet, not an expression, not a column name. A definition is a serialisable tree, which is what makes it storable, sendable, diffable, and executable somewhere other than where it was written.
It becomes SQL in exactly one place in the codebase. That is a deliberate constraint: it is what makes it possible to route a view to a different read backend one day without touching a single screen.
Kinds
A view has a kind: table, board, list or calendar.
The kind does not change what the view selects. It changes the layout, and for the grouped kinds it changes which route you call.
The API accepts all four. The interface currently renders three — table,
list and board; see what it does not do.
Grouping, and why columns come from the domain
A board column is the view, plus one group value. Same compiled SQL, one extra predicate — which is what makes the columns add up to the view exactly.
Two consequences you will meet immediately:
Each group paginates on its own. POST …/grouped-query returns groups, each
with its own cursor. Slicing a flat page into columns client-side would fill
three columns at the mercy of the sort order and leave "load more" on a single
column inexpressible.
The caller names the groups it wants. A GROUP BY only returns values that
have rows, so an empty "Done" column would not exist — and you cannot drop a
card into a column that is not rendered. The caller passes the groups, because
only the caller knows the workspace's statuses, a select's declared options,
or the member list.
So an axis you cannot write accepts no drops: dropping a card is a write, not a gesture of arrangement.
Group keys must survive a round trip
A group key comes out of one query as ::text and goes back into another to
find that group's rows. If it doesn't re-read in the type of the expression, the
column shows "12" in its header and zero rows underneath — with no error
anywhere. This is why grouping by sys:created_at is refused outright: the
column is a timestamp, its declared type is a date, and the comparison would
match nothing. (Grouping by an instant would also give one group per card.)
Manual order is global to the list
sys:order is one ordering per collection, not one per column.
So dropping a card at the top of a board column also puts it on the first row of the table view. Notion keeps a separate order per group; here there is one, and it shows. If that surprises you, it is the documented behaviour rather than a bug.
One consequence that was applied on purpose: dropping into an empty column writes no order at all. The card will be first there regardless, and writing one would have catapulted it to the top of the entire list.
The default view
A collection with no saved views is not unreadable. The API publishes
CollectionDetail.defaultView: manual order, grouped by status, secondary lists
included.
It is published rather than copied into each client for a reason — see collections and cards.
Sorting and paging
Pagination is keyset only. There is no OFFSET anywhere in the API, and no
way to ask for one: the cursor is opaque, you pass back what you were given.
That is measured, not aesthetic — around page 12 000, the offset form took 348 ms where the keyset form took 3 ms.
A cursor is bound to the sort it was issued for. Change the sort, start again; the API will tell you rather than silently return a wrong page.
Details in querying content.