Skip to content

Read Semantics

SDK read methods share a few behavior patterns across client.thing, client.shape, and repository list calls. Use the generated WarmHubClient API reference for exact signatures.

The match option accepts a glob pattern, but what it filters against depends on which method you call:

  • client.thing and repository list calls — the glob filters against full wrefs such as Location/cave.
  • client.shape.list — the glob filters against bare shape names only (e.g. GameState), not full wrefs. A pattern like GameState/** will not match the GameState shape itself; use GameState or GameState* to match it.

Glob rules:

  • * matches a single path segment.
  • ** matches zero or more path segments. When used as a trailing /** suffix (e.g. Location/**), it matches descendants only and does not match the node at the pattern boundary itself. The bare ** pattern remains a match-all and does match the boundary node.
await client.thing.head('acme', 'world', {
match: 'Location/dungeon/*',
})
await client.thing.about('acme', 'world', 'Location/cave', {
match: 'Observation/**',
})

Reads with a match filter may lag briefly after a write while WarmHub updates read indexes. Subsequent reads after the index catches up will see the new state.

Where match filters by a record’s identity (its wref), the where option filters on the values of a thing’s declared fields. It is accepted by client.thing.query, client.thing.head, client.thing.about, and client.thing.count. Each predicate is a structured { fieldPath, op, rhs } object:

await client.thing.query('acme', 'world', {
shape: 'Observation',
where: [
{ fieldPath: 'status', op: 'eq', rhs: 'active' },
{ fieldPath: 'severity', op: 'gte', rhs: 3 },
],
})

op is one of eq, ne, gt, gte, lt, lte, prefix (scalar rhs), in (array rhs), or exists (omit rhs). Predicates are ANDed, up to 8 per call, and resolve against a shape’s typed fields — so the query must set a shape. For the full operator reference, value-typing rules, and constraints, see Field-Value Predicates.

WarmHub ships six built-in collection shapes — Arc, Bond, Pair, Triple, Set, and List — each of which groups related things into an ordered or unordered structure. For background on how collections are modeled, see Collections.

For things whose shape is one of those six, client.thing.get(...) and client.thing.getMany(...) elide large collection bodies by default. In that case the read returns a collection summary — a lightweight payload that includes the member count and a short preview of the first few members — rather than the full member list. This default keeps routine reads fast when a collection has grown large. Pass dataMode: "full" to force the complete body:

// Default — large collection bodies may be elided; summary data present
const result = await client.thing.get('acme', 'world', 'Set/favorites')
// Full body — complete data always present, no elision
const full = await client.thing.get('acme', 'world', 'Set/favorites', undefined, {
dataMode: 'full',
})

The same dataMode option is accepted by client.thing.getMany(...).

client.thing.getMany(...) accepts any number of wrefs and auto-chunks requests above the backend’s 500-wref limit.

The result preserves duplicates: each duplicate counts toward the requested count and produces a duplicate result or missing entry. Dedupe upstream when you want one row per unique wref.

The optional top-level version pins every requested wref that does not already include @vN or @HEAD. Per-wref pins remain intact.

Missing and inaccessible refs are reported in missing rather than throwing per item. With includeRetracted: true, retracted records can appear in items; without it, retracted refs land in missing.

Use chunkSize to tune how many wrefs are fetched together — lower values help avoid timeouts when individual records are large, while higher values (up to the 500 maximum) reduce round-trips when records are small. Use chunkConcurrency to fetch multiple chunks at the same time, which shortens wall-clock time when scanning large sets of refs; the merged result order is deterministic regardless of which chunks finish first.

Thing read results include a metadata object alongside the record’s data:

FieldWhat it tells you
durableIdA stable id for the thing that never changes across rename, revision, or retraction. See Durable Ids.
createdOnWhen the thing was first created, as a Unix timestamp in milliseconds. Stable across later revisions and renames.
revisedOnWhen the current version was created, as a Unix timestamp in milliseconds.

Because createdOn is the original creation time, you can order things by when they were first created from a single read — without walking each thing’s history. History rows carry durableId and createdOn.

A read result carries wrefs in four places, and each pins differently: the record’s own identity, wref-typed values stored in the thing’s data, the context fields that describe the write — aboutWref, committerWref, createdBy, and revisedBy — and, for assertion reads, the affirmedWrefs field that lists the targets the assertion affirms.

The thing’s own identity is reported as two fields:

FieldWhat it is
wrefAlways present and unpinned (Shape or Shape/name). It names the shape or shaped thing without locking to a version — use it to look up or show the target as it is now.
pinnedWrefThe same name pinned to the exact version you just read (Shape@vN or Shape/name@vN). Use it to record or re-fetch that precise version.

Wrefs stored in the thing’s data — fields the shape declares as wref-typed — point at shapes or shaped things, and they come back pinned (@vN). A bare or @HEAD value is pinned to the version current when the data was written; an explicit @vN preserves that version selection. Either way the stored reference keeps resolving to that exact version, which is what makes references reproducible. If the target is renamed, reads use its new name with the same pin. A target in the same repo comes back in local form (Shape/name@vN); a readable target in another repo comes back fully qualified (wh:org/repo/Shape/name@vN).

Context wrefs may each be absent from a given result, and they behave differently from data wref fields and from each other:

  • aboutWref is assertion-specific — it is only populated on assertion reads, and is omitted when the target’s repo has been deleted or hidden. Merely lacking access to a live target repo does not drop the field; you still get a usable string handle. When present it comes back pinned, in local form (Shape/name@vN) for a same-repo target and fully qualified (wh:org/repo/Shape/name@vN) for a readable target in another repo, carrying the version the assertion was created against.
  • committerWref is absent when no committer was recorded for the write. When present, it is an identity field emitted unpinned — local form for a same-repo committer (e.g. Agent/bot-1) and fully qualified (wh:org/repo/Agent/bot-1) for one in another repo, in both cases without a version suffix. A committer whose repo is hidden is suppressed rather than labelled.
  • createdBy and revisedBy are optional and may be absent when author attribution cannot be resolved. When present, they are identity wrefs emitted unpinned — local form when you are reading the identity’s own repo (e.g. Identity/<externalId>) and fully qualified otherwise (e.g. wh:warmhub/users/Identity/<externalId>), in both cases without a version suffix.

affirmedWrefs is assertion-specific and appears on assertion list items, full assertion reads, and history version rows (versions[].affirmedWrefs). It is an optional string[] that lists the targets the assertion affirms. Hidden affirmed targets are suppressed from the array. When all affirmed targets are hidden, the field is omitted entirely — the same as when no visible affirmed targets remain. The field is also absent on non-assertion reads.

The two are independent: pinnedWref is the version of this result; a pinned wref in data is the version of the shape or shaped thing it points to.

To compare or index a wref without caring whether it is pinned, strip the version with normalizeWref:

import { normalizeWref } from '@warmhub/sdk-ts'
const result = await client.thing.get('acme', 'world', 'Location/cave')
result.wref // 'Location/cave' (unpinned name)
result.pinnedWref // 'Location/cave@v3' (the version you read)
// `data` is typed `unknown` — narrow it to your shape to read its fields.
// A wref-typed field points at a shape or shaped thing, pinned when written:
const data = result.data as { region: string }
data.region // 'Region/north@v2'
normalizeWref(data.region) // 'Region/north'
// aboutWref is assertion-specific and optional — guard before using:
const assertion = await client.thing.get('acme', 'world', 'Observation/note-1')
if (assertion.aboutWref) {
normalizeWref(assertion.aboutWref) // e.g. 'Game/test'
}
// affirmedWrefs is assertion-specific and optional — guard before using.
// It appears on assertion list items, full assertion reads, and history
// version rows (versions[].affirmedWrefs):
if (assertion.affirmedWrefs) {
assertion.affirmedWrefs.map(normalizeWref) // e.g. ['Game/test', 'Player/hero']
}
// Identity fields are already unpinned — normalizeWref is a no-op but safe:
// committerWref, createdBy, and revisedBy are also optional:
if (result.committerWref) {
result.committerWref // 'Agent/bot-1' (no @vN suffix)
}

normalizeWref removes a trailing @vN, @HEAD, or @ALL. Reach for it wherever a wref may arrive pinned — for example results from client.thing.get, client.thing.resolve, client.thing.graph, and client.thing.getMany.

client.thing.refs(...) queries wref-typed fields.

  • Inbound refs find records whose wref fields point to the supplied wref.
  • Outbound refs find records the supplied record points to.
  • Inbound queries can be narrowed to a field path.

Use client.thing.about(...) for assertions about a thing, collection, or shape target. The method accepts any string wref — including shape wrefs such as 'Player' — and returns assertions whose target is that wref. For example, client.thing.about(org, repo, 'Player') returns assertions whose target is the Player shape. Use refs(...) when you specifically need field-level wref links.

thing.about(...) returns a page of assertions, each of which may include nested assertion children (replies or sub-assertions attached to a top-level assertion). For thing.about traversal, includeRetracted: true applies to top-level assertions and every populated level of nested assertion children.

client.thing.search(...) supports text, vector, and hybrid modes.

sinceRepoSeq is not accepted by any search mode. Search operates against the current indexed state of the repo; for checkpointed incremental reads use head, query, or count.

ModeBehavior
textFull-text search
vectorEmbedding-based semantic search
hybridCombined text and vector search

See the WarmHubClient API reference for the per-mode option set.

When searching with an assertion target or collection resolution, pages may be sparse. Keep paginating until nextCursor is absent.

Paginated methods follow a shared cursor pattern: the request accepts limit and cursor, and the response includes items plus an optional nextCursor.

const page1 = await client.repo.listPage('acme', { limit: 10 })
if (page1.nextCursor) {
const page2 = await client.repo.listPage('acme', {
cursor: page1.nextCursor,
limit: 10,
})
}

Some methods return a generic page shape and others return method-specific envelopes. The cursor pattern is the same; methods that do not paginate return their full result set in one call.

Choose the pagination form that matches the work:

FormUse it when
Page methodYou need page metadata, want to persist nextCursor, or control request scheduling.
*IterYou want lazy item-by-item processing or may stop early. No request occurs until iteration begins, and breaking does not fetch a later page.
*AllYou need an array and can name a safe max for memory use.

For “scan everything matching this filter” reads, prefer *Iter over a manual cursor loop:

for await (const item of client.thing.queryIter('acme', 'world', {
shape: 'Location',
limit: 500,
})) {
console.log(item.wref)
}

Iterator and materializer companions are available for cursor-backed reads under component, repo, shape, action, and thing, plus collection.members. They preserve the page method’s filters and accept an initial cursor when resuming a saved scan. History iterators yield version rows; other helpers yield their page’s items.

Every *All helper accepts max to prevent accidental unbounded reads. It rejects invalid maxima and throws only after observing an item beyond the cap, while still following empty intermediate pages that carry nextCursor:

const refs = await client.thing.refsAll('acme', 'world', 'Location/cave', {
direction: 'inbound',
max: 10_000,
})

repo.exploreIter and repo.exploreAll support browse mode only; exact slugs lookup is not pageable. Action-run helpers send since on the first request only because the returned cursor carries that time window.

All helpers preserve the underlying endpoint’s visibility and cursor lifetime. A saved cursor can become invalid when its query scope or backing data changes; on VALIDATION_ERROR / Invalid cursor, restart the page method or helper without cursor. Iterators do not add snapshot guarantees beyond the endpoint’s existing contract. Collection member helpers preserve that endpoint’s versioned-read behavior: when no version is supplied, they pin continuation requests to the collection version returned by the first page.

Tokenless clients reading public repositories have narrower paging on shared read procedures:

  • limit is capped at 25 items per page.
  • Omitted limit defaults to 25.
  • The page size is bound to the cursor. Follow-up requests must omit limit or pass the same value used to mint the cursor.
  • Anonymous pagination stops after 2 pages for repository list pages, shape history, thing HEAD, thing about, thing query, thing search, and wref-form thing history.
  • Shape- or about-filtered thing history allows only page 0 anonymously.

The SDK surfaces these boundaries as WarmHubError kinds such as VALIDATION_ERROR or UNAUTHENTICATED. REST query endpoints can collapse equivalent deny paths to opaque 404 responses to keep repository existence private.

Authenticated callers see no anonymous pagination narrowing.