Collections
Collections turn groups of things into first-class things. Because collection members are untyped wrefs, any thing can be a member — including shapes and shaped things. A collection is an ordinary named thing with a built-in shape, version history, and wref. Because collections are things, they can be the target of assertions through the standard data.about field.
Collection Types
Section titled “Collection Types”| Type | Ordered? | Unique? | # of Members | Field Names |
|---|---|---|---|---|
| Arc | yes | yes | exactly 2 | from, to |
| Bond | no | yes | exactly 2 | ends |
| Set | no | yes | 1+ | members |
| List | yes | no | 1+ | items |
The built-in collection shapes (Arc, Bond, Set, List) are auto-created on first use. You create and revise collection instances as ordinary named things.
Creating Collections
Section titled “Creating Collections”Every collection needs a user-chosen name. Names are not generated from the members.
A collection is an ordinary Thing under a collection Shape, so any write surface creates one. An Arc keeps its endpoints in data.from and data.to:
wh commit submit --add cave-route --shape Arc \ --data '{"from":"a","to":"b"}' \ -m "Create directed cave route" \ --repo myorg/worldThe same operation as a full operation payload:
wh commit submit --ops '[ { "operation": "add", "name": "cave-route", "shapes": ["Arc"], "data": { "from": "a", "to": "b" } }]' -m "Create directed cave route" --repo myorg/worldFor a Set or List, the collection domain takes the members directly. SDK and MCP callers use the same model: add a named collection thing, then refer to that collection by name.
wh collection create --type set --name cave-sites \ --members a,b \ -m "Create cave sites" \ --repo myorg/worldconst receipt = await client.collection.create("myorg", "world", { type: "set", name: "cave-sites", members: ["a", "b"], message: "Create cave sites", eventRequestId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",})Both surfaces require an eventRequestId on create — a UUID that makes the
call idempotent, so reuse it when retrying rather than minting a new one — and
both return a receipt for the submitted operation. Over MCP the equivalent call
is warmhub_collection_create, taking the same type, name, members, and
eventRequestId arguments. See the SDK reference
for full type signatures and the MCP Tools Reference
for the tool’s argument schema.
Selector-Backed Sets
Section titled “Selector-Backed Sets”Set collections can be created from the same query mechanics used by thing queries. This is useful when the membership is “everything matching this selector” at a point in time.
Replace your-org/your-repo in the examples below with the slug of the repo you are writing into.
wh collection create --type set --name wake-voters \ --shape Voter \ --where county=Wake \ --where state=NC \ -m "Snapshot Wake County voters" \ --repo your-org/your-repoTo build a collection in one repo from things in another repo, pass the source repo. The stored members are pinned refs to the source things.
wh collection create --type set --name wake-voters \ --source-repo your-org/source-repo \ --shape Voter \ --where county=Wake \ --repo your-org/your-repoSelector-backed creation is set-only. Arc, Bond, and List require explicit members because their roles or arity are part of their meaning.
Asserting About Collections
Section titled “Asserting About Collections”data.about accepts a wref. It does not accept inline collection objects.
Create the collection first, then point the assertion at it. Distance here is a Shape that composes the built-in Assertion Shape:
wh commit submit --ops '[ { "operation": "add", "name": "cave-route", "shapes": ["Arc"], "data": { "from": "a", "to": "b" } }, { "operation": "add", "name": "cave-route-distance", "shapes": ["Distance"], "data": { "about": "cave-route", "value": 5 } }]' -m "Record route distance" --repo myorg/worldYou can also create the collection in one command and later assert about cave-route.
Version Pinning
Section titled “Version Pinning”Collection member fields are declared with binding: either, so the way you spell each member decides how it is stored. A bare member wref or @HEAD is stored as an identity reference that follows the Thing; an explicit @vN is stored pinned to that version:
{ "operation": "add", "name": "audit-scope", "shapes": ["Set"], "data": { "members": ["a", "b@HEAD", "c@v2"] }}Here a and b follow their Things and c stays at c@v2. Reads return the bare durable id for an identity member and id@vN for a pinned one. Pin a member with @vN when the collection must keep pointing at that exact version.
Revising Collections
Section titled “Revising Collections”Collections are revisable because they are ordinary named things. A new version of the collection records the new membership.
wh collection revise audit-scope \ --members a,c \ -m "Refresh audit scope" \ --repo myorg/worldPinned collection wrefs act as revision guards. Revising audit-scope@v1 fails if the live collection is already at a different version.
Reading Collections
Section titled “Reading Collections”Use the collection domain for collection operations:
wh collection members audit-scope --repo myorg/worldwh collection contains audit-scope a --repo myorg/worldwh collection diff old-scope new-scope --repo myorg/worldwh collection stats audit-scope --repo myorg/worldwh thing view still works because a collection is a thing — it reads as kind: "thing" and says what it is through a collections array:
{ "wref": "audit-scope", "kind": "thing", "declaredShapes": ["Set@v2"], "collections": [ { "shape": "Set@v2", "memberCount": 3 } ]}The array comes from the Shapes the version actually satisfies, not from its Shape’s name. Three consequences:
- Your own Shape can be a collection. A Shape that composes
Arcproduces things that carry anArcentry, and every collection read works on them. - A thing can be more than one collection. A Shape composing both
ArcandSetproduces two entries, and the collection reads take ashapeargument to say which to read. Asking for members without it, when there are two, is an error rather than a guess. - The layout comes from the Shape, not the row. An entry carries per-row facts only: the pinned
shapewref andmemberCount. Which fields hold members — single reference or array, in declared order — is that Shape version’s own contract, so you read it once from the Shape rather than on every row. A Shape composingCollectionwith no reference-typed field of its own is a collection with nothing to enumerate, and reads say so withmemberCount: 0.
For large Set/List bodies, thing reads may return the summary instead of the full member array. Use --data-mode full when you need the canonical JSON body:
wh thing view audit-scope --repo myorg/worldwh thing view audit-scope --data-mode full --repo myorg/worldkind: "collection" is not a kind any read returns. It survives as a deprecated filter for a window — see Filtering by kind.
Normalization Rules
Section titled “Normalization Rules”- Set: members are deduplicated and sorted lexicographically. A member’s value is its stored reference, so
aanda@v2are two members while the same reference written twice is one. - List: order is preserved and duplicates are kept.
- Arc: exactly two distinct endpoints; order is preserved as
from→to. - Bond: exactly two distinct endpoints; endpoints are sorted so input order does not matter.
Distinct means a distinct reference, for Arc and Bond as for Set. a → a@v2 is a legal Arc — relating a thing to one of its own versions is a real statement — while a → a and a@v2 → a@v2 are the same reference twice and are refused.
Modeling Guidance
Section titled “Modeling Guidance”Anchor an assertion about an Arc or Bond when the assertion describes a relationship that only makes sense with both endpoints. Use a single thing when the assertion is only about that thing.
Use Arc for directional relationships:
{ "operation": "add", "name": "alice-trusts-bob", "shapes": ["Arc"], "data": { "from": "alice", "to": "bob" }}Use Bond for symmetric binary relationships:
{ "operation": "add", "name": "alice-and-bob-are-teammates", "shapes": ["Bond"], "data": { "ends": ["alice", "bob"] }}Use Set when membership matters but order does not. Use List when order or duplicate membership is meaningful.