Wrefs
A wref (WarmHub reference) is a human-readable address for any entity in WarmHub — shapes and the shaped things, assertions, and collections created under them.
Wrefs come in two name-based forms — local and canonical (below). A thing can also be addressed by its identity-based durable id, which names it regardless of its current name.
Name-Based Forms
Section titled “Name-Based Forms”Local Wrefs
Section titled “Local Wrefs”Within the current repo, use the short form:
hub/shapes/Location # shapecave # thingcave-safe # assertionround-1/state # thing with hierarchical nameA wref is the entity’s full name. A bare name such as cave is always the thing with that name, never a shape. A shape is addressed by its full name, hub/shapes/<Name>; positions that only take a shape (a shapes list, --shape) use the bare <Name>. Shape/name is not a wref. Thing names can contain / for hierarchical organization — see Naming as Navigation for how this hierarchy enables scoped queries, predictive navigation, and event routing.
Canonical Wrefs
Section titled “Canonical Wrefs”For cross-repo references, use the fully qualified form:
wh:org/repo/hub/shapes/Location # shape in another repowh:org/repo/cave # thing in another repoCanonical wrefs always start with wh: followed by org/repo/ and then the local wref.
What Wrefs Can Target
Section titled “What Wrefs Can Target”An untyped wref accepts either a shape (hub/shapes/Player) or a thing (alice). This is consistent across wref-typed data fields, collection members, assertion about targets, and committers. Local and canonical forms have the same target rules.
A typed constraint such as wref<Player> — declared as { "type": "wref", "shape": "Player" } — instead checks the target’s Shape set. It passes when the named Shape is in the target’s Shape set: a Shape the target declares, or one reached from those through composition. Any one match is enough; no Shape in the set is primary. A shapeless thing and a Shape definition never pass. A pinned target (alice@v2) is checked by that version’s Shape set; any other target by its current version’s. So alice, declaring Player (or a Shape that composes Player), satisfies wref<Player>, while the hub/shapes/Player Shape itself does not: matching the Shape’s own name never qualifies it.
Shape-targeting data and collection references use the same ref index as every other wref. Inbound ref queries expose the referring data fields and collections; a shape has no governing wref fields of its own, so outbound refs from the shape target are empty.
Visibility gate on resolution
Section titled “Visibility gate on resolution”The canonical-wref syntax is universal — any client can construct one. Resolving one through a read surface requires effective repo:read permission on the target repo. Public repos are readable by anyone. For private repos, callers without that access see an error — except cross-repo search and batch lookup, which fold unreadable results into { items: [] } or missing[] entries to keep search and batch streaming-friendly.
See Getting Access — Repository Visibility for the full read-permission rules.
References in a write
Section titled “References in a write”A write resolves every reference it carries: wref-typed data fields (typed or untyped), an assertion’s about, collection members, and relation ends (Arc from and to, Bond ends). A target your token cannot read is treated according to how you address it:
- By name (a bare name,
hub/shapes/X, orwh:org/repo/name): a target your token cannot read answers exactly as an absent one. A required reference fails with the sameNOT_FOUND, and an optionalwref?field coalesces tonullwith the same warning. - By durable id: the reference resolves whether or not your token can read the target Thing, in every position and in any repo. A durable id of a Thing that does not exist still fails with
NOT_FOUND, and a target in another private repo still needsrepo:readon that repo.
So a write by durable id can confirm that the Thing exists and whether it satisfies a typed wref<T> field, and reading your own record back shows the version that was pinned.
Targets of inbound refs and about filters
Section titled “Targets of inbound refs and about filters”Inbound refs (thing.refs, the default direction), the about filter on thing.query and thing.count, and the affirmedAbout filter on thing.query, thing.count and thing.search resolve the target you name the same way:
- By name: a target your token cannot read answers exactly as an absent one, with the same
NOT_FOUND. This covers every version modifier (@vN, including a version that does not exist, and@ALL) and a retracted target. - By durable id: the target resolves whether or not your token can read it, and the result lists only the referrers your token can read. A durable id of a Thing that does not exist still fails with
NOT_FOUND, and a target in another private repo still needsrepo:readon that repo.
Outbound refs and direct reads (thing.get, thing.history, thing.about) answer as absent for a target you cannot read, however you address it.
Version Modifiers
Section titled “Version Modifiers”Append @ followed by a version modifier to target a specific version. @vN is case-sensitive and N starts at 1; @HEAD and @ALL are case-insensitive:
| Modifier | Meaning | Example |
|---|---|---|
@HEAD | Current (latest) version only | cave@HEAD |
@vN | Pinned to version N | cave@v3 |
@ALL | All versions | cave@ALL |
When no modifier is specified (a “bare” wref), the default depends on the operation:
- Most reads resolve bare wrefs to the current version — equivalent to
@HEAD. - About queries (SDK
thing.about(), CLIwh thing about/wh assertion list --about, MCPwarmhub_thing_about) resolve bare wrefs to@ALL— returning assertions across all versions of the target. - Writes (commit operations) resolve bare wrefs to
@HEAD— the operation targets the current version.
The @ALL modifier is rejected on write paths.
Pinned-ref requirement for affirmation inputs: Two inputs on the write path require an explicit pinned name@vN spelling and reject both bare and @HEAD refs:
affirmedTargetson an assertion — the list of versions of the assertion’sabouttarget that the assertion explicitly affirms. Every entry must resolve to the same identity as the assertion’saboutfield; supplying a pinned ref for a different thing is rejected with a validation error.reaffirmadd/remove— adding or removing a version from an assertion’s affirmed-targets set. The supplied pinned ref must resolve to the same identity as the assertion’saboutfield; a ref pointing at a different thing is rejected with a validation error.
If you supply a bare or @HEAD ref to either of these inputs, the operation is rejected with a validation error. Use name@vN (e.g. cave@v3).
affirmedAbout is a read-side filter on thing.query, thing.search, and thing.count. It restricts results to assertions that affirm a specific pinned version of their about target. Like the write-path inputs above, it requires an explicit pinned name@vN spelling — bare and @HEAD refs are rejected. It reaches those surfaces through the CLI (--affirmed-about on wh thing query and wh thing search), MCP, and the TypeScript SDK; wh thing about, wh assertion list, and the public REST query route do not accept it. See Filtering — affirmedAbout filter.
Shapes use the same modifiers. hub/shapes/Player and hub/shapes/Player@HEAD select the current shape definition, while hub/shapes/Player@v3 selects that exact historical version. On write paths, a floating reference to a retracted shape fails, but an existing pinned version remains valid. Renaming a shape invalidates every old name-based spelling — including hub/shapes/OldName@vN; the new name resolves both the current and historical versions (hub/shapes/NewName and hub/shapes/NewName@vN). Identity-based references already stored in data, collections, and assertions survive the rename: reads return them as durable ids, and a durable id does not change when its target is renamed. The target’s current name comes from label resolution.
About queries match the supplied target identity by default. A bare wref broadens version matching for that target identity, but it does not automatically include assertions about Arc, Bond, Set, or List collection things that contain the target. Use resolveCollections:true (MCP/SDK/HTTP) or --resolve-collections (CLI) when collection-member assertions should be included for identity-scoped inputs. Current-state about filters keep pinned @vN inputs version-exact and do not expand collection members. History about filters are different: thing history --about <name>@vN resolves the target identity, so history can still match assertions across versions of that identity and, with collection resolution, assertions about collections containing it.
Durable Ids
Section titled “Durable Ids”A wref is readable, but it is mutable: renaming a thing — or its org or repo — rewrites the wref. A consumer that stores wrefs as long-term keys sees a rename as a delete followed by a brand-new record.
A durable id avoids that. It is an opaque token that names a thing by its identity rather than its name, and it never changes — across renames, revisions, and retraction. Thing read results include one in their metadata.durableId field (see Read Result Metadata).
Durable ids have three useful properties:
- Stable — the same thing always has the same durable id, so it is safe to use as a long-lived key for mirroring, deduplication, or joins.
- Self-routing — a durable id carries its own location, so read surfaces can resolve it without being told its org or repo.
- Self-verifying — a corrupted or truncated durable id is rejected rather than resolving to the wrong thing.
A durable id is accepted anywhere a wref is accepted as input, and it carries the same version modifiers:
<durable-id> # current version<durable-id>@v3 # pinned to version 3<durable-id>@ALL # all versions (reads only)As with any wref, @ALL is read-only and is rejected on write paths.
Resolving a durable id is subject to the same visibility gate as a canonical wref: you can only read ids for repos you have access to. A reference in a write follows References in a write. See Durable ids on wh thing reads for the CLI surface.
Resolving a durable id to a label
Section titled “Resolving a durable id to a label”Every reference a read returns — wref-typed fields inside data, plus aboutWref, affirmedWrefs, committerWref, createdBy, and revisedBy — comes back as a durable id, not a name. A durable id reads back the same for every caller: one thing at one version is always the same value, whatever you have access to and whatever the target has since been renamed to. Names come from label resolution, which is per-request and subject to the visibility gate above.
How you get labels depends on the surface:
- TypeScript SDK — off by default. Construct the client with
decorateResponses: trueand each response gains an additivedecorationsrecord mapping every bare durable id it references to that target’s current canonical wref. It costs one extra batched lookup per decorated response. - CLI — on by default.
whresolves labels for you; pass--no-decorateto get the undecorated body, or setdecorateon the profile to change the default. - MCP — responses carry the same
decorationsmap when the result references any durable ids. - HTTP API — no automatic resolution, and no repo-less read. Every public read route is repository-scoped (
/api/repos/:orgName/:repoName/head,/query,/about/:wref), and the HTTP API does not mount a thing-by-wref lookup route. To get labels over HTTP you resolve them yourself against a repo-scoped route, or read through a surface that decorates — the SDK, CLI, or MCP.
An id you cannot read simply gets no entry, so a missing label means “not readable or not found” — never a rename you missed. Resolution only adds labels alongside the response; the ids in it are never rewritten.
Why two ids from the same repo look alike
Section titled “Why two ids from the same repo look alike”A durable id is opaque — you never take it apart yourself — but it is structured. In order, it encodes the repo the thing lives in, when the thing was created, what makes it that specific thing, and a trailing checksum — the checksum is what lets a corrupted or truncated id be rejected instead of resolving to the wrong thing. Because the repo comes first and every thing in a repo shares it, the whole leading run is identical across a repo. Things created close together in time share the next part too, since it is time-ordered. The characters that distinguish two things sit in the middle.
Two real ids from the same repo, split into their four parts (spaces added — the real token has none):
040SX6ADHPV74E534RBBEHSHXHDG 37N8GKTHE WZNN6PZQBHJ4P3V1 DHS04PG ← p000197040SX6ADHPV74E534RBBEHSHXHDG 37N8GKHGM ZFZKGQZFKM3MEB7Z YRZ1CWG ← b001230└───────────repo───────────┘ └─when──┘ └────unique────┘ └─crc─┘Here the first 34 characters match — same repo, and created closely enough in time to share the start of when too. The instinct from git short-hashes is to compare or truncate from the front; for a durable id that is the run that does not vary. The part unique to each thing sits in the middle, so compare from there.
The wh CLI does this for you: it tints the four parts in different colors — dimming the shared repo and checksum, and brightening the unique middle — so near-identical ids are easy to tell apart at a glance. Color needs a truecolor terminal and is suppressed when NO_COLOR is set or output is not a terminal; stripping the color always leaves the token byte-for-byte intact, so the styling is only ever a reading aid, never part of the value.
The web app’s thing Detail tab shows the durable id with the same emphasis and a button that copies the bare id. See Web UI overview.
For the full picture of what a rename does to the references pointing at a thing — which follow it and which break — see Retract, Rename & Schema Changes.
Linking to a Thing
Section titled “Linking to a Thing”wref.ai is WarmHub’s short-link host. It is operated by WarmHub and redirects only to the WarmHub web app at app.warmhub.ai. To turn a canonical wref into a shareable link, drop the wh: prefix and prepend https://wref.ai/:
wh:warmhub-data/us.congress/p000197 → https://wref.ai/warmhub-data/us.congress/p000197The same rule gives org and repo links:
https://wref.ai/warmhub-data # org pagehttps://wref.ai/warmhub-data/us.congress # repo pageA name that contains / keeps its slashes in the link. Percent-encode any other character that is not URL-safe, as in any URL path.
Pinned versions
Section titled “Pinned versions”Append @vN to the path to link to a specific version, the URL form of the @vN version modifier:
https://wref.ai/warmhub-data/us.congress/p000197@v2Only @vN and @HEAD are meaningful in a link. @ALL names no single version to show, so an @ALL link shows the not-found page. A version is read only from the path: a v query parameter is ignored.
Durable id links
Section titled “Durable id links”A durable id can stand in for the whole org/repo/name path. Because a durable id survives renames, the link keeps working after the Thing is renamed:
https://wref.ai/040SX6ADHPV74E534RBBEHSHXHDG37N8GKTHEWZNN6PZQBHJ4P3V1DHS04PGhttps://wref.ai/040SX6ADHPV74E534RBBEHSHXHDG37N8GKTHEWZNN6PZQBHJ4P3V1DHS04PG@v2A durable id link to a retracted Thing shows the not-found page rather than whatever now holds the old name.
Rendering a field as markdown
Section titled “Rendering a field as markdown”Add ?md=<field> to render a string field of the Thing’s data as a formatted markdown document:
https://wref.ai/warmhub-data/us.congress/Readme?md=contentmd accepts a comma-separated list (?md=summary,notes) and may be repeated. Any non-empty string field can be named; a field that is missing or is not a string is shown in the standard view. md is the only query parameter a wref.ai link keeps — every other parameter is dropped.
Access control
Section titled “Access control”A wref.ai link grants nothing: it opens the same page the web app would, under the viewer’s own access. A Thing in a public repo is visible to anyone. A link the viewer cannot read shows the same not-found page as a link to a Thing that does not exist, so a link never reveals whether a private Thing exists.
Path Segment Rules
Section titled “Path Segment Rules”Wref path segments have the following constraints:
- Segments must be non-empty — no
//allowed - Names cannot start or end with
/ - Segments cannot contain:
?,#,@,:,$, or whitespace - Collection thing names cannot contain
+; it is reserved for legacy collection namespace fencing
Retired Batch Tokens
Section titled “Retired Batch Tokens”$N and #N commit-token syntax is no longer supported. Create things with explicit deterministic names, then reference those names directly:
[ { "operation": "add", "name": "loc-2026-07-08-001", "shapes": ["Location"], "data": { "x": 1 } }, { "operation": "add", "name": "loc-2026-07-08-001-safe", "shapes": ["Observation"], "data": { "about": "loc-2026-07-08-001", "safe": true } }]Good explicit names usually come from source-system IDs, stable slugs, content hashes, or run-scoped sequence numbers.
Resolving Wrefs
Section titled “Resolving Wrefs”To resolve a wref to its canonical identity:
wh thing resolve caveVia MCP:
{ "name": "warmhub_thing_get", "arguments": { "orgName": "myorg", "repoName": "myrepo", "wrefs": ["cave"], "includeRetracted": true }}A local wref like cave carries no repo, so MCP needs orgName and repoName alongside it — warmhub_thing_get takes a wrefs array, so a single lookup is a one-entry array. Canonical (wh:myorg/myrepo/cave) and durable references self-route and need no locator. includeRetracted: true is what lets a retracted target resolve; without it a retracted thing reads as not found.
MCP warmhub_thing_get answers with a { requested, items, missing } envelope; items[0] holds the resolved entity, and a wref that does not resolve is listed in missing rather than raising an error. Each item carries the identifying fields (name, kind, active, version, shapeName, and the metadata identity/timestamp envelope) as a subset of the full payload. For a shape target, shapeName is that target shape’s own name. SDK client.thing.resolve(...) returns the full thing.get payload; wh thing resolve shows the identifying fields by default and the full payload with --json. See Wref Resolution.
Optional Wref Fields
Section titled “Optional Wref Fields”A shape field typed as an optional wref (wref?) may point at another entity but is not required to. WarmHub resolves the value when the commit is applied, and how a missing target is handled depends on whether the field is required:
- Required wref fields are the referential-integrity mechanism. If the target does not resolve, the operation fails with
NOT_FOUNDand stores no version for that operation. Other operations in the submission retain the write pipeline’s normal per-operation result semantics. - Optional wref fields coalesce a
thing_absenttarget tonullinstead of failing the operation. The operation succeeds,nullis stored in place of the reference, and acoalescedWrefswarning rides back on the operation result so the substitution is observable.
Coalescing is lossy: the original wref string is discarded and only null is stored — there is no pending or re-resolve state. Write the field again once the target exists.
Only the resolver outcome thing_absent coalesces: the target’s repo and shape exist and are active, but the named shaped thing does not exist. (A retracted target is not “absent” — it still hard-fails.) Every other unresolved reference on an optional field also fails that operation:
- a pinned target (
name@vN) whose version does not exist, - a shape mismatch — the wref points at a different shape than the field requires (
SHAPE_MISMATCH), - a missing repo or shape (local or canonical),
- a forward reference to a target created later in the same commit — this fails with
VALIDATION_ERROR; reorder your operations so the target precedes the reference, or create both entities with explicit deterministic names and reference those names directly, - direct elements of a wref array (
wref[]) —nullcannot stand in for an array element, so these hard-fail. (An optional wref field on objects reached through an array — e.g.rows[].ref— is a scalar leaf and does still coalesce.)
Examples
Section titled “Examples”# Shape wrefhub/shapes/Location
# Thing wrefcave
# Pinned to version 3cave@v3
# Canonical (cross-repo)wh:warmhub-data/us.congress/p000197
# Hierarchical thing nameround-1/turn-5/state
# Named collectioncave-route
# Deterministic generated nameloc-2026-07-08-001