Skip to content

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.

TypeOrdered?Unique?# of MembersField Names
Arcyesyesexactly 2from, to
Bondnoyesexactly 2ends
Setnoyes1+members
Listyesno1+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.

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:

Terminal window
wh commit submit --add cave-route --shape Arc \
--data '{"from":"a","to":"b"}' \
-m "Create directed cave route" \
--repo myorg/world

The same operation as a full operation payload:

Terminal window
wh commit submit --ops '[
{
"operation": "add",
"name": "cave-route",
"shapes": ["Arc"],
"data": { "from": "a", "to": "b" }
}
]' -m "Create directed cave route" --repo myorg/world

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

Terminal window
wh collection create --type set --name cave-sites \
--members a,b \
-m "Create cave sites" \
--repo myorg/world
const 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.

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.

Terminal window
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-repo

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

Terminal window
wh collection create --type set --name wake-voters \
--source-repo your-org/source-repo \
--shape Voter \
--where county=Wake \
--repo your-org/your-repo

Selector-backed creation is set-only. Arc, Bond, and List require explicit members because their roles or arity are part of their meaning.

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:

Terminal window
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/world

You can also create the collection in one command and later assert about cave-route.

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.

Collections are revisable because they are ordinary named things. A new version of the collection records the new membership.

Terminal window
wh collection revise audit-scope \
--members a,c \
-m "Refresh audit scope" \
--repo myorg/world

Pinned collection wrefs act as revision guards. Revising audit-scope@v1 fails if the live collection is already at a different version.

Use the collection domain for collection operations:

Terminal window
wh collection members audit-scope --repo myorg/world
wh collection contains audit-scope a --repo myorg/world
wh collection diff old-scope new-scope --repo myorg/world
wh collection stats audit-scope --repo myorg/world

wh 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 Arc produces things that carry an Arc entry, and every collection read works on them.
  • A thing can be more than one collection. A Shape composing both Arc and Set produces two entries, and the collection reads take a shape argument 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 shape wref and memberCount. 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 composing Collection with no reference-typed field of its own is a collection with nothing to enumerate, and reads say so with memberCount: 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:

Terminal window
wh thing view audit-scope --repo myorg/world
wh thing view audit-scope --data-mode full --repo myorg/world

kind: "collection" is not a kind any read returns. It survives as a deprecated filter for a window — see Filtering by kind.

  • Set: members are deduplicated and sorted lexicographically. A member’s value is its stored reference, so a and a@v2 are 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.

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.