Query grammar
The one query language that Explore, the CLI, the SDK and Seed Agents share for finding documents by their attributes, the DocumentFilter it compiles to, and the attribute listings that tell you which keys exist.

Every Hypermedia document carries attributes: its metadata, from name and summary to any key a schema or a person adds. The query grammar is one short string that says which documents you mean by those attributes, such as status="In Progress" AND priority>=3. The Explore surface in the Seed app, the Seed CLI's query --where, the SDK and the query tool of Seed Agents all read it the same way. Every surface compiles it to the same DocumentFilter that the daemon's QueryDocuments call evaluates.

This page is the reference for the grammar, the filter and the attribute listings. Full-text search is a different tool. Search ranks words, and a query matches attributes exactly.

The grammar

key=value key="two words" key!=value key>=3 key<10 compare (typed: string, integer, boolean) key:text key~text contains, case-insensitive key^text starts with has:key missing:key presence in:<space uid> in:<hm:// url> scope to a space or a document subtree path:/specs path:/specs/* exact path, or a path prefix type:document|block|comment|space|contact result type (Explore only) AND OR NOT ( … ) boolean structure; adjacency is AND free words "quoted phrases" full-text terms (Explore only) view:table cols:title,status sort:status,-priority presentation directives

Keys are attribute names. Nested keys are dotted (address.city:Berlin). Values with spaces are quoted. A value made of digits compares as an integer and true/false as booleans, so priority>=3 and done=true are typed comparisons. Quotes do not change the type: priority="3" is also an integer comparison. Parsing is forgiving. A malformed part becomes a diagnostic and never an error, and a query serializes back to a stable string.

Examples:

Query

Finds

attributesSchema=hm://<uid>/types/person

every document typed by that schema, including the children a folder types through its childAttributesSchema

in:hm://<uid>/places kind=fortress

fortresses under one subtree

has:childAttributesSchema

every typed folder, anywhere

status="In Progress" AND priority>=3

open, important work

path:/specs/* AND NOT has:reviewedBy

specs nobody has reviewed

name^Draft OR summary:draft

drafts by name or by summary

Two parts of the grammar belong to Explore alone. type: chooses what kind of result to show, and bare words are full-text terms that Explore sends to search. The SDK compiler returns them separately as text terms. The CLI and the agents tool warn about them and ignore them, because QueryDocuments matches attributes only. The view:, cols: and sort: directives are presentation. Explore renders a table with those columns and sorts by sort:. The CLI and the agents tool ignore all three and take sorting from their own options (--sort, --sort-by and --reverse, or the sort input).

The filter it compiles to

QueryDocuments takes {filter?, sort?, pageSize?, pageToken?} and returns {documents, nextPageToken}, where each document comes with its full metadata. The filter is a tree:

Node

Shape

Meaning

and

{and: {filters: […]}}

all match; empty matches everything

or

{or: {filters: […]}}

any matches; empty matches nothing

not

{not: {filter}}

inverts

comparison

{comparison: {key, operator, value}}

operator is EQUAL, NOT_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL; value is {stringValue}, {intValue} or {boolValue}

exists, missing

{exists: {key}}, {missing: {key}}

presence

stringMatch

{stringMatch: {key, value, prefix?, caseSensitive?}}

contains, or starts-with with prefix

spaceMatch

{spaceMatch: {space}}

one account

pathMatch

{pathMatch: {path, prefix?}}

a path, or everything under it

urlMatch

{urlMatch: {url, prefix?}}

an hm:// URL, or its subtree

sort is a list of {key, descending?} for a user attribute or {attribute, descending?} for a built-in field: NAME, PATH, CREATE_TIME, UPDATE_TIME, ACTIVITY_TIME, COMMENT_COUNT. Pages are pageSize wide (the agents tool allows up to 100) and continue with the returned nextPageToken.

The grammar maps onto this directly. key=value is a comparison, key:text a stringMatch, key^text a stringMatch with prefix, has: an exists, in: a spaceMatch or urlMatch, path: a pathMatch. When a query runs inside one site, the compiler adds that site's urlMatch so results stay within it.

Over HTTP, QueryDocuments is a POST /api/QueryDocuments whose body is the request as protobuf JSON. The answer is protobuf JSON too, unlike the superjson envelope the other keys use. The Seed API page has the transport. The three query encodings matter only when you bypass the SDK.

Attribute listings

Before you query you usually want to know which keys exist and what values they take. Two calls answer that, and both are cheap:

    ListDocumentAttributeNames{account?, parentPath?, prefix?, recursive?, pageSize?, pageToken?} lists the attribute names in use, with the kinds seen for each (string, int, bool, object). parentPath lists the children of a nested object. recursive lists complete dotted scalar paths instead. account puts one space first.

    ListDocumentAttributeValues{path, kind, account?, prefix?, pageSize?, pageToken?} lists the distinct values one key has been seen with, for one scalar kind. account restricts to a space.

Working with it

In the Seed app

Explore is the query grammar with a UI. You type a query and get a list or a table. The table controls write the view:, cols: and sort: directives back into the string. Query blocks inside documents use the older Query request (a directory listing with sort and limit) and do not use this grammar. See Query.

CLI

seed-cli query <space> -w 'status="In Progress" AND priority>=3' --sort-by priority --reverse seed-cli query '*' -w 'attributesSchema=hm://<uid>/types/person' -l 50 seed-cli query <space> --filter '{"exists":{"key":"reviewedBy"}}' # a raw filter, ANDed with -w seed-cli query <space> -w '…' --page-token <token> # the next page seed-cli attributes <space> # keys in use, with kinds seed-cli attributes <space> --parent address --recursive seed-cli attributes <space> --values status --kind string --prefix In

query without -w, --filter or * is a plain directory listing (Children or AllDescendants) through the Query request. -q prints one hm:// id and name per line. --where, --filter and attributes are newer than the npm release of mid-September 2026 (0.2.9), so for now they need a CLI built from source. See Seed CLI.

SDK

The grammar lives in @seed-hypermedia/client/explore-query: parseExploreQuery(q) returns {ast, presentation, diagnostics}, serializeExploreQuery writes it back, quoteExploreValue quotes a value, and compileExploreQuery(parsed, {type: 'node'} | {type: 'site', url}) returns the filter (and the full-text terms it could not use). Then client.request('QueryDocuments', {filter, sort, pageSize, pageToken}), client.request('ListDocumentAttributeNames', …) and client.request('ListDocumentAttributeValues', …). Import the grammar from the client package, never from the app's shared UI package, which wraps it in React state. See SDK.

Web API

POST /api/QueryDocuments, POST /api/ListDocumentAttributeNames, POST /api/ListDocumentAttributeValues with protobuf-JSON bodies. GET /api/Query?… serves directory listings. See Seed API.

Agents

Seed Agents call query with {q?, filter?, sort?, pageSize?, pageToken?} and attributes with {key?, kind?, parent?, recursive?, account?, prefix?, pageSize?, pageToken?}, both through the call verb, with the same grammar and the same filter JSON. The tool descriptions tell the model to use search for words and query for attributes, and to run attributes first to learn the keys. An external agent uses the CLI lines above. See call and Using Seed from your own agent.

See also

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime