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:

Location # shape
Location/cave # thing
Observation/cave-safe # assertion
GameState/round-1/state # thing with hierarchical name

The first segment is always the shape name. Everything after the first / is the thing name. 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/Location # shape in another repo
wh:org/repo/Location/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 (Player) or a shaped thing (Player/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 resolved type. Player/alice resolves as a shaped thing whose type is Player, so it satisfies the constraint. The Player shape itself has no governing resolved type, so it does not satisfy wref<Player>; 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.

Append @ followed by a version modifier to target a specific version:

ModifierMeaningExample
@HEADCurrent (latest) version onlyLocation/cave@HEAD
@vNPinned to version NLocation/cave@v3
@ALLAll versionsLocation/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(), 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.

Shapes use the same modifiers. Player and Player@HEAD select the current shape definition, while 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 OldName@vN; the new name resolves both the current and historical versions (NewName and NewName@vN). Identity-based references already stored in data, collections, and assertions follow the rename and rehydrate with the new spelling.

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 Shape/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. See Durable ids on wh thing reads for the CLI surface.

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 ← Legislator/p000197
040SX6ADHPV74E534RBBEHSHXHDG 37N8GKHGM ZFZKGQZFKM3MEB7Z YRZ1CWG ← Legislator/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.

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 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", "kind": "thing", "name": "Location/loc-2026-07-08-001", "data": { "x": 1 } },
{ "operation": "add", "kind": "assertion", "name": "Observation/loc-2026-07-08-001-safe",
"about": "Location/loc-2026-07-08-001", "data": { "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 Location/cave

Via MCP:

{ "name": "warmhub_wref_resolve", "arguments": { "wref": "Location/cave" } }

MCP warmhub_wref_resolve returns identifying fields (name, kind, active, version, and shapeName) plus the metadata identity/timestamp envelope. 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 same 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 (Shape/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-only wref
Location
# Thing wref
Location/cave
# Pinned to version 3
Location/cave@v3
# Canonical (cross-repo)
wh:warmhub-data/us.congress/Legislator/p000197
# Hierarchical thing name
GameState/round-1/turn-5/state
# Named collection
Arc/cave-route
# Deterministic generated name
Location/loc-2026-07-08-001