Skip to content

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.

Use the CLI or a commit operation to create a shape:

Terminal window
wh shape create Location --fields '{"x": "number", "y": "number", "label": "string"}'

Shapes can also be created as commit operations.

TypeDescriptionExample value
stringText value"hello"
numberNumeric value42
booleanTrue/falsetrue
wrefReference to another thing"Location/cave"
arrayList of values (see Array Fields)["a", "b"]

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.

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.

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.

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 type key (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:

ObjectInterpretationWhy
{ "type": "number", "description": "Height" }Typed fieldAll 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.

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 length
  • maxLength (non-negative integer) — maximum string length; must be ≥ minLength
  • pattern (string) — regular expression the value must match
  • enum (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 ≥ minimum
  • integer (boolean) — when true, 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 elements
  • maxItems (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
}
}

Shapes can have an optional top-level description that documents the shape’s purpose:

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

All field types work in --fields JSON:

Terminal window
# Array fields
wh 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 objects
wh shape create Player --fields '{"name":"string","position":{"x":"number","y":"number"}}'
# Typed field objects with descriptions
wh shape create Sensor --fields '{"value":{"type":"number","description":"Current reading"},"unit":"string"}'
# Constraints
wh 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 }
}'

Shapes are versioned just like things. Revising a shape creates a new version:

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

Terminal window
# List all shapes, including component-owned ones
wh shape list
# View a specific shape with its field definitions
wh shape view Location
# Filter by name pattern
wh shape list --match "Game*"
# Hide component-owned shapes
wh shape list --exclude-components
# Show only shapes owned by one component
wh shape list --component com.acme.research

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 is Readme/main (e.g., wh thing view Readme/main --repo org/repo).

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.