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 directivesKeys 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 |
|---|---|
| every document typed by that schema, including the children a folder types through its |
| fortresses under one subtree |
| every typed folder, anywhere |
| open, important work |
| specs nobody has reviewed |
| 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 |
|---|---|---|
|
| all match; empty matches everything |
|
| any matches; empty matches nothing |
|
| inverts |
|
|
|
|
| presence |
|
| contains, or starts-with with |
|
| one account |
|
| a path, or everything under it |
|
| an |
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 Inquery 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.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime