Skip to content

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.

Within the current repo, use the short form:

hub/shapes/Location # shape
cave # thing
cave-safe # assertion
round-1/state # thing with hierarchical name

A 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.

For cross-repo references, use the fully qualified form:

wh:org/repo/hub/shapes/Location # shape in another repo
wh:org/repo/cave # thing in another repo

Canonical wrefs always start with wh: followed by org/repo/ and then the local wref.

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.

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.

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, or wh:org/repo/name): a target your token cannot read answers exactly as an absent one. A required reference fails with the same NOT_FOUND, and an optional wref? field coalesces to null with 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 needs repo:read on 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.

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 needs repo:read on 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.

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:

ModifierMeaningExample
@HEADCurrent (latest) version onlycave@HEAD
@vNPinned to version Ncave@v3
@ALLAll versionscave@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(), CLI wh thing about / wh assertion list --about, MCP warmhub_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:

  • affirmedTargets on an assertion — the list of versions of the assertion’s about target that the assertion explicitly affirms. Every entry must resolve to the same identity as the assertion’s about field; supplying a pinned ref for a different thing is rejected with a validation error.
  • reaffirm add/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’s about field; 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.

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.

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: true and each response gains an additive decorations record 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. wh resolves labels for you; pass --no-decorate to get the undecorated body, or set decorate on the profile to change the default.
  • MCP — responses carry the same decorations map 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.

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 ← p000197
040SX6ADHPV74E534RBBEHSHXHDG 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.

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/p000197

The same rule gives org and repo links:

https://wref.ai/warmhub-data # org page
https://wref.ai/warmhub-data/us.congress # repo page

A name that contains / keeps its slashes in the link. Percent-encode any other character that is not URL-safe, as in any URL path.

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@v2

Only @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.

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/040SX6ADHPV74E534RBBEHSHXHDG37N8GKTHEWZNN6PZQBHJ4P3V1DHS04PG
https://wref.ai/040SX6ADHPV74E534RBBEHSHXHDG37N8GKTHEWZNN6PZQBHJ4P3V1DHS04PG@v2

A durable id link to a retracted Thing shows the not-found page rather than whatever now holds the old name.

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=content

md 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.

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.

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

$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.

To resolve a wref to its canonical identity:

Terminal window
wh thing resolve cave

Via 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.

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_FOUND and 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_absent target to null instead of failing the operation. The operation succeeds, null is stored in place of the reference, and a coalescedWrefs warning 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[]) — null cannot 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.)
Terminal window
# Shape wref
hub/shapes/Location
# Thing wref
cave
# Pinned to version 3
cave@v3
# Canonical (cross-repo)
wh:warmhub-data/us.congress/p000197
# Hierarchical thing name
round-1/turn-5/state
# Named collection
cave-route
# Deterministic generated name
loc-2026-07-08-001