Shapes
A shape defines the data structure for things and assertions. Every thing and assertion belongs to a shape, and its data is validated against the shape’s field definitions.
Creating a Shape
Section titled “Creating a Shape”Use the CLI or a commit operation to create a shape:
wh shape create Location --fields '{"x": "number", "y": "number", "label": "string"}'Shapes can also be created as commit operations.
Field Types
Section titled “Field Types”| Type | Description | Example value |
|---|---|---|
string | Text value | "hello" |
number | Numeric value | 42 |
boolean | True/false | true |
wref | Reference to another thing | "Location/cave" |
array | List of values (see Array Fields) | ["a", "b"] |
Optional Fields
Section titled “Optional Fields”There are two equivalent syntaxes for optional fields — append ? to either the field name or the type:
{ "x": "number", "y": "number", "label?": "string" }{ "x": "number", "y": "number", "label": "string?" }Both forms are interchangeable. Things created under this shape can omit label without validation errors. When an optional field is omitted, both null and undefined values are accepted during validation.
Array Fields
Section titled “Array Fields”Wrap the type in an array to define a list field:
{ "tags": ["string"], "scores": ["number"] }Typed array objects provide an alternative syntax with constraints and descriptions:
{ "tags": { "type": "array", "items": "string", "minItems": 1, "maxItems": 10 }, "scores": { "type": "array", "items": "number", "description": "Player scores" }}See Field constraints for the full set of array constraint keys.
Nested Objects
Section titled “Nested Objects”Use a plain sub-object to define nested structure:
{ "position": { "x": "number", "y": "number" }, "label": "string"}Nested objects are defined as plain sub-objects — do not wrap them in {"fields": ...}. The fields wrapper is only used at the top level of a shape definition, not inside nested types.
Typed Field Objects
Section titled “Typed Field Objects”Instead of a bare type string, you can use a typed field object to attach a description or constraints.
A type spec is any valid field type declaration. It can be a bare type string like "number", a typed field object like { "type": "number", "description": "Height" }, or an array shorthand like ["string"].
{ "x": { "type": "number", "description": "Horizontal position" }, "y": "number" }A typed field object must satisfy two rules:
- It has a
typekey (required) set to one of:"string","number","boolean","wref", or"array". Append?to mark the field as optional, e.g.{ "type": "string?", "description": "..." }. - All other keys must be recognised type-spec keys:
description, or any field constraint valid for the field’s type.
Typed field vs nested object — quick reference:
| Object | Interpretation | Why |
|---|---|---|
{ "type": "number", "description": "Height" } | Typed field | All keys are recognised type spec keys |
{ "type": "string", "value": "number" } | Nested object | "value" is not a recognised type spec key |
Any object with unrecognised keys is treated as a nested object, not a typed field. For example, { "type": "string", "value": "number" } is a nested object with two sub-fields because "value" is not a recognised constraint key.
Field Constraints
Section titled “Field Constraints”Typed field objects can include constraint keys to validate data at commit time. Constraints are validated when the shape is created and enforced when data is committed.
String constraints (type: "string"):
minLength(non-negative integer) — minimum string lengthmaxLength(non-negative integer) — maximum string length; must be ≥minLengthpattern(string) — regular expression the value must matchenum(non-empty string[]) — list of allowed values; must contain at least one entry
Number constraints (type: "number"):
minimum(number) — minimum value (inclusive)maximum(number) — maximum value (inclusive); must be ≥minimuminteger(boolean) — whentrue, value must be a whole number
Wref constraints (type: "wref"):
shape(string) — the referenced thing must belong to this shape
Array constraints (type: "array"):
items(type spec, required) — the element type (any valid type spec: primitive, typed field object, nested object, or array)minItems(non-negative integer) — minimum number of elementsmaxItems(non-negative integer) — maximum number of elements; must be ≥minItems
{ "status": { "type": "string", "enum": ["active", "inactive", "pending"] }, "score": { "type": "number", "minimum": 0, "maximum": 100, "integer": true }, "owner": { "type": "wref", "shape": "Player", "description": "The owning player" }, "tags": { "type": "array", "items": "string", "minItems": 1, "maxItems": 10 }}Shape Descriptions
Section titled “Shape Descriptions”Shapes can have an optional top-level description that documents the shape’s purpose:
wh shape create Location --fields '{"x":"number","y":"number"}' --description "A point in 2D space"The description is returned by wh shape view, wh repo describe, and the warmhub_repo_describe MCP tool.
CLI --fields Examples
Section titled “CLI --fields Examples”All field types work in --fields JSON:
# Array fieldswh shape create Tags --fields '{"labels":["string"],"scores":["number"]}'
# Optional fields (both syntaxes)wh shape create Review --fields '{"score":"number","reason?":"string"}'wh shape create Review --fields '{"score":"number","reason":"string?"}'
# Nested objectswh shape create Player --fields '{"name":"string","position":{"x":"number","y":"number"}}'
# Typed field objects with descriptionswh shape create Sensor --fields '{"value":{"type":"number","description":"Current reading"},"unit":"string"}'
# Constraintswh shape create GameItem --fields '{ "name": { "type": "string", "minLength": 1, "maxLength": 50 }, "rarity": { "type": "string", "enum": ["common", "rare", "epic"] }, "power": { "type": "number", "minimum": 0, "maximum": 100 }}'Versioning
Section titled “Versioning”Shapes are versioned just like things. Revising a shape creates a new version:
wh shape update Location --fields '{"x": "number", "y": "number", "z": "number", "label": "string"}'Existing things retain their association with the shape version they were validated against. New things are validated against the latest shape version.
Listing and Viewing Shapes
Section titled “Listing and Viewing Shapes”# List all shapes, including component-owned oneswh shape list
# View a specific shape with its field definitionswh shape view Location
# Filter by name patternwh shape list --match "Game*"
# Hide component-owned shapeswh shape list --exclude-components
# Show only shapes owned by one componentwh shape list --component com.acme.researchBuilt-in Shapes
Section titled “Built-in Shapes”Five core shapes are built-in and auto-created on first use. They cannot be created or revised manually. (Components may also install system-managed shapes like ComponentConfig — see Components for details.)
Collection shapes — see Collections for details:
- Pair — ordered pair of two things
- Triple — ordered triple of three things
- Set — unordered, deduplicated collection (1+ members)
- List — ordered collection with duplicates (1+ members)
Content shapes:
Readme— repository README content (content: string). When present, the conventional path isReadme/main(e.g.,wh thing view Readme/main --repo org/repo).
Shape Names in Wrefs
Section titled “Shape Names in Wrefs”The shape name is always the first segment in a wref. Given a thing with wref Location/cave:
- Shape:
Location - Thing name:
cave
This means shape names occupy a reserved namespace — you cannot create a thing whose name collides with a shape name at the top level.