Filtering and Lookup
Beyond HEAD snapshots, WarmHub provides targeted query functions for specific lookups, filtered searches, history, and batch operations.
Get by Wref
Section titled “Get by Wref”Fetch things by their exact wrefs. Use this when you know the reference and want current state or a pinned version. MCP warmhub_thing_get takes a wrefs array at every cardinality and always answers with the { requested, items, missing } envelope described under Fetching many wrefs at once — a wref that does not exist or that you cannot read is listed in missing, not returned as an error.
wh thing view cavewh thing view cave --version 3{ "name": "warmhub_thing_get", "arguments": { "wrefs": ["cave"], "version": 3 }}The HTTP API does not currently mount a thing-by-wref lookup route. Use the CLI, SDK, or MCP surfaces for direct wref lookups.
Each result includes the thing’s core identity fields (name, wref, kind, version, active), its data payload, and a metadata envelope containing durableId, createdOn, and revisedOn. A thing has no primary shape: results carry declaredShapes and validatedShapes arrays instead of a single shape field — see Declared-shape vs. shape filters for what each field contains and when it is present. Assertions also include an aboutWref field. The committerWref field is present when the originating write recorded a committer identity. The createdBy field, when present, carries the immutable creator attribution recorded at the time the thing was first written. The revisedBy field, when present, identifies the author of the version that was actually returned — which is the current HEAD version on an unversioned read, or the pinned version when a specific version was requested (e.g. --version 3).
Note on data availability: the data payload is present on most reads but is not guaranteed in all cases. Specifically, retracted Readme and Agents Content records are returned without data when the caller lacks repo:write access — the response carries the identity fields and active: false, but data is omitted. This applies whether you are using wh thing view Readme --include-retracted, client.thing.get(...), or warmhub_thing_get. See inactive records on the Content shape page for the full policy.
Within data, fields typed as wref are returned differently depending on how the reference was stored. For versioned bindings, the @vN suffix is preserved (e.g. <durable-id>@v3). For identity-bound stored references, read projection strips the @vN suffix and returns a bare durable id. Named labels are not inlined into stored reference fields. Identity metadata fields such as committerWref are returned as bare durable ids. If you need a human-readable label for a ref, see resolving a durable id to a label — the CLI does it for you by default.
Fetching many wrefs at once
Section titled “Fetching many wrefs at once”Fetch many things or shapes by wref in a single call rather than looping one wref at a time. On the CLI, wh thing view is variadic — pass multiple wrefs or use --file to trigger batch mode. The TypeScript SDK exposes batch reads as client.thing.getMany, and MCP warmhub_thing_get takes the same wrefs array it takes for a single lookup.
The CLI enforces a 500-wref cap per call, applied after deduplication — so repeated wrefs across positional args, --file, and stdin are collapsed before the cap is checked. The MCP enforces a 500-wref cap on the raw wrefs array length as supplied, before any deduplication. The SDK accepts any number of wrefs and automatically chunks requests above the 500-wref backend transport cap, so you do not need to manually split large lists when using the SDK.
The CLI examples below use a couple of wref conventions: a thing’s wref is its bare name, so cave reads HEAD and cave@v3 pins to version 3; a shape is hub/shapes/<Name>. Examples on this page name dungeon rooms dungeon/<…> and observations obs/<…> so globs have something to match — a / in a name is hierarchy, not a shape prefix. --file=- is the standard Unix marker for “read newline-delimited input from stdin.”
# CLI — wrefs come from positional args, --file <path>, or piped stdin (any combination)wh thing view cave forest alicecat wrefs.txt | wh thing view # one wref per line, piped stdinwh thing view --file wrefs.txt --version 1 # newline-delimited file, pin all to @v1wh thing view --file=- < wrefs.txt --format jsonl # one JSON line per *deduped* requested wref// SDK — accepts any number of wrefs; auto-chunks above 500const result = await client.thing.getMany( 'acme', 'world', ['cave', 'forest', 'alice'], 1, // optional fallback version for unpinned wrefs { includeRetracted: true }, // optional — return retract versions when reading a historical version (otherwise retract versions are excluded))// result: { requested, items, missing }// MCP equivalent{ "name": "warmhub_thing_get", "arguments": { "orgName": "acme", "repoName": "world", "wrefs": ["cave", "forest", "alice"], "version": 1 }}Shared across CLI, SDK, and MCP:
- All three surfaces return
{ requested, items, missing }:requestedis a number — the count of input wrefs the call processed. The CLI unions positional +--file+ stdin inputs and dedupes them before the round-trip, so a wref supplied more than once counts only once toward bothrequestedand the 500-wref cap. The SDK and MCP forward thewrefsarray as-is, so duplicates produce duplicateitems/missingentries; on MCP, each entry in the supplied array counts toward the 500-wref cap.items[]carries the resolved entities — same fields as a single-thing read, including themetadataenvelope withdurableId,createdOn, andrevisedOn, thedeclaredShapesandvalidatedShapesarrays, and the optionalcreatedByandrevisedByattribution fields. Thedatapayload follows the same availability rules as single-item reads: it may be absent for retractedReadmeandAgentsrecords when the caller lacksrepo:writeaccess.missing[]isstring[]— wrefs that don’t exist or that the caller can’t read, returned instead of throwing.
- Missing entries are version-qualified when a top-level
version/--versionwas supplied and the wref didn’t already carry a version modifier (@vNor@HEAD). Per-wref pins always survive intact (no double-pinning). - The CLI enforces a 500-wref cap after dedupe; MCP enforces a 500-wref cap on the raw supplied array. The SDK auto-chunks above that cap.
CLI-only:
-
--versionimplies--include-retracted, so retract versions can be retrieved by their pinned id (mirrorswh thing view --version). Pass--include-retractedexplicitly when you want retract versions without a fallback--version. -
--format jsonlemits one row per deduped requested wref, in input order. (Inputs from positionals +--file+ stdin are unioned and deduped before the round-trip, so a wref supplied twice produces one row.) Use it for shell pipelines where you want to filter or fan out the result. Use--jsoninstead when you want the full{requested, items, missing}envelope as a single object.Each row is
{requested, found, wref, ...}.requestedis the input wref preserved verbatim (so canonical/cross-repo inputs are still identifiable on the consumer side),wrefis the local form for hits or the version-qualified form for misses, andfoundis the boolean. -
--liveis rejected (batch reads are one-shot — usewh thing view <wref> --livefor per-thing polling).
Query by Filters
Section titled “Query by Filters”Query a repo by combinable filters such as shape, kind, about target, and wref glob. Use this when you don’t have an exact wref. Filters are optional and they stack.
# By shape (matches things certified by Location, directly or through composition)wh thing query --shape Location
# By kindwh thing query --kind assertion
# By about target (assertions about a specific thing, collection, or shape)wh thing query --about cave
# Combined filterswh thing query --shape Observation --about cave --limit 20
# Resolve through collections — include assertions about collections containing the targetwh thing query --about cave --resolve-collectionswh thing query --shape Observation --about cave --resolve-collections
# With glob pattern (matches the thing's name)wh thing query --shape Location --match "dungeon/*"
# By affirmed-about target (assertions pinned to a specific version of a thing)wh thing query --affirmed-about cave@v3
# Count matching items (no pagination)wh thing query --shape Location --countwh thing query --kind assertion --about cave --countwh thing query --affirmed-about cave@v3 --countMCP note: Pass
orgNameandrepoNamein theargumentsobject alongside the fields shown below.
{ "name": "warmhub_thing_query", "arguments": { "shape": "Observation", "about": "cave", "kind": "assertion", "match": "obs/*", "includeRetracted": false, "limit": 50 }}Count via MCP:
{ "name": "warmhub_thing_query", "arguments": { "shape": "Observation", "about": "cave", "count": true }}The HTTP example below uses the public warmhub-data/us.congress.trades repo so you can copy-paste it directly; the CLI and MCP blocks above use generic Location/Observation names that you’d swap for your repo’s own shapes.
GET /api/repos/warmhub-data/us.congress.trades/query?shape=CongressTrade&kind=thing&match=20034954%2F*&limit=25Anonymous callers reading public repos are capped at limit=25 per page; authenticated callers can request up to limit=500. See Anonymous Pagination Caps.
All filter parameters are optional. Combine them to narrow results. The HTTP API does not currently mount a count-only route; use CLI/MCP count surfaces for count-only reads.
A count is not always exact. When a count is narrowed by
where predicates that still match a large share of
the repository, WarmHub may stop counting once it has enough to answer rather
than scanning every match. The structured response then sets hasMore: true,
and you should read the number as “at least this many” rather than as a total.
When hasMore is absent or false the count is exact, which is the ordinary
case — only a large, broadly matching where count is capped. The CLI shows this in both output modes: pretty output
appends + to a bounded count, and --json carries the hasMore field itself
when you need to branch on it.
Declared-shape vs. shape filters
Section titled “Declared-shape vs. shape filters”WarmHub exposes two distinct shape-based filters. They resolve differently at runtime and can be combined in a single query:
--shape <name>(CLI) /shape(SDK/MCP) — matches things whose current version is certified by the named shape: the shapes the thing declares plus every shape they compose, so a thing declared under a composite also matches each shape it composes. This is the filter accepted bywarmhub_thing_queryon MCP.--declared-shape <name>(CLI) /declaredShape(SDK only) — matches things that directly declare the shape you supply — the shapes the thing declares directly, not ones reached through composition.declaredShapeis not accepted bywarmhub_thing_query— the MCP tool does not admit this argument. Use the CLI or SDK when you need to filter strictly by declared shape.declaredShapecannot be combined with a--match/matchglob or with a token narrowed to a subset of refs; that combination is refused withVALIDATION_ERROR.
Because the two filters resolve against different data, they can return different result sets for the same shape name, and both can be supplied together to narrow results to items that satisfy both conditions.
An additional control lets you inspect the full certified shape closure on each returned item:
--include-validated-shapes(CLI) /includeValidatedShapes: true(SDK only) — requests that each returned item include avalidatedShapesarray in the response, listing every shape in the item’s certified closure. This does not change which items are returned; it enriches the response so you can inspect the full closure without a separate lookup. Not accepted bywarmhub_thing_query.
The result fields that correspond to these controls are:
| Field | Description |
|---|---|
declaredShapes | Version-pinned shape names such as Location@v1 for the shapes the item directly declares, [] when it declares none. Presence is not guaranteed — whether membership is projected depends on the repo and the read path, so the field may be absent from every row. Never require it |
validatedShapes | The full certified shape closure — the declared shapes plus their composed shapes — as version-pinned names such as Location@v1. On query and list rows it is present only when includeValidatedShapes is requested (CLI/SDK only); single and batch gets always carry it |
Treat both as optional when parsing: declaredShapes may be absent entirely depending on the repo and read path, and --include-validated-shapes cannot be combined with --count. When you need the certified closure deterministically, ask for it with --include-validated-shapes rather than relying on declaredShapes appearing.
# Filter by certificate membership (default shape filter)wh thing query --shape Location
# Filter strictly by declared shapewh thing list --declared-shape Location
# Combine both filterswh thing query --shape Location --declared-shape Location
# Include the full certified shape closure on each returned itemwh thing list --declared-shape Location --include-validated-shapes
# Same on querywh thing query --declared-shape Observation --include-validated-shapes// SDKconst result = await client.thing.query('acme', 'world', { declaredShape: 'Observation', includeValidatedShapes: true,})// Each item in result.items carries a validatedShapes arrayShapeless filter
Section titled “Shapeless filter”--shapeless (CLI) / shapeless: true (SDKs and MCP warmhub_thing_query, warmhub_thing_head) returns only things that no Shape certifies — never a Shape itself. Use it to find unstructured data that still needs a Shape. It works on query, list, and count, and with --include-retracted, where a retracted thing is judged on its current version.
shapeless is refused with VALIDATION_ERROR when combined with --shape or --declared-shape (the filters are mutually exclusive), with --since-repo-seq, or with any --match or a token narrowed to a subset of refs. --where needs a shape, so it is refused with FIELD_NOT_INDEXABLE. Search does not take shapeless: MCP warmhub_thing_search refuses the argument, and the backend search input silently drops it.
wh thing query --shapelesswh thing list --shapelesswh thing query --shapeless --countaffirmedAbout filter
Section titled “affirmedAbout filter”The affirmedAbout filter narrows results to assertions that were explicitly pinned to a specific version of a target thing at the time they were written. It is available across CLI (--affirmed-about), MCP (warmhub_thing_query.arguments.affirmedAbout), and the TypeScript SDK (FilterOptions.affirmedAbout).
# Assertions pinned to version 3 of cavewh thing query --affirmed-about cave@v3
# Combined with a shape filterwh thing query --shape Observation --affirmed-about cave@v3
# Combined with --matchwh thing query --affirmed-about cave@v3 --match "obs/*"
# Count onlywh thing query --affirmed-about cave@v3 --count{ "name": "warmhub_thing_query", "arguments": { "affirmedAbout": "cave@v3" }}// SDKconst result = await client.thing.query('acme', 'world', { affirmedAbout: 'cave@v3',})Pinned-only contract: the value passed to affirmedAbout must include an explicit version pin (e.g. cave@v3). Passing an unpinned wref is rejected.
affirmedAbout is also accepted by thing.search and thing.count.
Role filter
Section titled “Role filter”The CLI and SDK support an additional --role filter for wh thing query. Use it to narrow results to assertions that play a specific role within a collection — for example, only the from end of an Arc or the to end of a Bond. For a full explanation of collection shapes (Arc, Bond, and their roles), see Collections.
# Filter by rolewh thing query --kind assertion --about cave --resolve-collections --role fromwh thing query --kind assertion --about cave --resolve-collections --role towh thing query --kind assertion --about cave --resolve-collections --role endsValid role values are from, to, and ends. The --role filter requires both --about and --resolve-collections to be set — the CLI validates this combination and returns an error if either is missing.
The SDK exposes the same filter as role in FilterOptions:
const result = await client.thing.query('acme', 'world', { kind: 'assertion', about: 'cave', resolveCollections: true, role: 'from',})MCP surface: warmhub_thing_query does not accept a role argument. To filter by role on MCP, use warmhub_thing_about with resolveCollections: true and the role argument — it accepts from, to, and ends alongside the standard warmhub_thing_about parameters.
By kind
Section titled “By kind”The kind filter takes four values, and there is no split to remember:
kind=thingreturns things, collections included — a collection is a thing.kind=assertionis a membership filter — it narrows results to assertions, but every matching row still projectskind: "thing". Assertion identity is carried by theaboutWreffield (andaffirmedWrefswhere present) on each returned row, not by a distinct kind value on the wire.kind=collectionis accepted but deprecated. It narrows to things certified under the built-inCollectionShape, so a thing under your own Shape that composes a collection Shape is included and a Shape that merely reuses a built-in name is not. Those rows still projectkind: "thing".kind=shapereturns shapes.
Every row projects one of kind: "thing" or kind: "shape". A collection additionally carries a collections array saying which collection Shapes it satisfies, so you identify one by that array rather than by a different kind:
{ "wref": "team", "kind": "thing", "collections": [{ "shape": "Set@v2", "memberCount": 3 }] }To read only collections, filter by shape — --shape Set — or keep the rows whose collections array is present.
Pagination
Section titled “Pagination”Both wh thing query and wh thing about are paginated reads. The initial call returns only the first page. When more results exist, the response carries a nextCursor token: the SDK returns it as nextCursor on the Page result, while the CLI --json output nests it under page.nextCursor — it is not a top-level field (see below).
Page-size caps depend on authentication:
- Anonymous callers (unauthenticated requests to public repos) are capped at
limit=25per page. Requesting alimitabove 25 returnsSign in for larger pages. Anonymous callers are additionally limited to at most two pages. How crossing that boundary surfaces depends on which surface you call: the SDK and the CLI read layer raiseUNAUTHENTICATEDwithSign in to keep paging, while the public HTTP query endpoints (/head,/query,/about) rewrite that deny path to an opaque404withVary: Authorizationto keep repository existence private (see Anonymous Pagination Caps). Reducinglimitwill not resolve that boundary; you need to authenticate to continue paginating. - Authenticated callers can request up to
limit=500per page.
To retrieve subsequent pages manually, pass both --limit and --cursor <token> together — --cursor is rejected unless --limit is also supplied. To fetch all pages in a single command, use --all. To control page size, use --limit=<n>:
# Fetch the first page (authenticated callers: up to 500 items; anonymous: up to 25)wh thing query --shape Observation --limit 100
# Fetch the next page — both --limit and --cursor are required togetherwh thing query --shape Observation --limit 100 --cursor <token>
# Fetch all pages automaticallywh thing query --shape Observation --all
# Same pagination contract applies to thing aboutwh thing about cave --limit 100wh thing about cave --limit 100 --cursor <token>wh thing about cave --allThe cursor token is only available in structured output, and on the CLI it is nested inside the page envelope rather than at the top level. --json returns the full { items, page: { limit, count, hasMore, nextCursor } } shape, so read the cursor from page.nextCursor (and page.hasMore tells you whether another page exists). --format jsonl emits only the item rows with no page metadata, so use --json when you need the cursor. Pretty (human-readable) output does not print the token — it emits a hint that more results exist and suggests --all. To capture the cursor for a follow-up CLI call, use --json:
# Capture page.nextCursor from the --json envelopewh thing query --shape Observation --limit 100 --json# Then pass it to the next pagewh thing query --shape Observation --limit 100 --cursor <token from page.nextCursor above>Keep paginating until page.nextCursor is null to collect all results.
Incremental reads with --since-repo-seq (wh thing query and wh thing list): use this flag to re-read only the items whose current state changed since a known checkpoint, instead of re-fetching the whole result set on every poll. --since-repo-seq is not available on wh thing about. (For wh thing list incremental reads, see also HEAD queries.)
This returns changed records, not a list of writes. You get the current state of each item that changed since your checkpoint — one row per item, however many times it was written. Two writes to the same thing between polls give you one row with its latest state, not two rows. If you would rather be notified when writes happen than poll for them, set up a subscription that fires on matching writes instead.
By default an incremental read returns only active items, so a retraction shows up as the row simply no longer appearing. Pass --include-retracted and retracted items are returned too, so you can see which ones went away. One exception: --include-retracted cannot be combined with a --where field predicate — that composition is rejected on any read, incremental or not.
Running the read
- Starting fresh: pass
--since-repo-seq -1on your first call.-1is a bootstrap sentinel rather than a real repository sequence — it requests the complete current-state snapshot, not a walk of the repo’s write history. The server never returns-1. - Resuming from a checkpoint: on subsequent calls, pass the
repoSeqvalue you stored from the previous exhausted page as--since-repo-seq <seq>. - Use the same
--limiton every page of a single incremental read. Changing--limitmid-stream — between pages of the same poll — causes the incremental cursor to be rejected.
Storing the checkpoint
- Where to find it: when you have read the last page —
page.hasMoreis false and there are no further items — the--jsonenvelope carries a top-levelrepoSeqalongside thepageobject. Count-only reads (--count --since-repo-seq <n>) carry it too, so you can checkpoint from a count call — but pass--jsonthere as well, because the default human-readable output prints just the number and drops the checkpoint. - What it means:
repoSeqmarks where your scan started, not the end of the repo. It is fixed when the scan begins, so anything written while you were paging is not missed — your next poll picks it up. - When there is none: an abandoned or failed scan earns no new checkpoint. Keep the previous one and retry.
- Next poll: pass the stored
repoSeqas--since-repo-seqinstead of a cursor. This works for both manual page-by-page reads and--alldrains.
When an incremental read is refused
-
Four query forms cannot be read incrementally. These you can fix yourself by changing the query:
- a glob
--matchfilter --resolve-collections- a cross-repo
--abouttarget, such as--about wh:other-org/other-repo/name - an
--affirmed-aboutfilter (affirmedAboutis not supported for incremental reads; use a fullquerycall when you need that filter)
- a glob
-
Availability is the other cause, and no query change will help. Incremental reads are not available on every repo yet. If your query avoids all four forms above and the call is still refused, the feature is not available to you there — fall back to a full read for now and retry incrementally later.
Both causes return
INCREMENTAL_READ_UNAVAILABLE, and the error deliberately does not say which applied — so rule out the four query forms first. -
--shapelessis refused the same way, withVALIDATION_ERRORnaming incremental reads. -
--declared-shapeand--include-validated-shapesare refused separately. Pairing either with--since-repo-seqfails withVALIDATION_ERROR, notINCREMENTAL_READ_UNAVAILABLE, and the message names the request fieldsdeclaredShapeandincludeValidatedShapes— so this refusal does tell you exactly what to drop. -
You need read access to the whole repo. If your token is restricted to a subset of wrefs, incremental reads return
FORBIDDEN— even if the restriction is broad enough to cover everything you are querying. Use a token with unrestricted read access.
Here is a minimal two-step polling flow. The first call seeds the initial snapshot; the second call resumes from the stored checkpoint:
# Step 1 — bootstrap: read the complete current-state snapshotwh thing query --shape Observation --since-repo-seq -1 --limit 100 --json# Paginate (keeping --limit 100) until page.hasMore is false, then store the# top-level repoSeq from the final page's JSON envelope as your checkpoint.
# Step 2 — subsequent poll: fetch only the items that changed after the checkpointwh thing query --shape Observation --since-repo-seq <stored repoSeq> --limit 100 --json# Again paginate until page.hasMore is false, then update your stored repoSeq.From the SDK you do not have to run this loop yourself. client.thing.queryChanges(...) takes the same filters plus a required sinceRepoSeq and hands you back the changed items — retracted ones included — along with the checkpoint to store. Note that affirmedAbout is not supported for incremental reads; use a full query call when you need that filter. client.thing.headChanges(...) is the equivalent for list-style reads. Reach for the manual cursor recipe above only when you want to process each page as it arrives.
Key a local cache by item.metadata.durableId, since a rename carries its checkpoint wref and a retraction arrives as active: false.
One limit to plan around: neither helper captures every writer’s changes. Treat what they return as a best-effort delta, and when you need a complete copy of the data, periodically reconcile it against a full read rather than relying on the deltas alone.
For SDK pagination details, see the SDK read semantics reference. For HTTP pagination details, see the HTTP Queries reference.
Field-Value Predicates
Section titled “Field-Value Predicates”Shape, kind, about target, and wref glob narrow by a record’s identity. To filter on the values of fields inside a thing’s data — state, severity, a nested address.county — add where predicates.
where is supported across the typed read surfaces: the CLI (wh thing query, wh thing list), the SDK (client.thing.query, thing.head, thing.about, thing.count), and MCP (warmhub_thing_query, warmhub_thing_head, warmhub_thing_about). No HTTP read route parses where, and thing.search does not accept it — use thing.query for structured field filtering.
# Equalitywh thing query --shape Observation --where "status=active"
# Numeric comparison (also >, <, <=, !=)wh thing query --shape Observation --where "severity>=3"
# Prefix match on a string field (trailing * is optional)wh thing query --shape Location --where "region~north"
# Set membership — value is one of the listwh thing query --shape Location --where "biome in:[forest,desert,tundra]"
# Field existencewh thing query --shape Observation --where "resolvedAt?"
# Multiple predicates are ANDed (up to 8 per query)wh thing query --shape Observation --where "status=active" --where "severity>=3"Each --where flag is one predicate, written as a field path, an operator, and (except for exists) a value:
| CLI form | Operator | Matches |
|---|---|---|
field=value | eq | equal |
field!=value | ne | not equal |
field>value / field>=value | gt / gte | greater than (or equal to) |
field<value / field<=value | lt / lte | less than (or equal to) |
field~prefix | prefix | string or wref field starts with prefix |
field in:[a,b,c] | in | value is one of the list (1–1000 values) |
field? | exists | field is present |
The SDK and MCP take the same predicates as structured objects — { fieldPath, op, rhs }, where rhs is a scalar for the scalar operators, an array for in, and omitted for exists:
// SDKconst result = await client.thing.query('acme', 'world', { shape: 'Observation', where: [ { fieldPath: 'status', op: 'eq', rhs: 'active' }, { fieldPath: 'severity', op: 'gte', rhs: 3 }, ],})// MCP equivalent{ "name": "warmhub_thing_query", "arguments": { "shape": "Observation", "where": [ { "fieldPath": "status", "op": "eq", "rhs": "active" }, { "fieldPath": "severity", "op": "gte", "rhs": 3 } ] }}Notes:
-
whererequires a shape and can’t be combined with a globmatchorincludeRetracted. Each predicate resolves against a shape’s typed fields, so the query must set a shape (--shape/shape). Combiningwherewith a--match/matchglob pattern or with--include-retracted/includeRetractedreturns a field-index error rather than running. -
Predicates combine as AND, up to 8 per call. Field paths are dotted for nested fields (
address.county). -
wherefilters depend on a repo-level field-index declaration, not just the shape schema. A field must be both declared on the shape and declared for indexing in the repo before it can be used in awherepredicate. Three distinct error cases can arise:- Field absent from the shape — the field path does not exist on the shape at all.
- Field present on the shape but not declared for indexing in the repo (
FIELD_NOT_INDEXABLE/field-not-declared) — the shape carries the path, but the repo has no field-index declaration for it. The read fails. To fix this, runwh repo index pinto declare the field for indexing in the repo, then retry. - Field declared for indexing but the index marker is not yet ready (
FIELD_INDEX_UNAVAILABLE) — the declaration exists but the index has not finished building. Wait for the index to become available before querying.
Filterable scalar types are
string,number,boolean, and reference (wref) fields. Fields that hold objects or arrays aren’t filterable. A predicate on a field that isn’t available for filtering fails with an error naming the field, rather than silently returning no matches. -
Value typing follows the field’s declared type. The CLI keeps numeric-looking strings (
"42") and ISO date strings as strings and lets the field’s type classify them, but parses baretrue/falseas booleans — quote them (field="true") to match literal text. In the SDK and MCP, pass the scalar as the type you want (3versus"3"). -
Execution budget: typed
wherereads are execution-bounded. If a broad predicate or a deep page exhausts the query budget, the call fails withQUERY_TOO_EXPENSIVE. When you see that error, narrow the predicate (add more filters or tighten the value range) or request a shallower page. The same budget applies to count-only reads (--count/count: true).
Assertion-domain field filtering
Section titled “Assertion-domain field filtering”wh assertion list also accepts an equality shorthand for field filtering: --field data.path=value. This uses the same indexed field filtering path as wh thing query, and works for unscoped lists, target-scoped lists (--about), and --count. Only equality (=) is supported — for other operators, use wh thing query --shape <YourAssertionShape> --kind assertion --where.
# Equality filter on an assertion data fieldwh assertion list --field data.status=active
# Combined with a target scopewh assertion list --about cave --field data.severity=3
# Count onlywh assertion list --field data.status=active --countField paths must use the data. prefix. The filter is equality-only on this surface.
About Queries
Section titled “About Queries”List the assertions that target a specific thing, collection, or shape. Use this when you have a target’s wref and want the assertions made about it:
wh thing about cavewh thing about cave --shape Observationwh thing about cave --match "obs/*"
# Include assertions about collections containing the targetwh thing about cave --resolve-collectionswh assertion list --about cave remains available when you are already working in the assertion domain.
wh thing about does not accept --where. To filter assertions about a target by field values, pass where to the SDK’s client.thing.about or the MCP warmhub_thing_about tool.
wh thing about <wref> (and wh thing query --about) takes the target’s wref: a name such as cave for a thing or collection, or a shape wref such as hub/shapes/Location for a shape. The SDK’s client.thing.about accepts the same targets.
Collection Resolution
Section titled “Collection Resolution”wh thing about and --about first resolve the wref to a thing. The bare name cave and the @HEAD and @ALL selectors (cave@HEAD, cave@ALL) are identity-scoped: they match assertions about any version of that thing. A pinned wref such as cave@v3 matches only assertions filed against version 3, and --resolve-collections has no effect on it. wh thing history --about is the exception: it ignores the pin and treats cave@v3 like cave. By default, assertions about collections that contain the thing are not included. To also include assertions about collections (Arc, Bond, Set, List) that contain the target, add --resolve-collections:
wh thing about cave --resolve-collectionswh assertion list --about cave --resolve-collectionswh thing query --about cave --resolve-collectionswh thing history --about cave --resolve-collectionswh thing search "safe" --about cave --resolve-collectionsCollection resolution looks up current (HEAD) collection memberships. On wh thing search it works only in text mode: --mode vector refuses --about, and --mode hybrid prints a warning and ignores --resolve-collections rather than refusing it. The SDK and MCP search pass resolveCollections through in every mode.
Search pagination caveat: when combining search with --about or --resolve-collections, pages may be sparse — a page may contain fewer items than limit, or even zero items, while nextCursor is still non-null. Keep paginating until nextCursor is absent to collect all results.
The --depth flag retrieves child assertions about the returned assertions:
wh thing about cave --depth 2{ "name": "warmhub_thing_about", "arguments": { "wref": "cave", "shape": "Observation", "match": "obs/*", "includeRetracted": false, "depth": 2 }}The response includes a target object (the thing being queried) and an assertions array. Both the target object and each entry in assertions include a metadata envelope with durableId, createdOn, and revisedOn. With depth > 1, each assertion may include a children array.
Foreign assertions on target-side reads
Section titled “Foreign assertions on target-side reads”When the queried target belongs to the addressed repo, thing.about (CLI wh thing about, SDK client.thing.about, and MCP warmhub_thing_about) also aggregates readable foreign assertions: assertions stored in other repos that the caller has repo:read access to and that target the same thing. These foreign assertions appear inline in the assertions array with their canonical wh:org/repo/... wrefs, so you can distinguish them from assertions stored in the addressed repo.
Version History
Section titled “Version History”See every version of a thing, or filter history by shape or about target. Use this when you need how an item changed over time, not just its current state:
wh thing history cavewh thing history cave --limit 10Query history by shape or about filters (without a specific wref):
wh thing history --shape Observation --limit 20wh thing history --about cave --limit 5
# Include collection assertions in history filteringwh thing history --about cave --resolve-collections{ "name": "warmhub_thing_history", "arguments": { "wref": "cave", "limit": 10 }}At least one of wref, shape, or about is required. Each version entry includes fields such as: version, operation (add/revise/retract), active, createdAt, committerWref — the committer’s durable id when the originating write recorded one, omitted otherwise — revisedBy — the author of that specific version, when recorded — metadata.durableId and metadata.createdOn, declaredShapes and validatedShapes arrays (see Declared-shape vs. shape filters for field semantics), and aboutWref on assertion rows. This list is not exhaustive; responses may also include other fields depending on the item.
Cross-repo visibility
Section titled “Cross-repo visibility”Cross-repo wref lookups require effective repo:read permission on the target repo. Public repos are readable by anyone. For private repos, the behavior when a caller lacks that access depends on the read surface:
thing.get/ batch lookup — unreadable targets are folded intomissing[]rather than returned as an error.thing.search— unreadable results are folded into{ items: [] }to keep search streaming-friendly.thing.aboutand inboundthing.refs— invisible cross-repo names are answered as absent rather than as an error.- Other query routes — callers without cross-repo read access see an error.
See Getting Access for the precise rules.
Wref Resolution
Section titled “Wref Resolution”Resolve a wref — a thing’s name or a shape’s hub/shapes/<Name> — to its canonical identity. Use this when you have a local or version-relative reference and need its stable canonical form — CLI and SDK also return the resolved version’s data:
wh thing resolve cavewh thing resolve hub/shapes/Location{ "name": "warmhub_thing_get", "arguments": { "orgName": "myorg", "repoName": "myrepo", "wrefs": ["cave"], "includeRetracted": true }}- CLI
wh thing resolveand SDKclient.thing.resolve(...)return the same payload asthing.get. The default CLI render shows identity columns only; pass--jsonfor the full payload. - MCP resolves through
warmhub_thing_get, which answers with the{ requested, items, missing }envelope — read the resolved entity fromitems[0]. The identity fields —name,kind,active,version, and themetadataidentity/timestamp envelope — are a subset of what it returns, so no second call is needed to fetch data. A wref that cannot be resolved lands inmissingrather than raising an error. - A local wref needs
orgNameandrepoName; canonical (wh:myorg/myrepo/cave) and durable references self-route without them. PassincludeRetracted: trueto resolve a retracted target — it reads as not found otherwise.
Refs Queries
Section titled “Refs Queries”List the things that reference a specific target — a thing such as cave or a shape such as hub/shapes/Location. By default, wh thing refs returns inbound refs — other things whose data fields point to the target. Pass --outbound to instead list the refs stored on the target’s own data fields.
# All things that reference cave (inbound, default)wh thing refs cave
# All refs stored on cave's own data fields (outbound)wh thing refs cave --outbound
# Filter inbound refs to identity-bound refs onlywh thing refs cave --binding identity
# Filter outbound refs to versioned refs onlywh thing refs cave --outbound --binding versionedThe --binding flag accepts identity or versioned:
identity— returns only refs that are stored as identity-bound references (the@vNsuffix is stripped on read; see Get by Wref).versioned— returns only refs that are stored as versioned bindings (the@vNsuffix is preserved on read).
Omitting --binding returns all refs regardless of binding mode.
Search
Section titled “Search”Search things by text content. Three modes are available:
- text (default) — full-text search against thing names, shape names, and data fields. Supports pagination.
- vector — semantic similarity search using embeddings. No pagination.
- hybrid — runs text and vector searches in parallel, merges results with reciprocal rank fusion. No pagination.
# Full-text search (default mode)wh thing search "safe location" --shape Observation
# Semantic similarity searchwh thing search "places that are dangerous" --mode vector
# Hybrid search — combines text and vector resultswh thing search "policy" --mode hybrid --limit 10
# Filter by about target (not supported with --mode vector)wh thing search "safe" --about cave
# Filter by affirmed-about target (pinned version)wh thing search "safe" --affirmed-about cave@v3
# Resolve through collections (text mode only)wh thing search "safe" --about cave --resolve-collections
# Paginate through text results# NOTE: when using --about or --resolve-collections, pages may be sparse —# a page may return fewer items than --limit (or even zero) while nextCursor# is still present. Paginate until nextCursor is absent to collect all results.wh thing search "policy" --limit 50 --cursor <token>
# Fetch all pages automatically (text mode only)wh thing search "policy" --allMCP note: Pass
orgNameandrepoNamein theargumentsobject alongside the fields shown below.
{ "name": "warmhub_thing_search", "arguments": { "query": "safe location", "shape": "Observation", "mode": "hybrid", "limit": 10 }}Search is available via the CLI, SDK (client.thing.search()), and MCP (warmhub_thing_search) but is not currently exposed as an HTTP endpoint.
Anonymous search narrowing: anonymous callers (unauthenticated requests to public repos) are subject to narrowed paging on all thing.search calls: limit is capped at 25 and an omitted limit defaults to 25. Text-mode paging additionally stops after two pages — once that boundary is crossed, the backend returns UNAUTHENTICATED (Sign in to keep paging). Reducing limit will not resolve that error; you need to authenticate to continue paginating.
Reactive Mode
Section titled “Reactive Mode”Most CLI queries support --live for real-time updates:
wh thing query --shape Location --livewh thing history cave --livewh thing about cave --liveThis re-runs the query periodically and re-renders whenever the underlying data changes.