WarmHub: An epistemic repository platform for versioned, structured knowledge
# MCP Server
> Endpoints, protocol details, and configuration for the WarmHub MCP server.
WarmHub implements the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) over HTTP, exposing all read and write operations as typed tools that AI agents can discover and call.
## Protocol
- **MCP version**: 2024-11-05
- **Transport**: JSON-RPC 2.0 over HTTP POST
- **Supported methods**: `initialize`, `tools/list`, `tools/call`, `ping`
## Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/mcp` | Global MCP endpoint |
| `GET` | `/mcp` | Returns 405 Method Not Allowed |
| `POST` | `/mcp/:org/:repo` | Repo-scoped MCP endpoint |
| `GET` | `/mcp/:org/:repo` | Returns 405 Method Not Allowed |
| `GET` | `/.well-known/oauth-protected-resource` | OAuth 2.0 Protected Resource Metadata ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) |
| `GET` | `/.well-known/oauth-authorization-server` | OAuth 2.0 Authorization Server Metadata ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)) |
## Global vs Repo-Scoped
### Global Mode (`POST /mcp`)
The global endpoint exposes the full MCP tool catalog. Repo-level tools require `orgName` and `repoName` arguments. It also includes org-level tools:
- `warmhub_org_list`
- `warmhub_org_get`
- `warmhub_org_set_description`
- `warmhub_org_archive`
- `warmhub_org_unarchive`
- `warmhub_repo_list`
- `warmhub_repo_create`
Best for agents that work across multiple repos.
### Repo-Scoped Mode (`POST /mcp/:org/:repo`)
Org and repo are baked into the URL. Tool schemas omit `orgName`/`repoName` parameters, and the following global-only discovery/creation tools are excluded from the listing:
- `warmhub_org_list`
- `warmhub_org_get`
- `warmhub_repo_list`
- `warmhub_repo_create`
Per-org and per-repo administration tools (`warmhub_org_set_description`, `warmhub_org_archive`, `warmhub_org_unarchive`, `warmhub_repo_set_description`, `warmhub_repo_archive`, `warmhub_repo_unarchive`) remain available on the repo-scoped endpoint and operate on the org/repo baked into the URL.
Best for agents that focus on a single repo — simpler tool schemas, fewer required arguments.
## Authentication
`POST /mcp` and `POST /mcp/:org/:repo` require a Bearer token. Unauthenticated POSTs return HTTP `401` with an RFC 9728 challenge:
```
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.warmhub.ai/.well-known/oauth-protected-resource"
Content-Type: application/json
{ "error": { "code": "UNAUTHENTICATED", "message": "Authentication required" } }
```
Standards-compliant MCP/OAuth clients use the challenge to discover the protected-resource metadata endpoint and bootstrap auth. Without a valid Bearer token, callers cannot list or call MCP tools — there is no anonymous tool catalog.
## Configuration
### MCP Client Setup
Add a WarmHub entry to the `mcpServers` section of your MCP client's configuration. The examples below use the standard format supported by Claude Desktop, Cursor, and other MCP-compatible clients. The `transport` field is required by some clients (e.g., Claude Desktop); others infer it from the URL scheme.
The MCP POST endpoints require a Bearer token — the URL alone is not enough. Standards-compliant MCP/OAuth clients follow the RFC 9728 challenge returned on the first unauthenticated request to discover the OAuth flow and acquire a token automatically. Clients without that capability must inject the token themselves; mint a personal access token (see [HTTP Authentication](/http-api/authentication/)) and configure your client to send `Authorization: Bearer ` on every MCP POST. Many clients accept a `headers` map in the `mcpServers` entry for this purpose, e.g.:
```json
{
"mcpServers": {
"warmhub": {
"transport": "http",
"url": "https://api.warmhub.ai/mcp/myorg/myrepo",
"headers": {
"Authorization": "Bearer ${WH_TOKEN}"
}
}
}
}
```
Consult your MCP client's documentation for the exact field name. See [Authentication](#authentication) above for the challenge format and discovery contract.
Repo-scoped (recommended):
```json
{
"mcpServers": {
"warmhub": {
"transport": "http",
"url": "https://api.warmhub.ai/mcp/myorg/myrepo"
}
}
}
```
Global (multi-repo):
```json
{
"mcpServers": {
"warmhub": {
"transport": "http",
"url": "https://api.warmhub.ai/mcp"
}
}
}
```
The examples above use the production API URL. For self-hosted deployments, replace `api.warmhub.ai` with your deployment URL.
## Tool Responses
Tool responses include both human-readable and structured content:
```json
{
"content": [{ "type": "text", "text": "..." }],
"structuredContent": { ... },
"isError": false
}
```
- `content` — tool result as text for the model-readable channel. Typically JSON.
- `structuredContent` — typed object for programmatic access. Includes an `auth` field on every response (see below).
- `isError` — `true` if the tool call failed
Tool definitions in `tools/list` include an `annotations` block with:
- `readOnlyHint` — `true` for tools that do not mutate repo state.
- `category` — one of `org`, `repo`, `shape`, `thing-read`, `commit`, `subscription`, `action`, `meta`.
- Clients can use `category` to group or filter tools in discovery UIs.
- `warmhub_capabilities` uses the same enum, but groups tools under each category rather than repeating the value per tool. Tool summaries in that payload contain `name`, `description`, and `readOnly` only — the category is implicit in the enclosing group.
### Authentication Awareness
`POST /mcp` requires a Bearer token (see [Authentication](#authentication) above), so every dispatched tool call has a resolved caller. Tool responses still include `structuredContent.auth` with the caller's authentication status — at the public HTTP transport this is always `{ "authenticated": true }` once a tool dispatches:
```json
{ "structuredContent": { "items": [...], "auth": { "authenticated": true } } }
```
Read-only tool descriptions append a runtime auth-hint that reads: _"Returns public data only when unauthenticated. If expected data is missing, suggest the user authenticate to access private orgs and repos."_ Agents using the same tools through transports that allow unauthenticated dispatch can branch on the hint to prompt the user to sign in.
## Error Handling
MCP tool failures arrive in three distinct ways. Inspect the tool response, not just the client-level `try`/`catch`.
**Tool-result errors.** The tool dispatched, but its execution failed. The response carries `isError: true` and a structured error in `content` / `structuredContent`. This covers `VALIDATION_ERROR`, `NOT_FOUND`, `FORBIDDEN`, `RATE_LIMITED` (per-user / per-org tier limits hit during the call), and `warmhub_commit_submit` ambiguous append failures. Each code is described in the [Tool-error code reference](#tool-error-code-reference) below.
**`warmhub_commit_submit` per-operation failures.** Per-op failures are *not* tool errors. The response is a normal success payload with `partial: true`, `statusCounts.error > 0`, and one or more rows with `status: "error"` plus an `error` object. Treat partial results as a routine outcome to inspect.
**Transport-level errors.** Some failures happen before the tool dispatches and surface as HTTP errors instead of tool results:
- **POST with no `Authorization` header** → HTTP 401 with the [`WWW-Authenticate` challenge](#authentication). Headerless requests always reach the challenge, so a `401` means *authenticate*.
- **POST with a malformed or unverified bearer** → treated as anonymous: the request draws down the anonymous rate-limit budget and, once that is exhausted, returns HTTP 429 (below) rather than the `401` challenge. A `429` after presenting a token means the token wasn't accepted — *re-authenticate* with a valid one rather than only backing off.
- **Repo-scoped endpoint cannot resolve the scope** (authenticated, but the repo isn't visible or doesn't exist) → HTTP 404.
- **Anonymous request over the rate limit** → HTTP 429 carrying a JSON-RPC error whose `error.data.warmhub` holds `{ code: "RATE_LIMITED", hint, retryAfter }`. These are the same structured fields as the tool-result error below, so one parser handles both. Read `retryAfter` (seconds) to back off.
Authenticated MCP traffic bypasses the [anonymous rate limit](/http-api/rate-limiting/) and never sees that 429. Once authenticated, rate limits surface instead as the `RATE_LIMITED` tool-result error described below (per-user / per-org tier limits hit during a call).
### Tool-error code reference
- **Validation failures** — include the validation message and, for `warmhub_commit_submit`, enriched hint data (see [`warmhub_commit_submit` Validation Hint](#warmhub_commit_submit-validation-hint) below).
- **`NOT_FOUND`** — includes the error details. Tool calls always carry a resolved caller (POST requires a Bearer token), and missing targets surface as `NOT_FOUND` regardless of which token was presented.
- **`FORBIDDEN`** — surfaced as `errorCode: "FORBIDDEN"` in the structured error when the presented token is valid but lacks the scope required for the requested resource. Distinct from `NOT_FOUND`; use it to decide whether retrying with a more privileged token is sensible.
- **`RATE_LIMITED`** — surfaced as `errorCode: "RATE_LIMITED"` when a per-user or per-org write limit is hit. See [Rate Limiting](/http-api/rate-limiting/) for tier limits and retry guidance. Treat as retryable with a bounded backoff, and prefer the `errorCode` over string-matching the message.
- **Ambiguous append failures** (`warmhub_commit_submit` only) — when a transport-ambiguous failure (network reset, 5xx, timeout) interrupts the stream append, the tool returns `isError: true` with `structuredContent.error.data` carrying `partial: true`, `completedOperationCount`, `failedOperationOffset`, and a `continuation` object containing the `streamId`. **The failed append may have landed server-side.** Treat repository state as the source of truth — inspect it with `warmhub_thing_get` / `warmhub_thing_query` before deciding whether to resume. When resuming, pass the `continuation`'s `streamId` in the new call and re-submit only the operations that haven't been acknowledged; make `add` operations idempotent with `skipExisting: true` for safer retries. See [SDK Streaming Write Failures](/sdk/transient-retry/) for the parallel SDK contract.
Tool-result errors (the `isError: true` responses above) also include `structuredContent.auth` with the caller's authentication status, so agents can decide whether to suggest authenticating and retrying. Transport-level failures do not carry this field.
### `warmhub_commit_submit` Validation Hint
When `warmhub_commit_submit` rejects a malformed `operations` entry, the tool result's `structuredContent.error.data` lists the eight valid operation variant signatures so agents can self-correct on the next turn — the `message` is the original validation message.
```json
{
"isError": true,
"content": [{ "type": "text", "text": "" }],
"structuredContent": {
"error": {
"code": -32602,
"message": "",
"data": {
"tool": "warmhub_commit_submit",
"errorCode": "VALIDATION_ERROR",
"backendCode": "VALIDATION_ERROR",
"expected": "one of the operation variants",
"operations": [
"ADD shape: { operation:'add', kind:'shape', name, data }",
"ADD thing: { operation:'add', kind:'thing', name, data }",
"ADD assertion: { operation:'add', kind:'assertion', name, about, data }",
"ADD collection: { operation:'add', kind:'collection', type, members, name? }",
"REVISE shape: { operation:'revise', kind:'shape', name, data }",
"REVISE thing: { operation:'revise', kind:'thing', name, data }",
"REVISE assertion: { operation:'revise', kind:'assertion', name, data }",
"RETRACT: { operation:'retract', name, reason?, kind? }"
]
}
},
"auth": { "authenticated": true }
}
}
```
The `expected` and `operations` fields are attached only for `warmhub_commit_submit` validation errors; validation errors from other tools continue to return `data` with just `tool` and `errorCode`. `backendCode` may also appear as a compatibility alias for `errorCode`; parse `errorCode`. The same variant list is also returned by `warmhub_repo_describe` under `commitContract.operationVariants`.
### Repo-Scoped 404
`POST /mcp/:org/:repo` requires a Bearer token; unauthenticated POSTs return `401` before any repo lookup happens (see [Authentication](#authentication)). For *authenticated* callers whose token cannot resolve the org or repo, the endpoint returns HTTP 404 with a plain `Not Found` body.
---
# MCP Tool Walkthrough
> Bootstrap, read, write, and query a WarmHub repo through the MCP tool sequence.
:::note[New to MCP?]
Start with the [Quickstart](/get-started/quickstart/#connect-via-mcp) to set up your client first. This page assumes you already have an MCP-connected agent pointed at WarmHub.
:::
:::tip[Try it against live data]
The walkthrough below uses a generic game-world example for clarity. To run it against real data, point your authenticated MCP client at the public [`warmhub-data/congress-trading`](https://app.warmhub.ai/orgs/warmhub-data/repos/congress-trading) repo and walk the same `warmhub_capabilities` → `warmhub_repo_describe` → read-tools sequence against the `StockTrade` shape. Other public repos in the [warmhub-data org](https://app.warmhub.ai/orgs/warmhub-data) work too — let `warmhub_repo_describe` tell you which shapes each one exposes.
The walkthrough interleaves reads and writes: run steps 1-3 and 5 against `warmhub-data/*` (the discovery + read steps — substitute your target repo's shapes from `warmhub_repo_describe`'s output for the placeholder `Location`/`Observation` payloads), and skip step 4 (the write step) on public datasets — it needs permission on the target repo. To run writes, repoint your MCP client to a repo you own (e.g. `/mcp//`).
The WarmHub MCP server requires a token even for public-repo reads, so make sure your client is signed in first.
:::
Once connected, an agent can interact with WarmHub using the tools below. Here is the recommended sequence for bootstrapping.
## 1. Discover the catalog with `warmhub_capabilities`
Call `warmhub_capabilities` first to see which tools are callable on the current endpoint, grouped by category, plus a short workflow cookbook and the wref syntax reference:
```json
{
"name": "warmhub_capabilities"
}
```
The payload is static (no arguments) and scoped to the endpoint you connected to (global or repo-scoped).
In repo-scoped mode, global-only discovery tools (`warmhub_org_list`, `warmhub_org_get`, `warmhub_repo_list`, `warmhub_repo_create`) are omitted.
Use this as your catalog index, then follow up with `warmhub_repo_describe` for per-repo specifics.
## 2. Orient to the repo with `warmhub_repo_describe`
Call `warmhub_repo_describe` to get a complete picture of the repository:
```json
{
"name": "warmhub_repo_describe"
}
```
This returns:
- **Shapes** defined in the repo, including field types, optional shape-level descriptions, and per-field inline descriptions extracted from [typed field objects](/data-modeling/shapes/#typed-field-objects) (fields with descriptions appear as `{ "type": "number", "description": "Horizontal position" }`)
- **Summary counts** — `shapeCount`, `subscriptionCount`, `totalCount`, plus breakdowns by kind and by shape
- **Sample wrefs** — example references the agent can use immediately
- **Write examples** — ready-to-use `warmhub_commit_submit` operations tailored to the repo's actual shapes
The write examples are generated from the repo's current shape definitions, so the agent gets correct field names and types without guessing.
## 3. Read current state with `warmhub_thing_head`
Get a snapshot of all active things:
```json
{
"name": "warmhub_thing_head"
}
```
Filter by shape or kind for targeted results:
```json
{
"name": "warmhub_thing_head",
"arguments": {
"shape": "Location",
"limit": 10
}
}
```
## 4. Write data with `warmhub_commit_submit`
Create or update things and assertions with versioned write operations. The `committer`
wref below must already resolve to an existing thing — create the agent
identity in a prior write, or omit `committer` to let the authenticated
user receive attribution via `createdByEmail`.
```json
{
"name": "warmhub_commit_submit",
"arguments": {
"committer": "Agent/claude",
"message": "Add initial game locations",
"operations": [
{
"operation": "add",
"kind": "thing",
"name": "Location/cave",
"data": { "x": 3, "y": 7, "label": "Dark Cave" }
},
{
"operation": "add",
"kind": "assertion",
"name": "Observation/cave-safe",
"about": "Location/cave",
"data": { "safe": true, "confidence": 0.8 }
}
]
}
}
```
A single MCP write request can carry multiple operations across `add` (shapes, things, assertions, collections), `revise` (shapes, things, assertions), and `retract` (any entity kind). One operation can succeed while another in the same batch fails — `warmhub_commit_submit` returns a result row for each. Read the rows to see which landed and which need a retry. See the [MCP Tools Reference](/agent-integration/mcp-tools-reference/#warmhub_commit_submit) for the exact field shape and [Writes](/writes/overview/) for the cross-surface contract.
## 5. Query with `warmhub_thing_query`
For targeted retrieval by shape, kind, or about-reference:
```json
{
"name": "warmhub_thing_query",
"arguments": {
"shape": "Observation",
"about": "Location/cave"
}
}
```
This returns all active `Observation` assertions about `Location/cave`.
## Key Concepts for Agents
**Discover, then describe.** Start with `warmhub_capabilities` to learn what tools are callable on the current endpoint, then call `warmhub_repo_describe` for schema definitions, sample data references, wref syntax rules, and write contract examples tailored to the repo.
**MCP writes can succeed in part.** `warmhub_commit_submit` returns a result row per operation, so one failed entry doesn't poison the batch. See [MCP Error Handling](/agent-integration/mcp-server/#error-handling) for the full failure taxonomy (per-op rows, tool-result errors, and transport-level errors).
**Wrefs address everything.** Things are referenced by `Shape/name` (local) or `wh:org/repo/Shape/name` (canonical). Assertions use `about` to reference their subject.
**Prefer `warmhub_thing_query` for targeted reads.** Use `warmhub_thing_head` for broad orientation and `warmhub_thing_query` when you know what shape or subject you need.
## Full Tool Reference
WarmHub exposes MCP tools covering organizations, repositories, shapes, things, assertions, commits, subscriptions, actions, and meta (capability discovery). See the [MCP Tools Reference](/agent-integration/mcp-tools-reference/) for the complete list with argument schemas and descriptions.
## Next Steps
- [Core Concepts](/get-started/core-concepts/) — review the data model behind things, assertions, and writes
- [MCP Server](/agent-integration/mcp-server/) — detailed MCP server configuration and protocol details
- [MCP Tools Reference](/agent-integration/mcp-tools-reference/) — full reference for all MCP tools
- [Agent Context (wh prime)](/agent-integration/wh-prime/) — CLI-side bootstrapping for agents with shell access
---
# MCP Tools Reference
> Complete catalog of all MCP tools with parameters and descriptions.
WarmHub exposes a full MCP tool catalog for organizations, repositories, shapes, things, assertions, commits, subscriptions, actions, and meta (capability discovery). In **repo-scoped** mode, `orgName`/`repoName` are omitted from all tool schemas. In **global** mode, they're required on repo-level tools.
## SDK and MCP Method Map
If you switch between the MCP tools and the [TypeScript SDK](/sdk/overview/), the names line up almost one-to-one. A few common pairs:
| MCP tool | SDK method |
|----------|-----------|
| `warmhub_thing_head` | `client.thing.head(...)` |
| `warmhub_thing_query` | `client.thing.query(...)` |
| `warmhub_thing_get` | `client.thing.get(...)` |
| `warmhub_shape_list` | `client.shape.list(...)` |
| `warmhub_subscription_list` | `client.subscription.list(...)` |
| `warmhub_commit_submit` | `client.commit.apply(...)` — note the verb differs |
| `warmhub_capabilities` | `client.diagnostics.capabilities()` — returns the backend API version, minimum supported SDK, and feature flags (not the tool catalog) |
| `warmhub_repo_describe` | no single method — `client.repo.get` + `client.shape.list` + `client.repo.getStats` cover repo metadata, shapes, and counts, but not the sampled data, query hints, `wrefSyntax`, or write contract / generated examples that `warmhub_repo_describe` also returns |
Most tools follow the `warmhub__` ↔ `client..` pattern; the last three rows are the exceptions.
:::note[Auth awareness in tool descriptions]
All read-only tools include an auth awareness note in their runtime description, instructing agents to suggest authentication when expected data is missing. This note is appended automatically and does not appear in the static descriptions below.
:::
## Meta Tools
Orientation and capability discovery. The `meta` category covers two tools:
| Tool | Description |
|------|-------------|
| `warmhub_capabilities` | Static, endpoint-scoped overview of the MCP tool catalog: tools grouped by category, a workflow cookbook, [wref](/data-modeling/wrefs/) syntax, and a pointer to the full write operation contract. Read-only; no arguments. |
| `warmhub_repo_describe` | Per-repo live view: schema, shape descriptions, field types, summary stats, wref syntax, operation contract, and write examples generated from the repo's own shapes. Documented under [Repository Tools](#repository-tools). |
Call `warmhub_capabilities` first to orient on what tools exist; then call `warmhub_repo_describe` to learn the repo-specific shapes and write examples.
### warmhub_capabilities
Takes no arguments. Returns a static orientation payload with the following fields:
| Response Field | Type | Description |
|----------------|------|-------------|
| `categories` | object[] | One entry per tool category (`org`, `repo`, `shape`, `thing-read`, `commit`, `subscription`, `action`, `meta`), each with `name`, `description`, and the tools (`name`, `description`, `readOnly`) advertised on the current endpoint. |
| `cookbook` | object[] | Common workflows as `{ task, steps: [{ tool, note }] }` — e.g. discovering shapes, searching by content, writing first data. |
| `usagePatterns` | object[] | Query-discipline guidance — recommended patterns for reading, querying, and writing efficiently. |
| `wrefSyntax` | object | Local and canonical wref forms, version modifiers, path/name constraints, and write-path preview rules. |
| `commitContractRef` | object | Pointer to `warmhub_repo_describe`, which returns the full write operation contract, operation variants, and live write examples scoped to a specific repo. |
The payload reflects the endpoint scope. On the repo-scoped endpoint (`/mcp/:org/:repo`), `categories` omits global-only tools: `warmhub_org_list`, `warmhub_org_get`, `warmhub_repo_list`, and `warmhub_repo_create`.
Call `warmhub_capabilities` first to orient an agent, then call `warmhub_repo_describe` for per-repo schema and write examples.
## Organization Tools
`warmhub_org_list` and `warmhub_org_get` are global-only. The per-org administration tools (`warmhub_org_set_description`, `warmhub_org_archive`, `warmhub_org_unarchive`) are available in both global and repo-scoped mode; repo-scoped callers operate on the org baked into the URL.
| Tool | Description |
|------|-------------|
| `warmhub_org_list` | List organizations (archived hidden by default). *(global only)* |
| `warmhub_org_get` | Get an organization by name. *(global only)* |
| `warmhub_org_set_description` | Set or clear an organization description. |
| `warmhub_org_archive` | Archive an organization. |
| `warmhub_org_unarchive` | Unarchive an organization. |
### warmhub_org_list
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `includeArchived` | boolean | no | Include archived organizations in results |
### warmhub_org_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
### warmhub_org_set_description
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `description` | string | no | New org description. Trimmed; empty strings clear the value; max 2000 characters. |
### warmhub_org_archive
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
Archived organizations block new repo creation and member additions.
### warmhub_org_unarchive
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
## Repository Tools
| Tool | Description |
|------|-------------|
| `warmhub_repo_create` | Create a new repository in an organization. *(global only)* |
| `warmhub_repo_list` | List repositories in an organization (archived hidden by default). *(global only)* |
| `warmhub_repo_get` | Get repository metadata by org/repo. |
| `warmhub_repo_describe` | Describe repository schema, shape descriptions, field types, per-shape `queryHints`, and summary stats for agent bootstrapping. |
| `warmhub_repo_set_description` | Set or clear a repository description. |
| `warmhub_repo_archive` | Archive a repository. |
| `warmhub_repo_unarchive` | Unarchive a repository. |
### warmhub_repo_create
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | yes | Repository name |
| `description` | string | no | Repository description |
| `visibility` | string | no | `"public"` or `"private"`. Defaults to `"private"`. |
Returns the created repo object (same shape as `warmhub_repo_get`). Only available on the global MCP endpoint.
### warmhub_repo_list
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `includeArchived` | boolean | no | Include archived repositories in results |
| `limit` | integer | no | Max repos to return (1–200). Must be paired with `cursor` when paging. |
| `cursor` | string | no | Pagination cursor from a prior response. Must be paired with `limit`. |
### warmhub_repo_set_description
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | yes | Repository name |
| `description` | string | no | New repo description. Trimmed; empty strings clear the value; max 2000 characters. |
### warmhub_repo_archive
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | yes | Repository name |
Archived repositories reject new commits.
### warmhub_repo_unarchive
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | yes | Repository name |
### warmhub_repo_describe
The most important tool for agent bootstrapping. Returns:
- Repository metadata
- Shape definitions with field types and optional shape-level `description`
- Per-shape `queryHints` with `queryableFields`, `wrefFields`, and suggested MCP query patterns
- Per-field descriptions inlined into each field entry. Fields without descriptions appear as bare type strings (e.g. `"string"`); fields with descriptions appear as `{ "type": "number", "description": "Horizontal position" }`. Descriptions are extracted from [typed field objects](/data-modeling/shapes/#typed-field-objects).
- Summary counts (`shapeCount`, `subscriptionCount`, `totalCount`)
- Counts by kind and by shape
- Sample wrefs from the repo
- Wref syntax reference
- Operation contract (add/revise/retract variants, inline collection syntax, rules, about semantics)
- Write examples generated from actual repo shapes
Call this first when connecting to a repo.
The response includes an `additionalInformation` array pointing at the three well-known [Content shape](/data-modeling/content-shape/) wrefs:
```json
"additionalInformation": [
{ "name": "Readme", "wref": "Content/Readme", "synthesized": false },
{ "name": "Agents", "wref": "Content/Agents", "synthesized": false },
{ "name": "LlmsTxt", "wref": "Content/LlmsTxt", "synthesized": true }
]
```
## Content Tools
Two tools cover the built-in [Content shape](/data-modeling/content-shape/) — `Readme`, `Agents`, and the synthesized `LlmsTxt` — discriminated by a `kind` argument.
| Tool | Description |
|------|-------------|
| `warmhub_repo_content_get` | Fetch repo Content markdown by `kind`. For `readme`/`agents`, returns a synthesized empty stub when nothing has been written — never null. `kind: llms-txt` always returns a synthesized response with the rendered sitemap and a structured `refs` field. Read-only. |
| `warmhub_repo_content_set` | Set `Content/Readme` or `Content/Agents` markdown (commits an add or revise operation). Writes to `kind: llms-txt` are rejected — it is synthesized and cannot be stored. Requires `things:write`. |
WarmHub no longer hosts README/AGENTS generation. To draft content, run the CLI command `wh repo content prompt --kind readme` to get an agent-ready prompt, let your own agent write the markdown, then persist it with `warmhub_repo_content_set`.
### warmhub_repo_content_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | yes | Repository name |
| `kind` | string | yes | One of `readme`, `agents`, or `llms-txt` |
For `kind: readme` and `kind: agents`, returns the stored content thing after the first write. When nothing has been written yet, the response is a synthesized empty stub (`{ synthesized: true, shape: "Content", name: "...", data: { content: "" }, active: true }`).
For `kind: llms-txt`, always returns a synthesized response. `data.content` contains the full rendered markdown, and the response includes a `refs` field with partitioned outbound/inbound references; cross-org refs the caller cannot read are omitted. (MCP requests are always authenticated; for the anonymous reduced-body variant of `llms.txt`, see [Content Shape](/data-modeling/content-shape/).)
### warmhub_repo_content_set
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `orgName` | string | yes | Organization name |
| `repoName` | string | yes | Repository name |
| `kind` | string | yes | One of `readme`, `agents`, or `llms-txt`. `llms-txt` is read-only — `set` attempts are rejected. |
| `content` | string | yes | Markdown content to store |
Returns the standard single-operation commit result: `operationCount`, one `operations[]` entry (`name`, `operation`, `version`, `dataHash`), plus optional `committer`, `createdByEmail`, and `message` metadata. Unlike `warmhub_commit_submit`, this helper does not return partial-result fields; rejected writes surface as tool errors.
## Thing / Query Tools
| Tool | Description |
|------|-------------|
| `warmhub_thing_head` | List all items at HEAD with optional filters (shape, kind, glob match). Use to enumerate a repo's current state; for fuzzy lookups use `warmhub_thing_search`. |
| `warmhub_thing_get` | Fetch one thing/assertion/collection by wref. For many wrefs in one call use `warmhub_thing_get_many`; to resolve a wref's canonical identity first use `warmhub_wref_resolve`. |
| `warmhub_thing_graph` | Get one thing and its embedded assertion/about/wref graph to a bounded depth. |
| `warmhub_thing_get_many` | Batch-fetch things by wref in one call — prefer over looping `warmhub_thing_get`. Missing wrefs are returned in a `missing` array. |
| `warmhub_thing_history` | List version history for a thing. Provide `wref` for one thing's history, or `shape`/`about` to survey history across matching things. With `about`, `resolveCollections:true` includes assertions about collections containing the target identity. |
| `warmhub_thing_about` | List assertions whose about target resolves to the supplied target identity. For identity-scoped inputs, use `resolveCollections:true` to include assertions about Pair/Triple/Set/List collections containing the target; pinned `@vN` inputs stay version-exact. Use `warmhub_thing_refs` with `direction:"inbound"` for broader backlink discovery. |
| `warmhub_thing_query` | Query things by structured filters (shape, kind, about, glob match). Best for exact/structured lookups; for fuzzy or semantic search use `warmhub_thing_search`. |
| `warmhub_thing_search` | Full-text/vector/hybrid search across thing data. Best for fuzzy lookups; for exact field matches use `warmhub_thing_query` with a glob filter. |
| `warmhub_thing_refs` | List inbound or outbound refs for a target wref. Use `direction:"inbound"` to find what references X. Pair with `warmhub_thing_get` to resolve details. |
| `warmhub_wref_resolve` | Resolve a wref (local or canonical) to its canonical thing identity. Accepts cross-repo canonical wrefs (`wh:org/repo/Shape/name`); pair with `warmhub_thing_get` to fetch the resolved data. |
### warmhub_thing_head
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `shape` | string | no | Filter by shape name |
| `kind` | string | no | Filter by kind |
| `match` | string | no | Glob pattern to filter wrefs (`*` = one segment, `**` = zero or more) |
| `excludeInfraShapes` | boolean | no | Hide internal infra shapes from results |
| `count` | boolean | no | Return count of matching items instead of the full result list |
| `limit` | integer | no | Max items (minimum 1) |
| `cursor` | string | no | Pagination cursor from previous response |
### warmhub_thing_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wref` | string | yes | WarmHub reference |
| `version` | integer | no | Specific version number |
| `includeRetracted` | boolean | no | Return the thing even if it is retracted |
In global mode, `orgName`/`repoName` may be omitted when `wref` is a [durable id](/data-modeling/wrefs/#durable-ids) — a durable id routes itself to the repo that owns the target.
### warmhub_thing_graph
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wref` | string | yes | WarmHub reference |
| `version` | integer | no | Specific version number |
| `depth` | integer | no | Embedded traversal depth, 1 through 5 |
| `limit` | integer | no | Max embedded nodes, 1 through 500 |
Returns the root thing with readable assertion `about` links and readable wref-typed fields embedded as objects. Refs the caller cannot read remain string wrefs, with no internal IDs or denial reasons exposed.
In global mode, `orgName`/`repoName` may be omitted when `wref` is a [durable id](/data-modeling/wrefs/#durable-ids).
### warmhub_thing_get_many
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wrefs` | string[] | yes | Array of wrefs, 1 through 500 entries per call |
| `version` | integer | no | Pin all lookups to this version |
| `includeRetracted` | boolean | no | Return things even when retracted at HEAD or at the requested `version` (mirrors `warmhub_thing_get`) |
Missing wrefs are returned in a `missing` array. When a top-level `version` is supplied and the input wref is not already pinned, missing entries are version-qualified (`Shape/name@vN`) so the round-trip is unambiguous; per-wref pins survive intact (no double-pinning). Duplicates in `wrefs` are not deduped — they count toward the 500-entry cap and produce duplicate `items`/`missing` entries.
### warmhub_thing_history
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wref` | string | no | Thing wref |
| `shape` | string | no | Filter by shape |
| `about` | string | no | Filter by about target |
| `includeRetracted` | boolean | no | Allow resolving retracted shape or about targets (does not filter results) |
| `resolveCollections` | boolean | no | With `about`, include assertion history for Pair/Triple/Set/List collections containing the target identity, including when the about wref is pinned |
| `limit` | integer | no | Max versions to return |
| `cursor` | string | no | Pagination cursor from previous response |
At least one of `wref`, `shape`, or `about` is required.
In global mode, `orgName`/`repoName` may be omitted only when `wref` is a [durable id](/data-modeling/wrefs/#durable-ids). `shape`/`about` surveys and local wrefs always require `orgName`/`repoName` — a repo-less filter query is rejected.
### warmhub_thing_about
By default, this tool returns assertions whose about target resolves to the supplied target identity. It does not expand collection member refs, so assertions about Pair, Triple, Set, or List collection things that contain the target appear only when `resolveCollections:true` is set on identity-scoped inputs: bare wrefs, `@HEAD`, or `@ALL`. Pinned `@vN` inputs stay version-exact and do not expand collection members.
For broader graph discovery, use `warmhub_thing_refs` with `direction:"inbound"` to find current things that reference the target through wref fields. Use `warmhub_thing_about` when you specifically need assertion records and about-target filtering.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wref` | string | yes | Target thing wref |
| `shape` | string | no | Filter assertions by shape |
| `match` | string | no | Glob pattern to filter assertion wrefs |
| `resolveCollections` | boolean | no | Include assertions about Pair/Triple/Set/List collections containing the target thing for identity-scoped inputs; ignored for pinned `@vN` inputs |
| `includeRetracted` | boolean | no | Resolve a retracted target and include retracted assertions |
| `depth` | integer | no | Recursive assertion depth |
| `limit` | integer | no | Max assertions to return |
| `cursor` | string | no | Pagination cursor from previous response |
:::note[Binomial-opinion constraint]
Any returned subjective-logic opinion `(b, d, u, α)` is a binomial opinion — well-formed only when the underlying assertion expresses a binary proposition (true/false). See [Opinions as Separate Assertions](/data-modeling/patterns/#opinions-as-separate-assertions).
:::
### warmhub_thing_query
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `shape` | string | no | Filter by shape |
| `about` | string | no | Filter by about target |
| `kind` | string | no | Filter by kind |
| `match` | string | no | Glob pattern to filter wrefs |
| `count` | boolean | no | Return count of matching items instead of the full result list |
| `resolveCollections` | boolean | no | When `about` is set, also include assertions about collections containing the target |
| `includeRetracted` | boolean | no | Include retracted entities |
| `componentId` | string | no | Filter results to items owned by the given component |
| `excludeComponents` | boolean | no | Exclude component-owned items from results. Cannot be combined with `componentId`. |
| `excludeInfraShapes` | boolean | no | Hide internal infra shapes from results |
| `limit` | integer | no | Max results. Must be between `1` and `500`. |
| `cursor` | string | no | Pagination cursor from previous response |
### warmhub_thing_search
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `query` | string | yes | Search query text |
| `shape` | string | no | Filter by shape name |
| `kind` | string | no | Filter by kind |
| `about` | string | no | Filter by about target (not supported with vector mode) |
| `match` | string | no | Glob pattern to filter wrefs |
| `resolveCollections` | boolean | no | When `about` is set, also include assertions about collections containing the target (text mode only) |
| `mode` | string | no | `"text"` (default), `"vector"`, or `"hybrid"` |
| `includeRetracted` | boolean | no | Include retracted entities |
| `componentId` | string | no | Filter results to items owned by the given component |
| `excludeComponents` | boolean | no | Exclude component-owned items from results. Cannot be combined with `componentId`. |
| `excludeInfraShapes` | boolean | no | Hide internal infra shapes from results |
| `limit` | integer | no | Max results. Must be between `1` and `500`. |
| `cursor` | string | no | Pagination cursor from previous response (text mode only — vector and hybrid reject cursor). When `about` or `resolveCollections` is set, pages may be sparse — paginate until `nextCursor` is absent. |
### warmhub_thing_refs
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wref` | string | yes | WarmHub reference |
| `direction` | string | no | `"inbound"` (default) or `"outbound"` |
| `fieldPath` | string | no | Filter by field path (inbound only) |
| `limit` | integer | no | Max results. Must be between `1` and `500`. |
| `cursor` | string | no | Pagination cursor from previous response |
Direction `"inbound"` returns things that reference the target wref. Direction `"outbound"` returns things the target wref references.
Use inbound refs as a broad discovery tool when you are unsure whether data points directly at a thing or at a collection containing it. Inbound refs are not a substitute for `warmhub_thing_about` when you need assertion-only results or assertion filters.
### warmhub_wref_resolve
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `wref` | string | yes | WarmHub reference to resolve |
## Cross-repo visibility
Cross-repo wref lookups (canonical form `wh:org/repo/Shape/name`) require effective [`repo:read`](/auth/getting-access/#repository-visibility) permission on the target repo. Public repos are readable by anyone. For private repos, callers without that access see an error — except `warmhub_thing_search` with a cross-repo `about` (returns `{ items: [] }`) and `warmhub_thing_get_many` (puts unreadable wrefs into `missing[]`) — both to keep batch and search streaming-friendly.
See [Getting Access](/auth/getting-access/) for the precise rules.
## Write Tools
| Tool | Description |
|------|-------------|
| `warmhub_commit_submit` | Submit a list of operations against a repo. Returns per-operation results; see the heading section below for the failure taxonomy and write examples. |
Use `warmhub_thing_history` for per-thing version trails.
### warmhub_commit_submit
The tool returns per-operation results. Per-op failures show up as `operations[]` entries with `status: "error"` and `partial: true`. For the full operation contract and shape-specific write examples, call `warmhub_repo_describe` and inspect the `commitContract` field. Opinion-bearing assertions must be [binary propositions](/data-modeling/patterns/#opinions-as-separate-assertions).
See [MCP Error Handling](/agent-integration/mcp-server/#error-handling) for the full failure taxonomy. Ambiguous append failures (a separate class from per-op failures) return a tool-result error with `continuation` recovery state; **the failed append may have landed server-side**, so inspect repository state before deciding whether to resume.
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `committer` | string | no | Optional [wref](/data-modeling/wrefs/) identifying the actor on whose behalf the writes are made (e.g. `Agent/bot-1`). Omit to attribute to the authenticated user via `createdByEmail`. |
| `componentId` | string | no | Attribute writes to an installed component. See component identity rules below. |
| `message` | string | no | Optional message recorded with each thing-version produced by this call |
| `operations` | array | yes | Operations array (non-empty). When resuming after an ambiguous append failure, pass only the operations that haven't been acknowledged — inspect repository state with `warmhub_thing_get` / `warmhub_thing_query` to determine which landed. |
| `streamId` | string | no | When resuming after an ambiguous append failure, copy the `streamId` from the prior error's `continuation` payload. |
:::note[Binomial-opinion constraint]
When writing an assertion that carries a subjective-logic opinion `(b, d, u, α)`, the assertion must express a binary proposition (true/false). The opinion tuple is a binomial opinion and is meaningless on open-ended claims. See [Opinions as Separate Assertions](/data-modeling/patterns/#opinions-as-separate-assertions).
:::
Component identity rules:
- User tokens may claim components installed by that user.
- Callers with [`org:configure`](/auth/personal-access-tokens/#available-permissions) for the org may claim any installed component in the org.
- Action tokens derive the component from the running subscription and reject mismatched explicit values.
**Operation variants:**
- ADD shape: `{ operation: "add", kind: "shape", name, data, skipExisting? }`
- ADD thing: `{ operation: "add", kind: "thing", name, data, skipExisting? }`
- ADD assertion: `{ operation: "add", kind: "assertion", name, about, data, skipExisting? }`
- ADD collection: `{ operation: "add", kind: "collection", type, members, name?, skipExisting? }`
- REVISE shape/thing/assertion: `{ operation: "revise", kind, name, data, expectedVersion? }`
- RETRACT: `{ operation: "retract", name, reason? }` — withdraws the entity from default reads; `kind` is an optional hint
Token rules: `$N` allocates in the last segment of add-operation names. `#N` references prior allocations in the same stream. Write path rejects `@ALL`.
**Per-operation result warnings:**
Successful and noop result entries can include:
```json
{
"warnings": {
"undeclaredFields": ["status", "filePath"],
"undeclaredFieldsTruncated": true,
"totalUndeclared": 600
}
}
```
`warnings.undeclaredFields` lists top-level keys present in the submitted `data` but not declared in the target shape. When the list is capped, `undeclaredFieldsTruncated: true` is set and `totalUndeclared` reports the full count. This warning is informational; it does not turn the operation into a failure.
For add operations, `skipExisting: true` returns `noop` when the target already exists instead of failing. For revise operations, `expectedVersion` applies the change only if the target is still at that version, otherwise it rejects with `CONFLICT`. A revise whose data matches the current version returns `noop` instead of creating a new version. See [Conditional Operations](/writes/operations/#conditional-operations) for the full model.
Unauthorized component claims reject with `FORBIDDEN`.
## Shape Tools
| Tool | Description |
|------|-------------|
| `warmhub_shape_list` | List shapes in a repository. Each item includes per-shape `queryHints`. |
| `warmhub_shape_get` | Get a shape by name. Response includes `queryHints`. |
### warmhub_shape_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `shapeName` | string | yes | Shape name |
Both `warmhub_shape_get` and `warmhub_shape_list` include a `queryHints` block (`queryableFields`, `wrefFields`, `suggestedPatterns`) to help agents choose `warmhub_thing_query`, `warmhub_thing_search`, and `warmhub_thing_refs` patterns for each shape.
## Subscription Tools
See [Subscriptions](/subscriptions/overview/) for concepts and [Creating Subscriptions](/subscriptions/creating/) for setup guides with filter and credential examples.
| Tool | Description |
|------|-------------|
| `warmhub_subscription_list` | List subscriptions in a repository. |
| `warmhub_subscription_get` | Get subscription metadata by name. |
| `warmhub_subscription_create` | Create a webhook subscription. |
| `warmhub_subscription_update` | Update an existing subscription's trigger or webhook config. |
| `warmhub_subscription_pause` | Pause an active subscription. |
| `warmhub_subscription_resume` | Resume a paused subscription. |
| `warmhub_subscription_delete` | Delete a subscription. |
### warmhub_subscription_get
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | yes | Subscription name |
### warmhub_subscription_create
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | yes | Subscription name |
| `kind` | string | yes | `"webhook"` |
| `shapeName` | string | no | Shape to subscribe to. For webhook subscriptions, provide either `shapeName` or `filterJson.shape` — except for [shape lifecycle subscriptions](/subscriptions/filter-json/#shape-lifecycle-subscriptions), which omit both and rely on a `{"kind":"shape", ...}` filter. |
| `filterJson` | object | yes | Filter object for matching write operations |
| `webhookUrl` | string | yes | Webhook endpoint URL |
| `fallbackWebhookUrl` | string | no | Optional fallback endpoint called after a terminal delivery failure |
| `allowTraceReentry` | boolean | no | Reentry policy for write-triggered subscriptions. Defaults to `false` |
| `sourceRepoRef` | string | no | Source repo (`org/repo`) for a [cross-repo subscription](/subscriptions/creating/#cross-repo-subscriptions). Must be in the same org as the home repo |
| `notifyOnSuccess` | boolean | no | Emit an in-app notification on successful delivery. Defaults to `false` |
### warmhub_subscription_update
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | yes | Existing subscription name |
| `shapeName` | string | no | Replacement shape for webhook subscriptions |
| `filterJson` | object | no | Replacement filter object |
| `webhookUrl` | string | no | Replacement webhook URL |
| `fallbackWebhookUrl` | string or null | no | Replacement fallback webhook URL. Use `null` to clear it |
| `allowTraceReentry` | boolean | no | Replacement reentry policy for write-triggered subscriptions |
### warmhub_subscription_pause / warmhub_subscription_resume / warmhub_subscription_delete
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | yes | Subscription name |
## Action Tools
| Tool | Description |
|------|-------------|
| `warmhub_action_livefeed` | Get delivery feed for a subscription. Includes run status, attempt count, and error details. |
| `warmhub_action_runs` | List action runs in a repository. |
| `warmhub_action_attempts` | Get attempt history for a specific action run. |
| `warmhub_action_notifications` | List repo-scoped action notification records, including terminal failures and optional success notifications. |
### warmhub_action_livefeed
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `subscriptionName` | string | yes | Subscription name |
| `limit` | integer | no | Max items, 1–500. Required when `cursor` is provided |
| `cursor` | string | no | Pagination cursor from previous response. **Must be paired with an explicit `limit`** — supplying `cursor` alone is rejected with `"cursor" requires "limit"` |
Each item in the response includes delivery fields plus optional run diagnostics:
| Response Field | Type | Description |
|----------------|------|-------------|
| `runStatus` | string? | Run outcome: `succeeded`, `failed_terminal`, `dead_letter`, etc. |
| `attemptCount` | number? | Current attempt number |
| `maxAttempts` | number? | Maximum attempts allowed |
| `lastErrorCode` | string? | Error classification code (for example `HTTP_502` or `WEBHOOK_TARGET_REJECTED`) |
| `lastErrorMessage` | string? | Human-readable error description |
### warmhub_action_runs
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | no | Filter by status: `pending`, `running`, `processing`, `retry_wait`, `succeeded`, `failed_terminal`, `dead_letter` |
| `since` | string | no | ISO datetime or unix timestamp |
| `limit` | integer | no | Max results. Must be between `1` and `200`. |
### warmhub_action_attempts
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `runId` | string | yes | Action run identifier (UUIDv7) for the target run. |
### warmhub_action_notifications
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `since` | string | no | ISO datetime or unix timestamp |
| `limit` | integer | no | Max results |
Returns repo-scoped action notification records for action deliveries, including terminal failures and optional success notifications. This is distinct from the web app's cross-repo user notification feed.
---
# Agent Context (wh prime)
> Context bootstrapping for AI agents — how to run it, its flags, output format, and token budget.
`wh prime` prints a compact CLI context dump built for AI agents. It's meant to be the first command an agent runs at the start of a session, so the agent knows WarmHub's concepts, wref syntax, and command surface before it does any work.
## When to Use
- **Session start** — give the agent full CLI context before it begins work.
- **After compaction** — restore context that was dropped when the conversation history was compressed.
- **New agent** — bootstrap a fresh agent with everything it needs to operate.
## How Agents Run It
`wh prime` is a normal CLI command, so any harness with shell access can run it. The goal is to feed its output into the agent's context once, early.
### Claude Code
Claude Code can run `wh prime` automatically with a [`SessionStart` hook](https://docs.claude.com/en/docs/claude-code/hooks), so every new session begins with WarmHub context loaded. Pair it with a `PreCompact` hook to re-run `wh prime` and restore context after the conversation is compacted:
```json
{
"hooks": {
"SessionStart": [{ "hooks": [{ "type": "command", "command": "wh prime" }] }],
"PreCompact": [{ "hooks": [{ "type": "command", "command": "wh prime" }] }]
}
}
```
### Other harnesses
Any agent harness with shell access can run `wh prime` and include the output in its system prompt or first message. Two common patterns:
- **System prompt** — run `wh prime` once at startup and prepend the output to the agent's system prompt.
- **First turn** — let the agent call `wh prime` as its first tool use, then continue with the result in context.
Use `--json` (below) when your harness ingests structured context rather than prose.
### No shell? Use the MCP or SDK surface
Agents that can't run a shell don't use `wh prime` directly. The MCP equivalent is `warmhub_capabilities`, which returns the same kind of orientation (concepts, workflows, wref syntax, and a pointer to the write contract — call `warmhub_repo_describe` for the full contract and shape-specific examples). SDK callers don't get a prose context dump — `client.diagnostics.capabilities()` returns version and feature-flag information for compatibility checks, not orientation prose; SDK agents read these docs instead. See the [MCP Tools Reference](/agent-integration/mcp-tools-reference/#warmhub_capabilities) for the MCP bootstrapping tool.
## Usage
```bash
# Markdown output (human-readable)
wh prime
# Structured JSON output
wh prime --json
```
### Flags
`wh prime` takes no command-specific flags. It accepts the standard global flags, of which two affect the output:
| Flag | Effect on `wh prime` |
|------|----------------------|
| `--json` | Emit structured JSON instead of Markdown (alias for `--format json`). |
| `--format pretty\|json\|jsonl` | Choose the output format. |
`wh prime` emits the same CLI bootstrap context regardless of `--repo` or `--profile` — it describes the CLI itself, not a specific repo's data. The Environment section reflects your configured default repo (from `WARMHUB_REPO` or `wh use org/repo`), and shows `No default repo` when none is set.
## Markdown Output
The default output includes:
- **Environment** — your default repo, if one is configured.
- **Core concepts** — thing, assertion, shape, write, and wref definitions.
- **Versioned things** — logical thing vs pinned version (`Shape/name` vs `Shape/name@vN`).
- **Wref quick reference** — local and canonical forms, version modifiers, batch tokens.
- **Key workflows** — read, write, and query command patterns.
Example excerpt:
```markdown
## Core Concepts
- **Thing**: A named entity versioned by write operations. **Assertion**: A claim about a thing with shape-validated data.
- **Shape**: Schema defining data structure. **Write**: One or more add/revise/retract operations with per-operation results.
- **wref**: Reference as `Shape/name` (e.g., `Player/alice`). Cross-repo: `wh:org/repo/Shape/name`.
## Versioned Things
- `Shape/name` identifies the logical thing. `Shape/name@vN` pins an exact version.
```
## JSON Output
`wh prime --json` returns the same context as a structured object — each domain lists its verbs with args, flags, aliases, and status, so a harness can parse the command surface directly:
```json
{
"version": "0.2.0",
"defaultOrg": null,
"defaultRepo": null,
"wrefSyntax": {
"localExamples": ["GameState", "GameState@v3", "Player/alice", "GameState/round-1/state"],
"canonicalFormat": "wh:org/repo/Shape/name",
"versionModifiers": ["@HEAD", "@vN", "@ALL"],
"batchTokens": { "allocate": "$N", "reference": "#N" }
},
"domains": [
{
"domain": "auth",
"summary": "Authentication management",
"verbs": [
{ "name": "login", "summary": "Log in via browser or --with-token for PATs", "args": "", "flags": ["--with-token", "--profile"], "status": "live" }
]
}
]
}
```
## Token Budget
The JSON output is approximately **4,800 tokens** (about 19 KB); the Markdown output is a bit smaller, around **3,500 tokens** (about 14 KB). Either way it leaves most of an agent's context window free for reasoning and conversation. Measure the current size yourself with `wh prime --json | wc -c` and divide by ~4 for a rough token count before you budget for it.
## Best Practice
Agents should run `wh prime` (CLI) or `warmhub_capabilities` (MCP) as their first action, then use targeted commands and tools for specific work. Loading context once up front avoids guessing at concepts or syntax and gives the agent accurate command contracts from the start.
---
# Why Agent-Native?
> Why WarmHub is agent-native and how AI agents connect.
WarmHub is designed as an **agent-native knowledge platform**. AI agents can read and write structured knowledge through standard protocols, with built-in attribution, versioning, and context bootstrapping. Every agent builds on what previous agents discovered — knowledge compounds across your work, your team, and your community instead of resetting at the end of each conversation.
## Agent-Friendly Properties
**MCP-first.** WarmHub exposes all operations via the [Model Context Protocol](/agent-integration/mcp-server/), so any MCP-compatible client (Claude, Cursor, custom agents) can discover and call tools automatically.
**Structured knowledge.** Shapes enforce schema on agent-written data. Agents can't write malformed records — the write path validates every operation.
**Attribution.** Every version records the authenticated user's email as `createdByEmail` and may also include an optional `committer` wref (e.g. `Agent/claude`) identifying the actor on whose behalf the write was made. When multiple agents collaborate on the same repo, you can always trace who wrote what.
**Immutable history.** Agents can't silently overwrite data. Every change creates a new version, and old versions are preserved. You can always answer "what did agent X assert at time T?"
**Context bootstrapping.** Agents orient in two steps. First, `wh prime` (CLI) or `warmhub_capabilities` (MCP) return static bootstrap context — concepts, wref syntax, and the command/tool surface. Then `warmhub_repo_describe` returns a repo-specific dump of shapes, sample data, and the write contract tailored to the repo's actual schema. See the [bootstrap surface map](#agent-bootstrap-surface-map) for the CLI/MCP/SDK equivalents.
**Batch operations.** Agents can submit multiple operations in one request. `$N`/`#N` placeholders let agents create entities and reference earlier operations in the same stream. See [operation tokens](/writes/operations/#token-system).
## Agent Bootstrap Surface Map
Every surface has the same two bootstrap steps — get static orientation, then load the repo's schema. The tools line up across CLI, MCP, and SDK:
| Step | CLI | MCP | SDK |
|------|-----|-----|-----|
| **Static orientation** — concepts, wref syntax, the command/tool surface | `wh prime` | `warmhub_capabilities` | Read these docs; `client.diagnostics.capabilities()` returns the backend API version, minimum supported SDK, and feature flags — not orientation prose |
| **Repo schema** — shapes, sample data, and stats for a specific repo | [`wh repo describe`](/cli-reference/commands/) | `warmhub_repo_describe` | `client.repo.get` + `client.shape.list` + `client.repo.getStats` |
Only `warmhub_repo_describe` (MCP) also returns the full write contract and generated write examples; the `wh repo describe` and SDK reads cover schema, shapes, and stats. MCP and SDK names are otherwise near-1:1 — see the [SDK and MCP method map](/agent-integration/mcp-tools-reference/#sdk-and-mcp-method-map).
## Integration Paths
### MCP Server (recommended)
For AI agents that support the Model Context Protocol:
1. Configure the MCP endpoint in your client
2. Agent discovers the MCP tool catalog automatically
3. Start with `warmhub_capabilities` for orientation, then `warmhub_repo_describe` for repo-specific schema and write examples
4. Use `warmhub_commit_submit` for writes, `warmhub_thing_query` for reads
See [MCP Server](/agent-integration/mcp-server/) for setup and [MCP Tools Reference](/agent-integration/mcp-tools-reference/) for the full tool catalog.
### TypeScript SDK
For custom agents and applications built in TypeScript:
1. Install `@warmhub/sdk-ts` (`react` is only needed if you use the React provider)
2. Create a `WarmHubClient` with your deployment URL
3. Use typed surfaces: `client.thing.head(...)`, `client.commit.apply(...)`
4. Use `OperationBuilder` for multi-operation submissions with client-side validation
See [SDK Overview](/sdk/overview/) for setup and [Client Surfaces](/sdk/client/) for the full API.
### CLI with --json
For agents with shell access:
1. Install `wh` CLI
2. Set `WARMHUB_REPO` environment variable
3. Run `wh prime --json` for structured context
4. Use `wh` commands with `--json` for machine-readable output
```bash
export WARMHUB_REPO=myorg/myrepo
wh prime --json # bootstrap context
wh thing list --json # read current state
wh commit submit --ops '...' # write data
```
See [Agent Context (wh prime)](/agent-integration/wh-prime/) for how harnesses invoke `wh prime`, its flags, and its token budget.
## Agent Workflow Pattern
A typical agent session:
1. **Bootstrap** — call `warmhub_capabilities` (MCP) or `wh prime` (CLI) for static orientation, then `warmhub_repo_describe` to load the repo's schema, existing data, and write contract
2. **Read** — use `warmhub_thing_head` for broad orientation, `warmhub_thing_query` for targeted lookups
3. **Write** — commit observations and decisions via `warmhub_commit_submit` (opinion-bearing assertions must be [binary propositions](/data-modeling/patterns/#opinions-as-separate-assertions))
4. **Iterate** — read updated state, make new assertions, revise existing ones
The describe tool dynamically generates write examples based on the repo's current shapes, so agents get correct field names and types without guessing.
For guidance on what to model and how — when to use assertions vs things, shape design, naming conventions, and common patterns — see [Modeling Overview](/data-modeling/overview/). For the underlying data model, see [Core Concepts](/get-started/core-concepts/).
---
# Access reference
> What each org role can do, what each scope grants, and the minimum scope for common tasks.
WarmHub access composes from your **org role** and the **scopes** that narrow it. Your role sets the ceiling on what you can do; a [personal access token](/auth/personal-access-tokens/)'s scopes narrow a token to a subset of that ceiling; and an org admin can attach [member scope overrides](/sdk/client/#clientorg) that narrow a member below their role on a given org or repo. A request is allowed only when your role permits it **and** every scope layer that applies to it covers it. This page is the canonical map of role → capability, scope → capability, and task → minimum scope.
## Roles and capabilities
Every member of an org has one role: **viewer**, **editor**, **admin**, or **owner**. Roles are cumulative — each includes everything the one before it can do. This table answers "what can an admin do that an editor can't?":
| Capability | viewer | editor | admin | owner |
|------------|:------:|:------:|:-----:|:-----:|
| Read repositories, things, assertions, and shapes (`repo:read`) | ✓ | ✓ | ✓ | ✓ |
| Read the org profile, members, and installed components (`org:read`) | ✓ | ✓ | ✓ | ✓ |
| Write — create, update, and rename things and shapes (`repo:write`) | | ✓ | ✓ | ✓ |
| Configure repos — subscriptions, credentials, actions, notifications, repo settings (`repo:configure`) | | | ✓ | ✓ |
| Administer repos — delete, archive, change visibility (`repo:admin`) | | | ✓ | ✓ |
| Configure the org — create repos, manage members, org settings, install components (`org:configure`) | | | ✓ | ✓ |
| Administer the org — rename, archive (`org:admin`) | | | | ✓ |
Owner and admin grant the **same repository access**; they differ only at the org level — **only an owner can rename or archive the org**. Member management has one further owner-only carve-out within `org:configure`: only an owner can assign or remove the **owner** role, and the last owner can't be removed or demoted. Admins manage all other members and roles.
## Scopes and what they grant
A token scope binds a [resource](/auth/personal-access-tokens/#scopes) to one or more of these permissions. Scopes are independent — `repo:write` does not include `repo:read` — and can only narrow access, never raise it above your role.
| Scope | Grants |
|-------|--------|
| `repo:read` | Read repositories, queries, things, and shapes |
| `repo:write` | Writes, shape mutations, and thing/shape renames |
| `repo:configure` | Subscriptions, credentials, action runs, notifications, and repo settings |
| `repo:admin` | Delete, archive, and change repository visibility |
| `org:read` | Read the org profile and members, see installed components, and list the org's repositories — including private ones — in org-level views; reading a repo's contents still needs `repo:read` |
| `org:configure` | Create repos, manage members and org settings, and install and manage [components](/sdk/component-identity/) |
| `org:admin` | Rename and archive the org |
## Minimum scope by task
The minimum scope a token needs for each common task. Anything not listed for a scope is not covered by it — request the narrowest scope that covers your task.
| Task | Minimum scope |
|------|---------------|
| Read things, assertions, shapes, or write history | `repo:read` |
| Submit a write (create or rename things and shapes) | `repo:write` |
| Read notifications | `repo:configure` |
| Create, update, pause, or remove a subscription | `repo:configure` |
| Read, lease, or deliver actions | `repo:configure` |
| Manage credentials — create, bind, grant, revoke | `repo:configure` |
| Rename a repo or change its settings | `repo:configure` |
| Delete, archive, or change a repo's visibility | `repo:admin` |
| Read an org profile or list its members | `org:read` |
| Read installed components | `org:read` |
| Create a repo in an org | `org:configure` |
| Add, remove, or change a member's role (assigning or removing **owner** requires owner) | `org:configure` |
| Install or manage a component | `org:configure` |
| Change org settings | `org:configure` |
| Rename or archive an org | `org:admin` |
Scopes are checked against your role: a token can carry `repo:write`, but the write still fails if your role is `viewer`. Use the [`role:` shorthand](/auth/personal-access-tokens/#role-shorthand) to mint a token that mirrors a whole role at once.
---
# Getting Access
> Create an account, log in, and manage your authentication state.
WarmHub handles sign-up and sign-in through a hosted login flow — there are no credentials to configure locally. The first time you sign in, your account is created automatically.
## Creating an Account
There is no separate registration step. The first time you log in (via the CLI or web app), WarmHub creates your account. Supported sign-in methods include:
- Email and password
- Google
- GitHub
- SSO (SAML/OIDC) — if configured for your organization
Your identity works across the CLI, web app, SDK, and HTTP API.
### Accepting an org invite
Org invites are sent to a specific email address. Sign in with an account that uses that same address — otherwise you land in a different account than the one that was invited.
- **Signing in with GitHub uses the primary email on your GitHub account.** If that primary email doesn't match the invited address, GitHub sign-in authenticates as the wrong identity, and GitHub offers no way to pick a different email for sign-in. When the invited address isn't your GitHub primary, sign in with **email and password** instead.
- **Each account is tied to a single email today.** A personal email and a work email are separate accounts — there's no way to consolidate them. Keep them distinct and sign in with the email that matches the org you're joining.
### Your personal org
First-time sign-in also provisions a **personal org** for you automatically — a namespace you own where you can create repos immediately without waiting to be added to a team org. WarmHub picks the slug as follows:
1. **GitHub login** — if you signed in with GitHub (or your identity exposes a GitHub login), WarmHub prefers that login as your personal org slug.
2. **Adoption** — if you already own an org whose name matches your GitHub login, WarmHub adopts it as your personal org rather than creating a duplicate.
3. **Suffixing** — if the preferred slug is taken by someone else, WarmHub appends a numeric suffix (`alice-2`, `alice-3`, …).
4. **Fallback** — if no usable login is available, the slug falls back to `user-` derived from your user ID.
Personal orgs are regular orgs in every other respect — you own them, you can create repos and invite members, and they appear in `wh org list`. Two constraints apply:
- **Rename.** Personal orgs linked to a GitHub login (including fallback slugs) can't be renamed with `wh org rename`. Personal orgs without a linked GitHub login (SSO or email-only) can be renamed normally.
- **Reserved names.** The following slugs are reserved and rejected on create and rename (case-insensitive): `admin`, `api`, `billing`, `blog`, `docs`, `help`, `login`, `public`, `settings`, `signup`, `status`, `support`, `system`, `warmhub`, `www`. This applies to all orgs, not just personal ones.
### Your public identity
When you sign in, WarmHub publishes a public identity for you — the identity your commits are attributed to, and the one other records point at when they name you as an author. It shows a display name derived from your account and **never includes your email address**.
Behind that, the identity is a record named `Identity/` in the globally readable `warmhub/users` repo, so it's visible in the graph and can be referenced from anywhere. It's published the first time you sign in and refreshed periodically; publishing is best-effort, so a delay never blocks sign-in or any write.
## CLI Authentication
### Log in
```bash
wh auth login
```
This opens your browser for a device authorization flow:
1. The CLI displays a verification URL and one-time code.
2. Your browser opens to the login page.
3. You authenticate and confirm the code.
4. The CLI receives a token and saves it to `~/.warmhub/auth.json`.
Tokens auto-refresh in the background for active device-flow sessions. You should only need to log in once per machine.
If your session is invalidated (for example, revoked server-side or an expired refresh token), the CLI surfaces a `Session expired` error and prompts you to run `wh auth login` again.
**Non-interactive environments** (CI, containers, headless servers):
For automation, use a [personal access token](/auth/personal-access-tokens/) via the `WH_TOKEN` environment variable instead of interactive login. See the [`wh token` commands](/cli-reference/commands/#token--personal-access-tokens) for creating tokens.
If you need to pipe a raw JWT (e.g., from another auth system):
```bash
echo "$JWT" | wh auth login --with-token
```
Piped tokens cannot auto-refresh — you'll need to provide a new one when it expires. PATs are the preferred approach for most non-interactive use cases.
### Named profiles
You can manage multiple credential sets with the `--profile` (`-P`) flag. Each profile stores its own tokens and the API endpoint used at login time.
```bash
# Log in with a named profile (e.g., for a separate account)
wh auth login --profile work
wh auth login --profile personal
# Use a profile for any command
wh thing list --repo myorg/myrepo --profile work
# Or set via environment variable
export WH_PROFILE=work
wh thing list --repo myorg/myrepo
```
Without `--profile` or `WH_PROFILE`, all commands use the `default` profile. Each profile remembers the API URL it was logged in with — that stored URL applies when the profile is active, unless you override it with `--api-url`. The `WARMHUB_API_URL` environment variable is the fallback when no profile is loaded (for example, on a fresh machine before login).
### Check status
```bash
wh auth status
```
Shows all profiles with their identity, auth method (device flow, piped token, or `WH_TOKEN` environment variable), token expiry, and API endpoint. Use `--profile ` to check a specific profile.
### Log out
```bash
wh auth logout
```
Removes the default profile's saved tokens from `~/.warmhub/auth.json`. Use `--profile ` to log out a specific profile. Other profiles are not affected. If `WH_TOKEN` is set as an environment variable, unset it separately.
## The `WH_TOKEN` Environment Variable
For scripts and automation, set `WH_TOKEN` to a [personal access token](/auth/personal-access-tokens/) or JWT:
```bash
export WH_TOKEN=eyJhbGciOi...
wh thing list # authenticates automatically
```
`WH_TOKEN` takes precedence over saved credentials from `wh auth login`. The CLI checks for it on every command.
## Token Types
All API requests authenticate via Bearer tokens in the `Authorization` header. WarmHub supports three token types:
| Type | Obtained via | Use case | Expires |
|------|-------------|----------|---------|
| **Session JWT** | `wh auth login` or web app sign-in | Interactive use | Short-lived (auto-refreshes) |
| **Personal access token** | `wh token create` or SDK `client.token.create` | Scripts, CI/CD, integrations | 30 days default, 1 year max |
| **MCP OAuth token** | Automatic (OAuth flow in MCP client) | MCP clients (Claude, Cursor, etc.) | Session-based |
All token types share a unified validation model — they go through the same server-side JWT verification and permission checking.
See [Personal Access Tokens](/auth/personal-access-tokens/) for creating and managing PATs.
## Repository Visibility
Repositories are either **public** or **private**. Visibility is set at creation time and can be changed by org administrators.
| | Public | Private |
|--|--------|---------|
| **Read (queries, HEAD, history)** | Anyone — no auth required | Authenticated, `repo:read` permission |
| **Write (commits, shape mutations)** | Authenticated, `repo:write` permission | Authenticated, `repo:write` permission |
| **Manage (subscriptions, credentials)** | Authenticated, `repo:configure` permission | Authenticated, `repo:configure` permission |
| **Anonymous read requests** | Return data normally | Return `404` |
All writes require authentication regardless of visibility. Private reads require `repo:read` permission on the target repo — sign-in alone is not enough; you need to be a member of the repo's org. PATs can narrow what a member is allowed to access, but they don't grant access on their own. Cross-repo reads to a private target apply the same check, so a canonical `wh:org/repo/...` wref only resolves when you can read the target repo.
The anonymous-access row covers ordinary reads (queries, HEAD, history). Some endpoints require management-level access even on a public repo: action observability (run history, attempts, live feed, notifications) needs `repo:configure`, so anonymous callers get an opaque `404` rather than data. See [Actions API](/http-api/actions/) for the full contract.
## Next Steps
- [Personal Access Tokens](/auth/personal-access-tokens/) — create tokens for scripts and CI/CD
- [HTTP API Authentication](/http-api/authentication/) — PAT endpoint reference
- [CLI Commands](/cli-reference/commands/) — full command list including `auth` and `token`
---
# Personal Access Tokens
> Create scoped tokens for scripts, CI/CD pipelines, and integrations.
Personal access tokens (PATs) let scripts, CI/CD pipelines, and integrations authenticate with WarmHub without interactive login.
## Overview
| Property | Detail |
|----------|--------|
| **Format** | Signed JWT |
| **Default expiry** | 30 days |
| **Maximum expiry** | 1 year (set via `--expires`) |
| **Management** | CLI (`wh token`) or SDK (`client.token.*`) |
| **Management auth** | Interactive session (manages all your tokens) or a PAT (manages only tokens in its own descendant subtree) |
## Token Lifecycle
```
Create → Use → [Expire] → Revoke
```
1. **Create** — Generate a named token via `wh token create`. The token value is displayed once — save it immediately.
2. **Use** — Set the `WH_TOKEN` environment variable or pass the token in the `Authorization: Bearer` header. WarmHub validates the token on every request.
3. **Expire** — Tokens expire after 30 days by default. Use `--expires` to set a custom duration (max 1 year). Expired tokens are rejected immediately.
4. **Revoke** — Revoke a token by name via `wh token revoke`. Takes effect immediately.
Expired and revoked tokens are preserved for audit visibility.
## Scopes
Scopes limit what a token can do. They are optional — a token created without scopes has the full permissions of its owning user.
Each scope entry binds a **resource** to a set of **permissions**:
| Tier | Resource format | Example | Meaning |
|------|----------------|---------|---------|
| Repo-scoped | `org/repo` | `myorg/myrepo` | Specific repo only |
| Org-level | `org` | `myorg` | All repos in org, capped by role |
| Global wildcard | *(omitted)* | | All resources, capped by role |
More specific entries take precedence. Scopes can only narrow access, never escalate beyond the user's role.
### Available permissions
Each `--scope` entry lists one or more permissions: `repo:read`, `repo:write`, `repo:configure`, `repo:admin`, `org:read`, `org:configure`, or `org:admin`. Scopes are independent — `repo:write` does not include `repo:read` — so request the specific permissions your token needs. For what each permission grants and the minimum scope per task, see the [access reference](/auth/access-reference/).
### Role shorthand
Instead of listing individual permissions, a scope entry can name a role with `role:`. The role expands to a fixed set of permissions when the token is created:
| Role | Repo permissions | Org-level entry also grants |
|------|------------------|-----------------------------|
| `role:owner` | `repo:read`, `repo:write`, `repo:configure`, `repo:admin` | `org:read`, `org:configure`, `org:admin` |
| `role:admin` | `repo:read`, `repo:write`, `repo:configure`, `repo:admin` | `org:read`, `org:configure` |
| `role:editor` | `repo:read`, `repo:write` | `org:read` |
| `role:viewer` | `repo:read` | `org:read` |
A `role:` entry requires a concrete repo or org resource — it is rejected on the global wildcard. (Omit `--scope` entirely for full user access.) On a repo-scoped entry, `role:owner` and `role:admin` grant the same permissions; they differ only on an org-level entry. Like any scope, a role can only narrow access, never escalate beyond your own role.
The role is expanded at creation time, so redefining a role later does not change tokens already issued.
## CLI Usage
You can run `wh token` commands from an interactive session — which manages all your tokens — or with a PAT, which manages only the tokens in its own descendant subtree (the tokens it created, and theirs). Component (action) tokens cannot manage tokens.
### Create a token
The `--scope` flag accepts three forms:
| Form | Meaning | Example |
|------|---------|---------|
| `org/repo=perms` | Repo-scoped | `--scope myorg/myrepo=repo:read,repo:write` |
| `org=perms` | Org-level (all repos in org) | `--scope myorg=repo:read` |
| `perms` | Global wildcard (all resources) | `--scope repo:read,repo:write` |
Separate multiple permissions with commas. Repeat `--scope` for multiple entries. In the repo-scoped and org-level forms, the permission list can be a [`role:` shorthand](#role-shorthand) instead of individual permissions.
```bash
# Full user access, default 30-day expiry
wh token create --name ci-bot
# Repo-scoped token
wh token create --name ci-bot --scope myorg/myrepo=repo:read,repo:write
# Role shorthand — same as myorg/myrepo=repo:read,repo:write
wh token create --name ci-bot --scope myorg/myrepo=role:editor
# Org-level wildcard (all repos in org, capped by role)
wh token create --name org-reader --scope myorg=repo:read
# Global wildcard with description and custom expiry
wh token create -n deploy --scope repo:write -d "Deploy pipeline" --expires 90d
# Multiple scopes — repeat --scope for each entry
wh token create --name ci-bot \
--scope myorg/private-repo=repo:read,repo:write \
--scope myorg=repo:read \
--expires 90d
```
### Name-restricted tokens with `--scopes-json`
For advanced use cases, the `--scopes-json` flag accepts a JSON array of scope entries with optional `allowedMatches` patterns that restrict which [thing](/data-modeling/things/) names the token can access. A thing name is the WarmHub object name portion of a wref, such as `Signal/temp-1` or `Config/settings`.
`--scopes-json` is mutually exclusive with `--scope` — you cannot use both in the same command.
Each entry in the JSON array has:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `resource` | string | No | Resource name: `"org/repo"`, `"org"`, or omitted (global wildcard) |
| `permissions` | string[] | Yes | Permissions for this resource |
| `allowedMatches` | string[] | No | Glob patterns restricting which thing names this entry can access. **Repo-scoped entries only** (`"org/repo"`). Ignored on org-level and global entries. |
`allowedMatches` behavior:
- **Repo-scoped entries only.** `allowedMatches` on org-level or global wildcard entries is stripped with a warning — name restrictions require a specific repo target.
- Patterns use glob syntax, for example `Signal/*` or `Config/**`.
- When present, the token can only read or write thing names that match at least one pattern.
- Omitting `allowedMatches` means no name restriction for that entry.
- An empty array (`[]`) is valid and means deny-all for that entry.
With `--scopes-json`, you can assign different `allowedMatches` to different permissions on the same resource by using multiple entries. Each `(resource, permission)` pair must be unique — duplicate pairs are rejected with a validation error.
```bash
# Read Signal/* and Config/*, but only write Signal/*
wh token create --name scoped-bot --scopes-json '[
{"resource":"myorg/myrepo","permissions":["repo:read"],"allowedMatches":["Signal/*","Config/*"]},
{"resource":"myorg/myrepo","permissions":["repo:write"],"allowedMatches":["Signal/*"]}
]'
# Simple name restriction — read-only for Signal/*
wh token create --name reader --scopes-json '[
{"resource":"myorg/myrepo","permissions":["repo:read"],"allowedMatches":["Signal/*"]}
]'
# No allowedMatches — same as --scope shorthand
wh token create --name full --scopes-json '[
{"resource":"myorg/myrepo","permissions":["repo:read","repo:write"]}
]'
```
The token is printed once. Save it to a secret store or environment variable immediately.
### Committer identity
A token can carry a default committer identity with `--committer-identity`. Writes made with the token are attributed to that identity unless a per-command `--committer` overrides it:
```bash
wh token create --name ci-bot --committer-identity wh:warmhub/users/Identity/
```
The value is an [identity](/auth/getting-access/#your-public-identity) wref. Without it, writes are attributed to your own user identity.
### List tokens
```bash
wh token list
wh token list --json
```
Shows all your tokens with their status (active, expired, or revoked) and scopes.
### View a token
```bash
wh token get --name ci-bot
```
Shows token details: name, description, scopes, status, creation date, and expiry.
### Revoke a token
```bash
wh token revoke --name ci-bot
```
Revocation is immediate. The token is rejected on the next API request.
## Using PATs with the HTTP API
Pass the token in the `Authorization` header:
```bash
curl -H "Authorization: Bearer eyJhbGciOi..." \
https://api.warmhub.ai/api/repos/myorg/myrepo/head
```
Or with the CLI via `WH_TOKEN`:
```bash
export WH_TOKEN=eyJhbGciOi...
wh commit submit --add cave --shape Location \
--data '{"x":3,"y":7}' -m "Add cave"
```
## Using PATs with MCP Clients
MCP clients like Claude and Cursor normally authenticate via OAuth automatically. However, PATs are useful when:
- Your organization restricts custom MCP connectors
- You're on WSL2, where the OAuth callback can't reach the browser
- You want to authenticate a dev instance without configuring OAuth
Use [`mcp-remote`](https://github.com/geelen/mcp-remote) as a stdio bridge with your PAT:
```json
{
"mcpServers": {
"warmhub": {
"command": "npx",
"args": [
"-y", "mcp-remote@latest",
"https://api.warmhub.ai/mcp",
"--header", "Authorization:${WH_TOKEN}",
"--transport", "http-only"
],
"env": {
"WH_TOKEN": "Bearer eyJhbGciOi..."
}
}
}
}
```
This works in both Claude Desktop (`claude_desktop_config.json`) and Claude Code (`.mcp.json`). The `--transport http-only` flag ensures HTTP Streamable transport.
See the [Quickstart](/get-started/quickstart/#connect-via-mcp) for standard OAuth-based setup.
## Security Model
- **No escalation.** A PAT can manage only the tokens in its own descendant subtree — the tokens it created, and theirs. It cannot list, read, or revoke tokens outside that subtree, so a leaked PAT can't reach the rest of your tokens. An interactive session manages all your tokens.
- **Scope enforcement.** Each API endpoint checks the token's scopes. A `repo:read` token cannot push commits.
- **Immediate revocation.** Revoking a token takes effect on the next request — there is no grace period.
- **Name restrictions.** Token names must be alphanumeric, hyphens, and underscores only (URL-safe).
## Limitations
- **No token rotation.** To rotate, revoke the old token and create a new one.
- **No web UI.** Tokens are managed via the CLI or SDK only.
- **Subtree-scoped under a PAT.** `wh token` and `client.token.*` accept an interactive session (all your tokens) or a PAT (only its own descendant subtree); component (action) tokens are rejected. To manage tokens outside a PAT's subtree, sign in with `wh auth login` (CLI) or use an interactive session before calling the SDK methods.
## Next Steps
- [Getting Access](/auth/getting-access/) — interactive login and account creation
- [HTTP API Authentication](/http-api/authentication/) — using PATs as Bearer tokens
- [CLI Commands](/cli-reference/commands/) — full `token` command reference
---
# Commands
> All CLI commands organized by domain — org, repo, shape, thing, assertion, commit, use, init, doctor, sub, auth, token, credential, component, prime, channel, notifications.
All commands follow the pattern `wh [args] [--flags]`. Global flags `--repo`, `--json`, `--live`, `--format`, and `--profile` (`-P`) are available on most commands. Pass `--no-update-check` on any invocation to skip the CLI update notice for that run.
`--format` controls output mode: `pretty` (default), `json`, or `jsonl`. When `--live` is active, `json` and `jsonl` emit structured output per update; `pretty` (or unset) renders a human-readable display.
## use — Local repo context
| Command | Description |
|---------|-------------|
| `wh use ` | Set the default repo for the current directory |
| `wh use` | Show the active repo context and where it comes from |
| `wh use --clear` | Remove the `.wh` context file |
Writes a `.wh` JSON file in the current directory. The CLI resolves the target repo using this priority: `--repo` flag > `WARMHUB_REPO` env > `.wh` file. The repo is validated against the backend before writing.
```bash
wh use myorg/world # writes .wh, validates repo exists
wh use # shows active context with provenance
wh use --clear # removes .wh
wh thing list # uses .wh repo (no --repo needed)
```
## org — Organization management
| Command | Description |
|---------|-------------|
| `wh org create [--display-name "..."] [--description "..."]` | Create a new organization. Description is trimmed; max 2000 characters. Requires an interactive browser sign-in — personal access tokens can't create organizations. |
| `wh org view ` | View organization details |
| `wh org list [--include-archived]` | List the organizations you belong to (archived hidden by default) |
| `wh org rename ` | Rename an organization |
| `wh org member add [--role role]` | Add a member or send an invite. Only owners can assign `owner` role. If the email is not a WarmHub user, a pending invite is created and an invite email is attempted (best-effort, async). |
| `wh org member remove ` | Remove a member or revoke a pending invite |
| `wh org member list [--pending]` | List members of an organization. Requires authentication and org membership. `--pending` filters to pending invites only. |
| `wh org member set-role --role ` | Change a member's role. Only owners can promote to or demote from `owner`. Cannot demote the last owner. |
| `wh org archive [--yes]` | Archive an organization (blocks new repos and members) |
| `wh org update --description "..."` | Update org description. Trimmed; empty strings clear the value; max 2000 characters. |
| `wh org unarchive ` | Unarchive an organization |
Roles: `owner` (full control), `admin` (manage members and repos), `editor` (read/write data), `viewer` (read-only). Default: `editor`.
### Reserved org names and personal orgs
`wh org create` and `wh org rename` both reject these reserved slugs (case-insensitive): `admin`, `api`, `billing`, `blog`, `docs`, `help`, `login`, `public`, `settings`, `signup`, `status`, `support`, `system`, `warmhub`, `www`.
When a user signs in for the first time, WarmHub provisions a **personal org** for them. GitHub-linked personal orgs use a login-based slug when available, and collisions receive a suffix. GitHub-linked personal orgs can't be renamed — `wh org rename` returns `FORBIDDEN`. Personal orgs without a linked GitHub login can be renamed normally.
```bash
wh org create acme --display-name "Acme Corp"
wh org list
wh org member add acme alice@example.com --role editor
wh org member list acme
wh org member remove acme alice@example.com
```
## repo — Repository management
| Command | Description |
|---------|-------------|
| `wh repo create [--description "..."] [--visibility ]` | Create a repo (auto-creates org if needed). Description is trimmed; max 2000 characters. |
| `wh repo list [org] [--include-archived] [--limit n] [--cursor c] [--all]` | List repos in an org (archived hidden by default). Results are paginated: by default the first page is returned with a notice when more exist. Use `--all` to fetch every page, or `--limit` / `--cursor` to page manually. |
| `wh repo search [--limit n] [--cursor c] [--all]` | Search **public repos across all orgs** by name, description, and shape vocabulary. Cross-org discovery (not org-scoped like `list`). |
| `wh repo view [org/repo]` | Show repo details |
| `wh repo describe [org/repo]` | Describe repo schema: shapes, fields, inline descriptions, and per-shape counts. Shapes from installed component manifests appear with count `0` when no things have been written yet. |
| `wh repo rename ` | Rename a repo |
| `wh repo archive [--yes]` | Archive a repo (blocks new commits) |
| `wh repo update --description "..."` | Update repo description. Trimmed; empty strings clear the value; max 2000 characters. |
| `wh repo visibility ` | Set repo visibility |
| `wh repo unarchive ` | Unarchive a repo |
| `wh repo delete [--hard] [--yes]` | Delete a repo. Soft by default (hidden immediately; purged after 30-day grace window). `--hard` runs the purge cascade immediately (owner-only, irreversible). Blocks if another repo holds inbound cross-repo references or subscriptions. |
`wh repo create` also accepts `--org ` as a tolerance fallback — `wh repo create --org ` is combined into `/` and the CLI prints a one-line hint pointing at the canonical positional form. Prefer `/` in scripts.
```bash
wh repo create myorg/world -d "Game world"
wh repo create myorg/world -d "Game world" --visibility public
wh repo list myorg
wh repo search users
```
## repo content — README, AGENTS.md, llms.txt
WarmHub stores per-repo markdown content as built-in `Content` instances. Two are stored and editable: `Content/Readme` (human-facing) and `Content/Agents` (agent-facing). A third, `Content/LlmsTxt`, is synthesized per request from the repo's shapes and content and is read-only.
| Command | Description |
|---------|-------------|
| `wh repo content get --kind readme\|agents\|llms-txt` | Fetch repo Content markdown by kind. |
| `wh repo content set --kind readme\|agents\|llms-txt [--content "..." \| --file ]` | Set Content markdown (file, inline, or piped stdin). `llms-txt` is read-only — `set` attempts are rejected. |
| `wh repo content prompt --kind readme\|agents` | Print an agent-ready prompt (bounded repo context + the follow-up `set` command) so your own agent can draft Content locally — no hosted LLM. `llms-txt` is read-only. |
`set` reads from `--file `, `--content "..."`, or stdin when neither flag is passed. Use `--file -` to force stdin.
```bash
wh repo content get myorg/world --kind readme
wh repo content set myorg/world --kind readme --file README.md
cat README.md | wh repo content set myorg/world --kind readme
wh repo content set myorg/world --kind agents --content "# Agents"
# Draft locally with your own agent, then save the result:
wh repo content prompt myorg/world --kind readme
wh repo content set myorg/world --kind readme --file README.md
wh repo content get myorg/world --kind llms-txt
```
## shape — Shape management
| Command | Description |
|---------|-------------|
| `wh shape list [--match "glob"] [--include-retracted] [--component id] [--exclude-components]` | List shapes. Use `--include-retracted` to show retracted shapes and `--exclude-components` to hide component-owned shapes. |
| `wh shape view [--include-retracted]` | Show shape details with field definitions |
| `wh shape create --fields '' [--description '...']` | Create a new shape |
| `wh shape revise --fields '' [--description '...']` | Revise shape fields (creates new version) |
| `wh shape retract [--reason ''] [-m msg] [--committer ]` | Retract a shape, marking it inactive |
| `wh shape history [--include-retracted] [--limit n] [--cursor c] [--all]` | Show shape version history |
| `wh shape rename ` | Rename a shape |
Teaching rejection: `wh shape delete ` exits with a hint to use `wh shape retract`.
```bash
wh shape create Location --fields '{"x":"number","y":"number"}'
wh shape list
wh shape view Location
wh shape revise Location --fields '{"x":"number","y":"number","label":"string"}'
wh shape retract OldShape -m "Withdraw old shape"
```
## thing — Thing operations
| Command | Description |
|---------|-------------|
| `wh thing list [--shape s] [--kind k] [--limit n] [--cursor c] [--all] [--match glob] [--include-retracted] [--count] [--component id] [--exclude-components]` | Show current HEAD state. Use `--count` to return only the count of matching items. System-managed component infrastructure records are hidden by default. Use `--exclude-components` to also hide component-owned data things. |
| `wh thing create [--shape s] --data '' [-m msg] [--committer ]` | Create a thing. Pass either a qualified `Shape/name` or an unqualified name with `--shape`, not both. |
| `wh thing view [...] [--file ] [--version n] [--depth n] [--include-retracted]` | Show a thing's details. Variadic: a single wref routes to the single-thing path; multiple wrefs or `--file` routes to batch fetch (max 500). `--depth` returns an embedded graph view and cannot be combined with `--live`. Pretty output includes a `by:` row with the committer wref when the version was written with `--committer`. Batch returns `{ requested, items, missing }` in `--json`; `--format jsonl` emits one record per deduped requested wref. |
| `wh thing history [wref] [--shape] [--about] [--resolve-collections] [--limit] [--cursor] [--all] [--include-retracted]` | Show version history. Each version line ends with `by ` when attribution is recorded; the field is also exposed on `--json` output as `versions[].committerWref`. |
| `wh thing resolve ` | Resolve wref to thing identity |
| `wh thing lease [--ttl ]` | Acquire a short [read lease](#thing-read-leases) on a thing and read it in one step. Prints the lease id and expiry. `--ttl` defaults to `5000` (min `1000`, capped at a maximum); a value outside that range is rejected, never clamped. |
| `wh thing revise --data '' [-m msg] [--committer ] [--expected-version ] [--lease-id ]` | Revise a thing. `--committer` is optional; when omitted, the commit is attributed to your token's bound committer identity (set via `wh token create --committer-identity`) if one is configured, otherwise to your authenticated user identity. Pass `--expected-version` to apply the revise only if the thing is still at that version (optimistic concurrency). Pass `--lease-id` to write under a [read lease](#thing-read-leases); the lease auto-releases on a successful or no-op write. |
| `wh thing retract [--kind thing\|assertion\|shape\|collection] [--reason ''] [-m msg] [--committer ] [--lease-id ]` | Retract an entity (thing, assertion, shape, or collection), marking it inactive. `--kind` is an optional safety check that errors if the resolved entity's kind doesn't match. `--committer` is optional; when omitted, the commit is attributed to your token's bound committer identity (set via `wh token create --committer-identity`) if one is configured, otherwise to your authenticated user identity. `--lease-id` writes under a [read lease](#thing-read-leases). |
| `wh thing release-lease --lease-id ` | Release a [read lease](#thing-read-leases) early when you decide not to write. Idempotent — a non-matching or already-released lease is not an error. |
| `wh thing query [--shape s] [--kind k] [--about wref] [--resolve-collections] [--limit n] [--cursor c] [--all] [--match glob] [--include-retracted] [--count] [--component id] [--exclude-components]` | Query by filters. Use `--count` to return only the count. When no `--shape` is given, system-managed component infrastructure records are hidden by default. |
| `wh thing search [--shape s] [--kind k] [--about wref] [--mode text\|vector\|hybrid] [--resolve-collections] [--include-retracted] [--limit n] [--cursor c] [--all] [--component id] [--exclude-components]` | Search by text content. `--cursor` and `--all` only apply in text mode. `--component` and `--exclude-components` only apply in text mode. In text mode, system-managed component infrastructure records are hidden by default when no `--shape` is given. `--resolve-collections` also only applies in text mode. |
| `wh thing refs [--inbound\|--outbound] [--field path] [--limit n --cursor c] [--all]` | Show things that store this wref in a field (inbound, default) or wrefs stored in this thing's fields (outbound). Use `wh thing about` when you want assertions whose `about` target is this thing. `--field` filters inbound refs by field path. `--cursor` requires `--limit` (cursors are scoped to a specific page size). |
| `wh thing about [--shape s] [--match glob] [--depth n] [--resolve-collections] [--limit n --cursor c] [--all] [--include-retracted]` | Show assertions whose `about` target is this thing. Use `--shape` or `--match` to narrow the assertions, `--depth` to include child assertions about the returned assertions, and `--resolve-collections` to include assertions about collections containing the target. `--cursor` requires `--limit` (cursors are scoped to a specific page size). |
| `wh thing rename ` | Rename a thing |
```bash
wh thing list --shape Location
wh thing view Location/cave --version 3
wh thing view Location/cave --depth 2
wh thing query --kind assertion --about Location/cave
wh thing search "safe location" --shape Observation --mode hybrid
wh thing history Location/cave --limit 10
wh thing refs Location/cave # inbound refs (default)
wh thing refs Location/cave --field target # filter by field path
wh thing refs Observation/obs1 --outbound # outbound refs
wh thing about Location/cave # assertions about this thing
wh thing about Location/cave --shape Observation # filter assertions by shape
wh thing about Location/cave --depth 2 # include child assertions
wh thing list --count # count all active items
wh thing list --shape Location --count # count by shape
wh thing query --about Location/cave --count # count matching query results
# wh thing view is variadic — batch fetch up to 500 wrefs in one call
wh thing view Player/alice Player/bob Player/cara # variadic positionals
cat wrefs.txt | wh thing view # piped stdin (one wref per line)
wh thing view --file wrefs.txt --version 3 # pin all to @v3 (implies --include-retracted)
wh thing view Player/alice@v1 Player/bob@v2 # per-wref pinning
wh thing view --file=- < wrefs.txt --format jsonl # `--file=-` reads stdin (use the `=`; `--file -` errors with `Flag --file requires a value`)
```
Batch `wh thing view` (multiple wrefs or `--file`) supports three output modes:
- **Pretty** (default): `Requested N, found N, missing N` header, then one line per resolved item with kind label and `[RETRACTED]`/`[draft]` markers, then a `Missing:` block listing wrefs that didn't resolve.
- **`--json`** (or `--format json`): a single `{ requested, items, missing }` object — `items[]` carries the same fields as single-thing view, `missing[]` is `string[]` of unresolved wrefs (qualified when applicable).
- **`--format jsonl`**: one JSON record per *deduped* requested wref, in input order. Inputs from positionals + `--file` + stdin are unioned and deduped before the round-trip, so a wref supplied twice produces one row. Each row is `{requested, found, wref, ...}`. The fields below distinguish two [wref](/data-modeling/wrefs/) shapes — the **local form** (`Shape/name`, repo-relative) and the **canonical form** (`wh:org/repo/Shape/name`, fully qualified for cross-repo reads):
- `requested` — the original input string verbatim, so canonical inputs like `wh:org/repo/Loc/cave` are still identifiable downstream after the backend normalizes them to local form.
- `wref` — the local form on hits (e.g. `Loc/cave` even when the input was canonical), or the version-qualified form from `result.missing` on misses.
- `found` — boolean.
Correlate input lines to result lines by `requested` and filter by `found` without reconstructing the envelope.
Passing `--version` automatically implies `--include-retracted`, so retract versions can be retrieved by their pinned id.
`--live` is rejected in batch mode — batch reads are one-shot. Use `wh thing view --live` for per-thing polling.
### Thing read leases
A read lease gives you a short, exclusive window on a single thing for a read-modify-write cycle. While you hold the lease, another caller's `revise`/`retract` of that thing fails fast with a recoverable `LEASE_UNAVAILABLE` error. You learn about the conflict *before* computing a new value, not after the write. Leasing is opt-in: plain `wh thing view` reads are unaffected, and an expired or released lease falls back to a normal optimistic write.
```bash
# 1. Acquire + read in one step (prints lease id and expiry).
wh thing lease Player/alice --ttl 5000
# 2. Compute the new value, then write it back under the lease.
# The lease auto-releases on a successful or no-op write.
wh thing revise Player/alice --data '{"score":2}' --lease-id
# Or hand the lease back early if you decide not to write.
wh thing release-lease Player/alice --lease-id
```
Notes:
- `wh thing lease` is the acquire verb; the plain read verb stays `wh thing view`. Acquiring requires write access. If another caller already holds an active lease, it fails fast with `LEASE_UNAVAILABLE` instead of waiting.
- TTL defaults to 5s and has a fixed 1s floor and an upper bound. An out-of-range `--ttl` is rejected, never silently clamped.
- On [`wh commit submit`](/cli-reference/write-submit-deep-dive/), `--lease-id` binds to a single `--revise`/`--retract` short-form op and is broadcast to every op of a multi-target `--retract`. For `--ops`/`--file`/`--stream` writes, carry `leaseId` inline on the operation instead.
- A `LEASE_UNAVAILABLE` error tells you when the current holder's lease expires, so you can retry after that time.
### Durable ids on `thing` reads
A [durable id](/data-modeling/wrefs/#durable-ids) is accepted anywhere a wref is. Because a durable id routes itself, `--repo` is optional on `wh thing view`, `history`, `resolve`, `refs`, and `about` when the target is a bare durable id:
```bash
wh thing view # no --repo needed
wh thing history
wh thing about
```
`wh thing graph` and `wh thing view --depth` still require `--repo` — graph expansion follows neighbour wrefs that need a repo to resolve. A `wh:`-prefixed durable id also needs `--repo`; pass the bare form to route without one.
## thing graph — Embedded graph reads
`wh thing graph` is a sub-command of the `thing` domain. It returns a thing with readable `about` assertions and wref fields embedded as objects. Depth defaults to 2 and is capped at 5. Only data the caller can read is resolved — inaccessible refs stay as string wrefs and the output does not expose internal IDs or denial reasons.
```bash
wh thing graph Location/cave
wh thing graph Location/cave --depth 3
wh thing graph Location/cave --depth 2 --json
```
## assertion — Assertion operations
| Command | Description |
|---------|-------------|
| `wh assertion list [--about wref] [--shape s] [--depth n] [--limit n] [--cursor c] [--all] [--match glob] [--resolve-collections] [--include-retracted] [--count]` | Browse assertions in HEAD, optionally scoped to assertions about a thing. Use `--count` to return only the count. |
| `wh assertion view [--version n] [--depth n] [--include-retracted]` | Show an assertion's details (equivalent to `wh thing view` — assertions are things). `--depth` returns an embedded graph view instead of the flat assertion record and cannot be combined with global `--live`. |
| `wh assertion create --shape --about [--name n] --data '' [-m msg] [--committer ]` | Create an assertion. `--about` accepts either a single wref or a collection (`pair:a,b`, `triple:a,b,c`, `set:a,b,...`, `list:a,b,...`). |
| `wh assertion revise --data '' [-m msg] [--committer ]` | Revise an assertion |
| `wh assertion retract [--reason ''] [-m msg] [--committer ]` | Retract an assertion, marking it inactive |
| `wh assertion history [--include-retracted] [--limit n] [--cursor c] [--all]` | Show assertion version history |
Prefer `--about` for target-scoped assertion lists. A bare target after `list` is accepted as an agent convenience when translating user phrasing like "assertions about X".
`wh thing view ` also works since assertions are things.
```bash
wh assertion create --shape Observation --name cave-safe --about Location/cave --data '{"safe":true}'
wh assertion create --shape Pairing --about pair:Location/cave,Location/lake --data '{"connected":true}'
wh assertion list --about Location/cave --shape Observation
wh assertion view Observation/cave-safe --depth 2
wh assertion revise Observation/cave-safe --data '{"safe":true,"confidence":0.9}'
wh thing view Observation/cave-safe
wh assertion list --shape Observation
wh assertion list --count
wh assertion history Observation/cave-safe --include-retracted
```
## commit — Write operations
| Command | Description |
|---------|-------------|
| `wh commit submit [--ops json] [-f file] [--stream] [--stream-id id] [--skip-existing] [--add name] [--revise wref] [--retract wref] [--reason text] [flags]` | Submit a stream of operations. Bare `wh commit` is equivalent. |
`wh commit submit` is the write entrypoint. Use `wh thing history ` for
the per-thing version trail.
See [Commit Submit Deep-Dive](/cli-reference/write-submit-deep-dive/) for the full breakdown of `commit submit` flags, including JSONL streaming with `--stream` / `--file *.jsonl` (both require `--stream-id` and `--skip-existing`), plus `--progress` and `--chunk-size`.
```bash
wh commit submit --add cave --shape Location --data '{"x":3,"y":7}' -m "Add cave"
wh commit submit --retract Location/old-cave --reason "duplicate" -m "Retract duplicate cave"
wh commit submit --add alice --data '{"score":1}' --add bob --data '{"score":2}' --shape Player -m "seed players"
wh commit submit --file dataset.jsonl --stream-id bulk-import --skip-existing --progress -m "Bulk import"
wh commit submit --file dataset.jsonl --stream-id bulk-import --skip-existing -m "Resume seed"
cat ops.jsonl | wh commit submit --stream --stream-id pipe-stream --skip-existing -m "Pipe stream"
```
`--add` is repeatable up to 20 operations; pair each with its own `--data`. See [Commit Submit Deep-Dive](/cli-reference/write-submit-deep-dive/#add-multiple-things-in-one-write-request) for cardinality rules.
## shape template — Scaffold write operations
`wh shape template` is a sub-command of the `shape` domain. It generates sample operations from shape definitions for use with `wh commit submit`.
| Command | Description |
|---------|-------------|
| `wh shape template [shape2 ...] [--operation add\|revise\|retract] [--kind thing\|assertion\|shape\|collection] [--about wref] [--count n] [--output file]` | Generate sample operations from shapes. `--kind shape` and `--kind collection` are only valid with `--operation retract`; `add`/`revise` accept `thing` or `assertion`. |
```bash
wh shape template Hypothesis
wh shape template Hypothesis --operation retract
wh shape template Hypothesis Evidence --count 3 -o experiment.json
wh shape template Hypothesis --kind assertion --about ResearchTopic/example
```
## init — Bootstrap local harness
| Command | Description |
|---------|-------------|
| `wh init [org/repo] [-d "description"]` | Install harness hooks and optionally onboard a repo |
Aliases: `init` passes unknown verbs as positional args, so `wh init myorg/myrepo` works directly.
```bash
wh init
wh init myorg/myrepo
wh init myorg/myrepo --description "Demo repo"
```
## doctor — Diagnostics and health checks
| Command | Description |
|---------|-------------|
| `wh doctor [--fix]` | Run environment and backend health checks |
Checks API URL, default repo, backend connectivity, authenticated identity (email plus server-validated scopes and token name for PATs), Claude hooks, and AGENTS.md. If the backend doesn't recognize your token — for example, when it was issued for a different API URL — the `auth` check warns, shows the API URL it tried, and suggests running `wh auth login`. The auth check is skipped when the backend is unreachable. Use `--fix` to auto-install missing hooks and stanzas.
```bash
wh doctor
wh doctor --fix
```
## sub — Subscription management
See [Subscriptions](/subscriptions/overview/) for concepts (filters, retry behavior) and [Creating Subscriptions](/subscriptions/creating/) for detailed setup guides.
| Command | Description |
|---------|-------------|
| `wh sub create --on --filter '' --webhook-url [flags]` | Create an event-driven webhook subscription bound to a target shape |
| `wh sub create --filter '{"kind":"shape", ...}' --webhook-url [flags]` | Create a [shape lifecycle subscription](/subscriptions/filter-json/#shape-lifecycle-subscriptions) — `--on` is omitted because the filter scopes matching to shape ops |
| `wh sub update [flags]` | Update an existing subscription |
| `wh sub view ` | View subscription details |
| `wh sub list [--limit n]` | List all subscriptions |
| `wh sub log [--limit n]` | Tail subscription delivery feed (shows run status and errors) |
| `wh sub attempts ` | View per-attempt detail for a specific run |
| `wh sub pause ` | Pause a subscription |
| `wh sub resume ` | Resume a paused subscription |
| `wh sub delete ` | Delete a subscription |
| `wh sub bind --credentials ` | Bind a credential set for webhook auth |
| `wh sub unbind ` | Remove credential binding |
**Delivery / fallback flags**
| Flag | Description |
|------|-------------|
| `--on` | Target shape — required for most webhook subscriptions; omit for [shape lifecycle subscriptions](/subscriptions/filter-json/#shape-lifecycle-subscriptions) whose filter is `{"kind":"shape", ...}` |
| `--filter` | JSON filter expression |
| `--webhook-url` | Destination URL for webhook deliveries |
| `--fallback-webhook-url` | Optional fallback URL called after a terminal delivery failure |
| `--clear-fallback-webhook-url` | Remove the configured fallback URL on `wh sub update` |
**Webhook flags**
| Flag | Description |
|------|-------------|
| `--on` | Target shape — required for most webhook subscriptions; omit for [shape lifecycle subscriptions](/subscriptions/filter-json/#shape-lifecycle-subscriptions) whose filter is `{"kind":"shape", ...}` |
| `--filter` | JSON filter expression |
| `--allow-trace-reentry` | Allow same-chain reentry (write-triggered only) |
**Source flag**
| Flag | Description |
|------|-------------|
| `--source` | Watch a different repo in the same org; accepts `repoName` or `orgName/repoName`. |
`--source` pins the subscription to watch a different repo in the same organization. When set, the subscription lives in the repo specified by `--repo` but fires on writes to the source repo. The action runs in the context of the subscription's home repo.
`--allow-trace-reentry` defaults to `false`. When omitted, a write-triggered subscription runs at most once in the same causal chain for the same shape. When present, same-chain reentry is allowed, but a global chain-depth safety limit still stops runaway recursion.
```bash
# Create a webhook subscription
wh sub create signal-hook --on Signal --kind webhook \
--filter '{"shape":"Signal"}' --webhook-url https://example.com/hook \
--fallback-webhook-url https://example.com/fallback
# Create a shape lifecycle subscription (no --on; filter scopes to shape ops)
wh sub create shape-changes --kind webhook \
--filter '{"kind":"shape"}' --webhook-url https://hooks.example.com/shapes
# Create a cross-repo subscription (watch signals in another repo)
wh sub create cross-hook --on Signal --kind webhook \
--filter '{"shape":"Signal"}' --source myorg/other-repo \
--webhook-url https://example.com/hook
# Allow same-trace reentry for a self-chaining webhook subscription
wh sub create signal-loop --on Echo --kind webhook \
--filter '{"shape":"Echo"}' --webhook-url https://example.com/hook \
--allow-trace-reentry
# Update a webhook subscription
wh sub update signal-hook --on Signal \
--filter '{"shape":"Signal"}' \
--webhook-url https://example.com/new-hook
# Clear a fallback webhook URL
wh sub update signal-hook --clear-fallback-webhook-url
# List and inspect
wh sub list
wh sub view signal-hook
# Tail delivery feed (shows run status, attempt count, errors)
wh sub log signal-hook
wh sub log signal-hook --live
# Drill into a specific run's attempts
wh sub attempts 019d90f0-0000-7000-8000-000000000000
# Credential binding
wh sub bind signal-hook --credentials webhook-keys
wh sub unbind signal-hook
```
`wh sub attempts` expects the full dashed run UUID shown by `wh sub log`. MCP and HTTP action-attempt surfaces use the same canonical run ID.
## notifications — Action notifications
| Command | Description |
|---------|-------------|
| `wh notifications [--limit n] [--since ts]` | List repo-scoped action failure notifications |
| `wh notifications [--limit n] [--since ts]` | Explicit form of the same command |
These commands return repo-scoped terminal failure notification records for action deliveries. They are distinct from the web app's cross-repo user notification feed.
`--since` accepts either epoch milliseconds or an ISO timestamp.
```bash
wh notifications
wh notifications --since 2026-03-30T12:00:00Z
wh notifications --limit 10
```
## auth — Authentication
| Command | Description |
|---------|-------------|
| `wh auth login` | Log in via browser (device authorization) |
| `wh auth login --with-token` | Log in with a JWT piped via stdin |
| `wh auth login --profile ` | Log in to a named profile |
| `wh auth logout` | Log out the default profile |
| `wh auth logout --profile ` | Log out a specific profile |
| `wh auth status` | Show all profiles and their auth state |
| `wh auth status --profile ` | Show auth state for a specific profile |
| `wh auth whoami` | Show the authenticated identity for all profiles |
| `wh auth whoami --profile ` | Show the authenticated identity for a specific profile |
The `--profile` (`-P`) flag selects which named credential set to use. Without it, commands use the `default` profile. Each profile stores its own tokens and the API endpoint used at login time.
If you pass a named `--profile` that doesn't exist in `~/.warmhub/auth.json`, the CLI fails early with a clear error that names the missing profile, lists your available profiles, and suggests `wh auth login --profile ` to create it. (The `default` profile still returns the standard "not logged in" path when missing, so interactive users aren't forced through a login hint on first run.)
```bash
# Default profile
wh auth login
wh auth status
wh auth logout
# Named profiles (e.g. for different environments)
wh auth login --profile staging --api-url https://api.example.com
wh auth login --profile prod
wh auth status --profile staging
wh auth logout --profile staging
# Piped token into a named profile
echo "$TOKEN" | wh auth login --with-token --profile ci
# Use a profile for any command
wh thing list --repo myorg/myrepo --profile staging
```
See [Getting Access](/auth/getting-access/) for the full authentication guide.
## token — Personal access tokens
| Command | Description |
|---------|-------------|
| `wh token create --name [--scope scope]... [--scopes-json json] [-d desc] [--expires duration] [--committer-identity wref]` | Create a new PAT |
| `wh token list [--all]` | List your active tokens (interactive: all of them; PAT: only its own descendant subtree). Add `--all` to include expired and revoked tokens. |
| `wh token get --name ` | View a token's details |
| `wh token revoke --name ` | Revoke a token |
`token` commands run from an interactive session (which manages all your tokens) or under a PAT (which manages only the tokens it created, and tokens those created in turn); component (action) tokens are rejected. `--scope` and `--scopes-json` are mutually exclusive — use one or the other, not both.
```bash
# Repo-scoped token
wh token create --name ci-bot --scope myorg/myrepo=repo:read,repo:write
# Role shorthand — expands to the role's permissions (role:owner|admin|editor|viewer)
wh token create --name ci-bot --scope myorg/myrepo=role:editor
# Org-level wildcard (all repos in org, capped by role)
wh token create --name org-reader --scope myorg=repo:read
# Global wildcard (all resources, capped by role)
wh token create -n deploy --scope repo:write -d "Deploy pipeline" --expires 90d
# Multiple scopes — repeat --scope for each entry
wh token create --name ci-bot \
--scope myorg/private-repo=repo:read,repo:write \
--scope myorg=repo:read \
--expires 90d
# Bind a default committer identity (stamped on writes unless --committer overrides per call)
wh token create --name ci-bot --committer-identity wh:warmhub/users/Identity/
# Full access (no --scope flag)
wh token create --name full-access
# Name-restricted scopes (--scopes-json, mutually exclusive with --scope)
wh token create --name scoped-bot --scopes-json '[
{"resource":"myorg/myrepo","permissions":["repo:read"],"allowedMatches":["Signal/*"]}
]'
wh token list
wh token get --name ci-bot
wh token revoke --name ci-bot
```
See [Personal Access Tokens](/auth/personal-access-tokens/) for scopes, lifecycle, and security details.
## credential — Credential sets
Credential sets store named keys that subscriptions and actions can bind to. Key names are visible in `list`/`view`, but values are write-only — once set, they can't be read back through the CLI.
| Command | Description |
|---------|-------------|
| `wh credential create [--repo org/repo \| --org org] [--scope org\|repo] [--description "..."]` | Create an empty credential set. Org-scoped sets are available across repos in the org; repo-scoped sets are repo-only. |
| `wh credential list [--repo org/repo \| --org org]` | List credential sets accessible from a repo or org |
| `wh credential view [--repo org/repo \| --org org]` | View a credential set (key names only, no values) |
| `wh credential set [--repo org/repo \| --org org] [--value ]` | Add or update a single key. Reads value from `--value` or stdin. |
| `wh credential set [--repo org/repo \| --org org]` | Bulk add or update keys. Reads a JSON object from stdin, e.g. `{"KEY":"val"}`. |
| `wh credential unset [--repo org/repo \| --org org]` | Remove a key |
| `wh credential audit [--repo org/repo \| --org org] [--limit n]` | View the audit log for a credential set |
| `wh credential revoke [--repo org/repo \| --org org] [--reason "..."]` | Revoke the set (blocks new binds and strips auth from existing webhook deliveries) |
| `wh credential delete [--repo org/repo \| --org org]` | Delete a credential set |
## component — Component management
| Command | Description |
|---------|-------------|
| `wh component init ` | Scaffold a new component repository |
| `wh component install com.warmhub.identity --repo org/repo` | Install the bundled Identity system component into an existing repo. Bundled system components do not require registry registration or a component package. |
| `wh component install ` | Install a registered component identity. The CLI resolves the latest published manifest from the registry and runs optional setup. Install accepts registered `/` refs and bundled system ids; register your own component first with `wh component register`. |
| `wh component list [--limit n] [--cursor c] [--all]` | List installed components. Paginated: pass `--all` to auto-fetch every page, or `--limit` / `--cursor` to page manually. |
| `wh component search [--limit n] [--cursor c] [--all]` | Search **public registered components across all orgs** by name, description, and manifest vocabulary (shapes, CLI methods, subscriptions). Always cross-org — to list components *installed* in a repo, use `wh component list --repo`. |
| `wh component view ` | Show component details |
| `wh component exec [method flags] --repo org/repo` | Invoke a CLI method an installed component exposes (the methods declared in its manifest [`cli`](/components/manifest-reference/#cli) block). Method-specific flags are validated against the installed manifest snapshot. Shorthand: `wh `. |
| `wh component validate ` | Validate a component package without installing |
| `wh component update ` | Re-resolve a registered component's latest published manifest and replay setup, rotating minted setup/runtime tokens. |
| `wh component doctor ` | Run health checks on an installed component |
| `wh component teardown ` | Pause subscriptions and mark the component as torn down (non-destructive — shapes and data are preserved) |
| `wh component register --org --manifest [flags]` | Register a component identity in an org from a local manifest file so it can be installed via `install `. `--manifest` is required; `--source-url` is optional documentation metadata. |
| `wh component unregister ` | Remove a registered component identity |
| `wh component registry list --org ` | List registered components in an org |
| `wh component registry view ` | View one registered component |
| `wh component registry update [flags]` | Update a registered component |
Components are declarative packages that bundle shapes, subscriptions, credentials, and seed data into a single installable unit. Each component is identified by a unique `componentId` and owns the resources it creates — external writes to component-owned shapes, things, and assertions are blocked.
```bash
wh component init my-component
wh component register veritas --org warmhub --manifest ./warmhub/manifest.json
wh component install warmhub/veritas --repo myorg/world
wh component registry list --org warmhub
wh component search reputation
wh component list --repo myorg/world
wh component view my-component --repo myorg/world
wh component validate ./local-component
wh component doctor my-component --repo myorg/world
wh component teardown my-component --repo myorg/world
```
## prime — Agent context
| Command | Description |
|---------|-------------|
| `wh prime` | Print CLI context in markdown |
| `wh prime --json` | Print CLI context as structured JSON |
See [wh prime](/agent-integration/wh-prime/) for details on output format and token budget.
## channel — Real-time repo events in Claude Code
| Command | Description |
|---------|-------------|
| `wh channel --repo [--repo ...] [--profile name]` | Start an MCP channel server that pushes repo events into a Claude Code session |
`wh channel` runs as an MCP server subprocess. Claude Code receives a notification each time a commit is applied or an action run status changes in any watched repo. Each notification includes the affected shapes and things.
Pass `--repo` multiple times to watch several repos in parallel from one channel server. Use `--profile` to select a named auth profile.
**Setup**
Register the channel as an MCP server using the Claude Code CLI:
```bash
claude mcp add warmhub -- wh channel --repo org/repo
```
Or add it manually to your project's `.mcp.json`:
```json
{
"mcpServers": {
"warmhub": {
"command": "wh",
"args": ["channel", "--repo", "org/repo"]
}
}
}
```
During the research preview, start Claude Code with the `--dangerously-load-development-channels` flag:
```bash
claude --dangerously-load-development-channels server:warmhub
```
**Example usage**
```bash
# Watch a repo and push events into Claude Code
wh channel --repo acme/world
# Watch multiple repos in parallel
wh channel --repo acme/world --repo acme/datasets
# With a named auth profile
wh channel --repo acme/world --profile staging
```
**Notification format**
When a commit is applied, Claude sees:
```
Commit applied to acme/world — affected Player/alice, Score/round-1 (shapes: Player, Score)
```
When an action run status changes:
```
Action updated in acme/world — affected Player/alice (shapes: Player)
```
**Limitations**
- Claude Code only — not supported in Claude Desktop, claude.ai, or the web app
- Events are one-way; use existing `wh` commands or MCP tools to query data
- Requires `--dangerously-load-development-channels` during the research preview
---
# CLI Overview
> Install the wh CLI, configure your environment, and learn the command patterns.
The `wh` CLI is the primary human interface for interacting with WarmHub. It covers all operations — creating repos, writing data, querying state, and bootstrapping agent context.
## Installation
The CLI requires **Node 22+**.
```bash
npm install -g @warmhub/cli
wh --version
```
After installing, `wh` is available everywhere. See the [Quickstart](/get-started/quickstart/#install-the-cli) for the full walkthrough.
## Everyday Use
Use `wh use` to set the repo for a working directory, pass `--repo org/name` when a script should be explicit, and add `--json` when another tool needs structured output. Most commands accept a repo from the current `.wh` file, the active profile, or a flag.
## Command Pattern
All commands follow the `wh ` pattern:
```bash
wh org create myorg
wh repo list myorg
wh thing list --shape Location
wh commit submit --ops '[...]'
```
Domains: `org`, `repo`, `shape`, `graph`, `thing`, `assertion`, `commit`, `use`, `init`, `doctor`, `sub`, `auth`, `token`, `credential`, `component`, `prime`, `notifications`, `channel`
See the [full command reference](/cli-reference/commands/) for every domain, verb, flag, and alias.
## Configuration
### Default Repo (`.wh` file)
The easiest way to set a default repo is `wh use`, which writes a `.wh` file in the current directory:
```bash
wh use myorg/world
wh thing list # targets myorg/world
```
The CLI resolves the target repo using this priority:
1. `--repo` flag (per-command override)
2. `WARMHUB_REPO` environment variable
3. `.wh` file in the current directory
### Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `WARMHUB_REPO` | Default org/repo for all commands | `myorg/myrepo` |
| `WARMHUB_ORG` | Default org | `myorg` |
| `WARMHUB_API_URL` | Backend URL | `https://api.warmhub.ai` |
| `WH_PROFILE` | Named auth profile (same as `--profile` / `-P`) | `staging` |
| `WARMHUB_CLI_HTTP_TIMEOUT_MS` | HTTP client headers/body timeout in milliseconds for long-running requests (Node only; no-op under Bun). Default `5700000` (95 minutes). Set to `0` for unlimited. | `3600000` |
| `WARMHUB_CLI_NO_UPDATE_CHECK` | Disable the background CLI update check. Set to `1`, `true`, or `TRUE` to enable the opt-out. Equivalent to passing `--no-update-check`. | `1` |
### Per-Command Override
```bash
wh thing list --repo myorg/other-repo
```
## Output Formats
### Default (Human-Readable)
Colored, formatted text output:
```bash
wh thing list
```
### JSON
Structured output for scripts and agents:
```bash
wh thing list --json
```
### Live (Reactive)
Real-time WebSocket updates — auto-refreshes as data changes:
```bash
wh thing list --live
```
## Ergonomics
**Prefix expansion.** Flag prefixes expand to the full name if unambiguous:
```bash
wh repo create myorg/repo --desc "My repo"
# expands to --description
```
**Fuzzy matching.** Typos in domains, verbs, and flags get "did you mean?" suggestions.
**Verb aliases.** A handful of verbs accept an alias — for example, `wh auth whoami` resolves to `wh auth status`. There is no general `show`/`get` aliasing; use the canonical verb shown in the [command reference](/cli-reference/commands/). A mistyped verb returns a "did you mean?" suggestion rather than silently resolving.
## Getting Help
```bash
wh help # full help overview
wh # list verbs for a domain
wh --help # verb details with flags and examples
wh help --format json # structured CLI spec as JSON
wh doctor # verify environment and connectivity
```
## Next steps
| Need | Page |
|------|------|
| Every domain, verb, flag, and alias | [Command Reference](/cli-reference/commands/) |
| Build and submit a multi-operation write | [`commit submit` deep dive](/cli-reference/write-submit-deep-dive/) |
| Install the CLI as part of first-run setup | [Quickstart](/get-started/quickstart/#install-the-cli) |
---
# Write Submit Deep-Dive
> All the ways to submit operations — --ops JSON, --file, --stream, and shorthand flags.
`wh commit submit` is the primary write command (bare `wh commit` is equivalent). There are several ways to specify operations: inline JSON, file-based, streaming JSONL for large datasets, and shorthand flags, which accept one operation by default and up to 20 paired operations per call.
> Operations stream one-at-a-time and apply with per-operation results: the
> response tells you what succeeded, what no-op'd, and what failed, and later
> failures do not roll back earlier successes. See
> [Writes Overview](/writes/overview/).
## 1. Inline JSON with --ops
Pass a JSON array of operations directly:
```bash
wh commit submit --ops '[
{"operation":"add","kind":"thing","name":"Sensor/temp-1","data":{"location":"Building A","type":"temperature"}},
{"operation":"add","kind":"assertion","name":"Reading/temp-1-v1","about":"Sensor/temp-1","data":{"value":72.5,"unit":"fahrenheit"}}
]' -m "Add sensor with reading" --committer Agent/bot-1
```
This is the most flexible form — supports any number of operations with any combination of adds, revises, and retracts.
## 2. From File with --file / -f
Load operations from a JSON file:
```bash
wh commit submit -f operations.json -m "Batch update"
```
Where `operations.json` contains a JSON array:
```json
[
{ "operation": "add", "kind": "thing", "name": "Sensor/temp-1", "data": { "location": "Building A", "type": "temperature" } },
{ "operation": "add", "kind": "thing", "name": "Sensor/humidity-1", "data": { "location": "Building A", "type": "humidity" } }
]
```
This is useful for moderate-sized pre-generated operation sets. For larger datasets (thousands of operations), use the streaming JSONL format below.
## 3. Streaming JSONL with --file or --stream
For large datasets (hundreds to millions of operations), the streaming protocol sends operations in chunks rather than packing them into one payload. This avoids payload size limits and provides interactive progress feedback. Each chunk hits `stream.append` and applies its operations one at a time on the server.
Both JSONL paths — `--file .jsonl` and `--stream` — require `--stream-id` and `--skip-existing`. The stream id is a stable, caller-chosen name that lets an interrupted run resume by re-submitting from the start; `--skip-existing` makes those replayed `add` operations idempotent (an already-applied add returns `noop` instead of failing).
### From a JSONL file
```bash
wh commit submit --file dataset.jsonl --stream-id bulk-ingest --skip-existing -m "Bulk ingest" --progress
```
Where `dataset.jsonl` is a newline-delimited JSON file (one operation per line):
```jsonl
{"operation":"add","kind":"thing","name":"Sensor/temp-1","data":{"location":"Building A","type":"temperature"}}
{"operation":"add","kind":"thing","name":"Sensor/temp-2","data":{"location":"Building B","type":"temperature"}}
{"operation":"add","kind":"assertion","name":"Reading/temp-1-v1","about":"Sensor/temp-1","data":{"value":72.5}}
```
### From stdin
Pipe operations from any producer:
```bash
cat dataset.jsonl | wh commit submit --stream --stream-id pipe-ingest --skip-existing -m "Pipe ingest"
# or from a generator:
my-etl-tool --format jsonl | wh commit submit --stream --stream-id etl-ingest --skip-existing -m "ETL ingest"
```
### Chunking
Operations are sent to the backend in chunks. The CLI chooses safe defaults
for large streams, and you can override the per-request chunk size when you
need smaller request bodies:
```bash
wh commit submit --file dataset.jsonl --stream-id smaller-chunks --skip-existing --chunk-size 100 -m "Smaller chunks"
```
All chunks share one `streamId` so `$N` / `#N` token allocations carry across chunk boundaries. The CLI returns once the last chunk has been applied — there is no separate finalize step.
If an append fails after earlier chunks were acknowledged, the CLI reports the
acknowledged operation count. The failed append may also have landed, so inspect
repository state before submitting any remaining JSONL operations.
### Interactive progress
Add `--progress` to see a live progress bar on stderr (TTY only):
```bash
wh commit submit --file dataset.jsonl --stream-id 10k-sensors --skip-existing --progress -m "10k sensors"
# Appending [####------] 4000/10000 (40%) 8 chunks 2.3s
```
For `--stream` input (where total count is unknown), progress shows running totals without a percentage.
Progress output is stderr-only and suppressed when stderr is not a TTY, so `--json` stdout remains machine-readable.
### Batch tokens across chunks
`$N` allocates an opaque identifier (replaced by a unique server-generated suffix), and `#N` references the value allocated by the matching `$N`. These tokens work across chunk boundaries — a `$1` allocated in chunk 1 can be referenced with `#1` in chunk 2+:
```jsonl
{"operation":"add","kind":"thing","name":"Player/player-$1","data":{"score":0}}
{"operation":"add","kind":"assertion","name":"Score/score-$2","about":"Player/player-#1","data":{"value":0}}
```
### Debug timing
Pass `--debug` to see a detailed timing breakdown (per-chunk append, server resolve-repo, token resolve, apply) and throughput metrics on stderr.
## 4. Shorthand Flags
For shorthand commits, use named flags instead of writing JSON. Add and retract shorthands are repeatable; mixed operation batches should use `--ops` or `--file`.
### Add a thing
```bash
wh commit submit --add temp-1 --shape Sensor --data '{"location":"Building A","type":"temperature"}' -m "Add sensor"
```
This produces: `{ operation: "add", kind: "thing", name: "Sensor/temp-1", data: {...} }`
The `--shape` flag is prefixed to the `--add` name to form the wref, WarmHub's typed reference format (`Shape/name`).
### Add an assertion
When `--about` is provided, the kind auto-infers to `assertion`:
```bash
wh commit submit --add temp-1-v1 --shape Reading --about Sensor/temp-1 --data '{"value":72.5}'
```
This produces: `{ operation: "add", kind: "assertion", name: "Reading/temp-1-v1", about: "Sensor/temp-1", data: {...} }`
### Revise a thing
```bash
wh commit submit --revise Sensor/temp-1 --data '{"location":"Building B","type":"temperature"}' -m "Relocate sensor"
```
This produces: `{ operation: "revise", kind: "thing", name: "Sensor/temp-1", data: {...} }`
### Retract a thing
```bash
wh commit submit --retract Sensor/temp-1 --reason "duplicate" -m "Retract duplicate sensor"
```
This produces: `{ operation: "retract", kind: "thing", name: "Sensor/temp-1", reason: "duplicate" }`
`--kind` is optional for thing retractions and can be used as a safety check or to retract non-thing identities such as assertions, shapes, and collections. `--reason` is repeatable and can be provided once for all retractions or once per `--retract`.
To write under a [read lease](/cli-reference/commands/#thing-read-leases) acquired with `wh thing lease`, add `--lease-id ` to the `--revise` or `--retract` short-form:
- The lease auto-releases on a successful or no-op write.
- If the lease has already expired, the write proceeds as an ordinary write; but if an active lease is still in force and your `--lease-id` doesn't match it, the write is rejected with `LEASE_UNAVAILABLE`.
- `--lease-id` requires a `--revise`/`--retract` short-form. For `--ops`/`--file`/`--stream` payloads, carry `leaseId` inline on the operation object instead.
### Add a shape
```bash
wh commit submit --add Location --kind shape --data '{"fields":{"x":"number","y":"number"}}'
```
Shape data supports an optional top-level `description`, typed field objects with descriptions, and [field constraints](/data-modeling/shapes/#field-constraints):
```bash
# Typed field with description
wh commit submit --add Location --kind shape --data '{"description":"A point in 2D space","fields":{"x":{"type":"number","description":"Horizontal position"},"y":"number"}}'
# Field constraints (string enum, number range, wref constrained to a shape, array bounds)
wh commit submit --add GameState --kind shape \
--data '{
"fields": {
"status": { "type": "string", "enum": ["active", "paused", "ended"] },
"score": { "type": "number", "minimum": 0, "integer": true },
"owner": { "type": "wref", "shape": "Player" },
"tags": { "type": "array", "items": "string", "maxItems": 5 }
}
}'
```
### Specify kind explicitly
Override the auto-inferred kind with `--kind`. For example, to explicitly mark an operation as a thing:
```bash
wh commit submit --add my-item --kind thing --shape Player --data '{"name":"Alice"}'
```
### Add multiple things in one write request
Repeat `--add` to pack up to 20 operations into a single write request. Each `--add` pairs with its own `--data` by position:
```bash
wh commit submit \
--add alice --data '{"score":1}' \
--add bob --data '{"score":2}' \
--shape Player \
-m "seed players"
```
Rules:
- `--data` must appear exactly once per `--add`, in the same order. A mismatched count errors rather than silently dropping ops.
- `--shape`, `--about`, and `--kind` accept 0, 1 (broadcast to every op), or N values (paired by position).
- The short-form path is capped at 20 ops. Beyond that, use `--ops ''` or `-f operations.json` to keep the bulk path explicit.
## Flag Reference
| Flag | Short | Description |
|------|-------|-------------|
| `--ops` | | Operations JSON array (full control) |
| `--file` | `-f` | Path to operations file (`.json` array or `.jsonl` newline-delimited) |
| `--stream` | | Read newline-delimited operations from stdin |
| `--progress` | | Show interactive progress on TTY stderr (requires `--stream` or `.jsonl --file`) |
| `--chunk-size` | | Ops per append chunk for `--stream` or `.jsonl --file` (default: `1000`, max: `10000`) |
| `--timing-out` | | Write per-append timing details to a local JSON sidecar for debugging or benchmarks |
| `--stream-id` | | Caller-chosen stream id for JSONL token continuity and resumable reruns. **Required** for `--stream` or `.jsonl --file`. |
| `--skip-existing` | | For add operations, return `noop` when the target shape, thing, assertion, or collection already exists instead of failing. **Required** for `--stream` or `.jsonl --file`. |
| `--add` | | Name for add operation (shorthand). Repeatable; pair each with `--data`. |
| `--revise` | | Name for revise operation (shorthand) |
| `--retract` | | Name for retract operation (shorthand). Repeatable. |
| `--reason` | | Optional retraction reason for `--retract` operations. Repeatable; one per `--retract` or one value broadcast to all. |
| `--expected-version` | | Apply the `--revise` only if its target is still at this version (optimistic concurrency). Requires `--revise`. |
| `--lease-id` | | [Read-lease](/cli-reference/commands/#thing-read-leases) token from `wh thing lease`, bound to a single `--revise`/`--retract` (auto-released on a successful or no-op write). Broadcast to every op of a multi-target `--retract`. Requires `--revise` or `--retract`; for `--ops`/`--file`/`--stream` writes, carry `leaseId` inline on the operation instead. |
| `--kind` | | Kind override: thing, assertion, shape, collection. Repeatable; 1 (broadcast) or N (paired). |
| `--shape` | | Shape name (prefixed to `--add` name). Repeatable; 1 (broadcast) or N (paired). |
| `--data` | | Data payload as JSON string. Repeatable; must match `--add` count exactly. |
| `--about` | | Target wref for assertions. Repeatable; 1 (broadcast) or N (paired). |
| `--type` | | Collection type shorthand: `pair`, `triple`, `set`, `list` |
| `--members` | | Comma-separated member wrefs for collection shorthand |
| `--message` | `-m` | Optional message recorded with each thing-version produced by this call. When omitted for `--ops`, `--file` (JSON array), and shorthand flag paths, a message is synthesized automatically from the operations (see [Default message synthesis](#default-message-synthesis)). For `--stream` and `.jsonl --file` paths, the message is not synthesized. |
| `--committer` | | Optional [wref](/data-modeling/wrefs/) identifying the actor on whose behalf the writes are made (e.g. `Agent/bot-1`). When omitted, attribution falls back to the authenticated user via `createdByEmail`. |
`--skip-existing` and `--expected-version` are the CLI form of WarmHub's conditional writes. See [Conditional Operations](/writes/operations/#conditional-operations) for the full model across the CLI, SDK, and result statuses.
## Shorthand Resolution Rules
The shorthand flags are resolved as follows:
1. `--add X --shape Y` → name becomes `Y/X`, kind defaults to `thing`
2. `--add X --about Z` → kind auto-infers to `assertion`
3. `--add X --shape Y --about Z` → name becomes `Y/X`, kind is `assertion`
4. `--revise X` → kind defaults to `thing`
5. `--retract X` → kind defaults to `thing` and does not require `--data`
6. `--kind` overrides the auto-inferred kind in all cases
If none of `--add`, `--revise`, `--retract`, `--type`, `--ops`, or `--file` are provided, the command prints a usage error.
## Default Message Synthesis
When `-m` / `--message` is omitted and the input path is `--ops`, a JSON-array `--file`, or shorthand flags, the CLI synthesizes a commit message from the resolved operations:
| Operations | Synthesized message |
|---|---|
| Single `add` (thing, assertion, shape) | `add ` |
| Single `add` (collection) | `add ` |
| Single `revise` | `revise ` |
| Single `retract` (non-shape) | `retract ` |
| Single `retract --kind shape` | `retract shape ` |
| Two or more operations | `batch: N operations` |
An explicit `-m` value always takes precedence over synthesis.
This synthesis does **not** apply to `--stream` or `.jsonl --file` paths — for those, omitting `-m` leaves the commit message unset.
## Output
Pretty output shows the message, operation count, and per-operation details:
```
Add sensor with reading (2 ops)
+ Sensor/temp-1@v1
+ Reading/temp-1-v1@v1
```
Markers: `+` adds, `~` revises, `-` retracts or non-mutating results such as `noop` from `--skip-existing`, and `!` per-operation failures. The header includes `, failed` when any operation in the batch failed:
```
Seed players (2 ops, 1 failed)
+ Player/alice@v1
! Player/bob caller is not a member
```
When an operation's `data` carries top-level fields not declared in the target shape, the CLI prints a non-blocking warning line under the op (see [Shapes — Undeclared Fields](/data-modeling/shapes/#undeclared-fields)):
```
seed finding (1 ops)
+ DocFinding/issue-001@v1
⚠ 3 fields not declared in shape DocFinding: status, filePath, category
```
The warning is informational; it does not turn the operation into a failure. If the field count exceeds the server-side cap, the line carries a `(+N more)` tail with the count of additional undeclared fields.
With `--json`, returns the structured per-operation result list. Each operation includes an `opIndex` (its zero-based position in the submitted operations array) and a `status` field (`'applied'`, `'noop'`, or `'error'`); on `status === 'error'`, an `error` object with `code` and `message` accompanies the row. Mixed-result responses include `partial: true` and `statusCounts`. When validation found undeclared fields, a `warnings` object lists them:
```json
{
"committer": "Agent/bot-1",
"message": "Seed players",
"operationCount": 2,
"partial": true,
"statusCounts": { "applied": 1, "noop": 0, "error": 1 },
"operations": [
{
"opIndex": 0,
"name": "Player/alice",
"operation": "add",
"status": "applied",
"version": 1,
"dataHash": "abc123",
"warnings": { "undeclaredFields": ["b", "d", "u"] }
},
{
"opIndex": 1,
"name": "Player/bob",
"operation": "add",
"status": "error",
"error": {
"code": "FORBIDDEN",
"message": "caller is not a member"
}
}
]
}
```
Each operation result includes the resolved name, operation type, `opIndex`, status, and (on success) version number and server-computed data hash. `opIndex` is the zero-based position of the source operation in the submitted array and is the correlation key callers use to map results back for retry or recovery. `committer` appears only when the caller passed `--committer`. `warnings` is omitted entirely when no undeclared fields were found, so callers can branch on its presence. Failed entries never carry `warnings` — when an op fails, the error is the relevant signal.
**Exit codes.** Mixed-result commits exit `0` — at least some operations landed, and the per-op `status`/`error` fields communicate the partial result. The CLI exits non-zero only when *every* operation failed; in that case stdout stays empty (no JSON success payload) and stderr carries the error message. The exit code is picked deterministically from the worst per-op failure: any auth-class failure (`FORBIDDEN`, `UNAUTHENTICATED`) exits `5` (auth), otherwise any user-input failure (`VALIDATION_ERROR`, `SHAPE_MISMATCH`, `CONFLICT`, etc.) exits `2` (user input), otherwise `4` (backend). Order of operations in the request does not affect the exit code.
There is no `commitId` field in the result. Per-thing version trails are the audit source — use `wh thing history `.
---
# Authoring Components
> How to build a WarmHub component package from scratch.
## Directory structure
A component is a Git repository (or subdirectory) with this layout:
```
my-component/
warmhub/
component.json # Identity
manifest.json # Resource declarations
```
The `warmhub/` directory is required. If your component reacts to subscriptions, the webhook handlers themselves live wherever you deploy them; the manifest only stores their URLs.
## Step 1: Create component.json
```json
{
"id": "com.example.MyComponent",
"name": "my-component",
"version": "1.0.0",
"description": "Watches for Foo things and processes them"
}
```
The `id` uses reverse-DNS format: lowercase domain segments followed by PascalCase name segments. This is the ownership key — it must be globally unique.
## Step 2: Create manifest.json
Start with the skeleton and add sections as needed:
```json
{
"$schema": "https://docs.warmhub.ai/schema/component-manifest.v1.json",
"component": {
"id": "com.example.MyComponent",
"name": "my-component",
"version": "1.0.0"
},
"shapes": [],
"credentials": [],
"subscriptions": [],
"seeds": [],
"health": {},
"teardown": {}
}
```
The `component` section must match `component.json`. The `$schema` field enables IDE autocomplete.
### Add shapes
Declare shapes for data your component creates or consumes:
```json
"shapes": [
{ "name": "FooInput", "fields": { "url": "string", "priority": "number" } },
{ "name": "FooResult", "fields": { "summary": "string", "score": "number" } }
]
```
If your component only reacts to shapes that already exist in the target repo (created by another component or manually), you don't need to declare them here.
### Add subscriptions
Wire webhook subscriptions. Components declare event-triggered webhook subscriptions:
```json
"subscriptions": [
{
"name": "mc/on-foo-add",
"trigger": { "kind": "event", "shape": "FooInput" },
"kind": "webhook",
"webhookUrl": "https://handler.example.com/foo",
"credentials": ["my-creds"]
}
]
```
Convention: prefix subscription names with a short component abbreviation (e.g., `mc/` for my-component).
### Add seeds
Create initial data at install time:
```json
"seeds": [
{
"kind": "thing",
"shape": "ComponentConfig",
"name": "my-component",
"data": { "version": "1.0.0", "enabled": true }
}
]
```
`ComponentConfig` is a built-in shape — you don't need to declare it in `shapes`.
### Configure health and teardown
```json
"health": {
"requires": {
"shapes": ["FooInput", "FooResult"],
"subscriptions": ["mc/on-foo-add"]
}
},
"teardown": {
"subscriptions": { "onDisable": "pause" }
}
```
`wh component doctor` validates the shapes, subscriptions, credentials, and seeds declared in the manifest.
## Step 3: Deploy your webhook handler
Component subscriptions post to webhook URLs that you operate; they do not run local action scripts or action-container runtimes.
Your handler should accept the [webhook payload](/subscriptions/creating/#webhook-payload) and then use the CLI, SDK, or HTTP API to write results back to WarmHub.
A minimal handler contract looks like this:
- Read `event`, `runId`, `repo`, and `matchedOperations` from the POST body.
- Start any long-running work asynchronously if needed.
- Use `callback_url` to report `processing`, `success`, `failure`, or `retry_requested` for asynchronous work.
- Authenticate write-back calls with your own token.
## Step 4: Validate
```bash
wh component validate .
```
Fix any errors before publishing. Common issues:
- Missing `webhookUrl` on a declared subscription
- Invalid trigger definitions (for example, an event trigger without `shape`)
- Component ID format (must be reverse-DNS)
- Mismatched id/name between `component.json` and `manifest.json`
## Step 5: Register, install, and test
Components install by identity, so register the manifest first, then install by `/`:
```bash
# Register the component identity from its manifest
wh component register my-component --org myorg --manifest ./warmhub/manifest.json
# Install it into a repo
wh component install myorg/my-component --repo myorg/myrepo
# Set any required credentials
wh credential set my-creds openai_key --value sk-...
# Check health
wh component doctor my-component --repo myorg/myrepo
# Trigger by creating data
wh commit submit --add test-input --shape FooInput \
--data '{"url": "https://example.com", "priority": 1}' \
--repo myorg/myrepo
# Check subscription logs
wh sub log mc/on-foo-add --repo myorg/myrepo
```
:::note
Components install by registered identity (`/`) or bundled system id. To roll out manifest edits, bump `component.version` (re-publishing requires a strictly-greater semver), re-publish with `wh component registry update --manifest `, and then run `wh component update `.
:::
## Tips
- **Avoid self-triggering**: If your webhook writes to the same shape the subscription monitors, it will create an infinite loop. Write to a different output shape or tighten the filter.
- **Keep handlers idempotent**: WarmHub retries failed deliveries. Use `X-WarmHub-Idempotency-Key` or `runId` to deduplicate side effects.
- **Prefix subscription names**: Use a consistent prefix (e.g., `rk/` for research-knowledge) to avoid collisions with other components.
- **Test with `wh sub attempts`**: After triggering, check delivery status with `wh sub attempts ` to see exit codes and error categories.
---
# Component Lifecycle
> How components are installed, updated, diagnosed, and torn down.
## Install
```bash
wh component install --repo org/repo
```
Only registered components (`/`) can be installed. To install your own component, [register](/cli-reference/commands/#component--component-management) it first (`wh component register --org --manifest `). The installer runs through these stages:
WarmHub's own system components install the same way: the identity system is the registered component `warmhub/identity`. WarmHub-managed internal components are not installable.
1. **Source resolution** — Resolves the registered component reference to its latest published manifest snapshot from the component registry.
2. **Parse and validate** — Validates the resolved manifest's JSON structure, required fields, and cross-references (for example, credentials and shapes referenced by subscriptions and seeds).
3. **Ensure shared infrastructure** — Creates the `ComponentInstall` and `ComponentConfig` shapes if they don't already exist in the repo. These are system-managed shapes shared across all components.
4. **Record install** — Creates a `ComponentInstall/` thing with state `installing` and the serialized manifest. This is the tracking record for all future operations.
5. **Apply manifest-provisioned resources** — In order:
- **Shapes**: Created if they don't exist. Tagged with the component's ID.
- **Credential sets**: Created if they don't exist. Keys are declared but not populated — the user must set values after install.
- **Subscriptions**: Manifest subscriptions are compiled into normal WarmHub webhook subscriptions, then bound to credential sets.
- **Seeds**: Initial things created via a commit. Seeds targeting `ComponentConfig` and other shapes are batched.
Resources marked `provisioning: "setup"` are skipped by the local installer and are expected to be created by the registered component's setup callback.
6. **Run registered setup** — If a registered component advertises a setup endpoint, the CLI calls it after the local install succeeds. Setup-provisioned shapes, credential sets, and subscriptions are created by that external service.
7. **Compute state** — Checks whether all resources were created successfully. Sets the final state to `ready` when everything succeeded, or `degraded` when a resource failed. Missing credential values do not change the state — they surface as `doctor` findings instead.
8. **Finalize** — Updates the `ComponentInstall` record with the final state and a hash of the manifest for change detection.
### Subscription compilation
The installer derives the effective subscription kind from `trigger.kind`:
- `event` triggers compile to normal webhook subscriptions with `shape`, `filter`, and `webhookUrl`
Component manifests declare webhook subscriptions only; handlers live outside the manifest and are referenced by webhook URL.
### Reconciliation (reinstall)
If a component is already installed (a `ComponentInstall` record exists), the installer enters **reconciliation mode**:
- **Missing resources are added** — New shapes, subscriptions, and seeds from the updated manifest are created.
- **Existing resources are preserved** — The reconciler does not delete resources that were in the old manifest but absent from the new one.
- **Seeds are updated** — If a seed thing already exists, its data is revised to match the new manifest.
- **Failed installs are retried** — If the previous state was `installing` or `error`, the reconciler treats the old manifest as empty and retries all resources.
- **Degraded installs are re-probed** — If the previous state was `degraded`, the reconciler keeps the old manifest and probes the live resources, deciding per resource whether to add, revise, or update rather than retrying everything.
- **Paused subscriptions are not automatically resumed** — Reinstall adds missing subscriptions, but a subscription that still exists in paused state stays paused until you resume or recreate it yourself.
- **Registered setup is replayed on update** — `wh component update` for a registered install re-resolves the latest published manifest, reruns the local installer, and calls the setup endpoint again, bringing the component up to the latest published version.
## Doctor
```bash
wh component doctor --repo org/repo
```
Doctor checks an installed component against the manifest snapshot recorded for that install rather than your local files, so results reflect what is actually deployed. It checks:
| Check | Pass | Fail |
|-------|------|------|
| Shape exists and is active | `ok` | `missing` or `inactive` |
| Subscription exists and is active | `ok` | `missing` or `inactive` |
| Credential set exists with all required keys populated | `ok` | `missing` keys listed |
| Seed things exist | `ok` | `missing` |
Doctor computes and persists the component state:
- All subscriptions paused, nothing else wrong → `paused`
- Missing resources, inactive non-subscription resources, or shape drift → `degraded`
- A mix of active and paused subscriptions does not by itself move the component out of `ready`
- Everything healthy → `ready`
Missing credential values are reported as `doctor` findings, but they do not change the component state — there is no `credentials-required` state. If a CLI method needs credentials that aren't set, the call fails at invocation time with a precise error; populating those keys is the component setup's job.
## Teardown
```bash
wh component teardown --repo org/repo
```
Teardown pauses all subscriptions declared in the component's manifest. Current CLI behavior only uses `teardown.subscriptions.onDisable`:
- `onDisable: "pause"` — Subscriptions are paused (can be resumed later).
`onUninstall` is accepted by the manifest schema but is not executed by the current CLI teardown flow.
After teardown, the component state is set to `paused`. Shapes, seeds, and credential sets are preserved.
To restore a torn-down component, resume or recreate the paused subscriptions. Reinstall alone does not automatically unpause existing subscriptions:
```bash
wh sub resume --repo org/repo
```
## State transitions
```
install (new) → installing → ready | degraded | error
install (exists) → reconcile → ready | degraded
doctor → ready | degraded | paused
teardown → paused | degraded
```
## Validate (offline)
```bash
wh component validate ./my-component
```
Runs all checks without connecting to a repo:
- JSON syntax and schema validation
- Cross-reference checks (credential refs, shape refs)
- Duplicate name detection
- Subscription trigger and webhook validation
- Component ID format
- Consistency between `component.json` and `manifest.json`
---
# Manifest Reference
> Complete reference for the warmhub/manifest.json component manifest format.
Every component package contains two files in a `warmhub/` directory:
- **`component.json`** — Component identity (id, name, version)
- **`manifest.json`** — Declarative resource definitions
Both are validated at install time. You can also validate offline with `wh component validate `.
## JSON Schema
Add the `$schema` field to get IDE autocomplete and inline validation:
```json
{
"$schema": "https://docs.warmhub.ai/schema/component-manifest.v1.json",
"component": { ... },
...
}
```
## component.json
```json
{
"id": "com.warmhub.MyComponent",
"name": "my-component",
"version": "1.0.0",
"description": "Optional description",
"author": "Your Name",
"tags": ["research", "ai"]
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Reverse-DNS identifier (e.g., `com.warmhub.MyComponent`). Ownership key. |
| `name` | string | Yes | Display name, typically kebab-case. |
| `version` | string | Yes | Semver version string. |
| `description` | string | No | Human-readable description. |
| `author` | string | No | Author or organization. |
| `tags` | string[] | No | Discovery tags. |
The `id` must match the pattern: one or more lowercase domain segments, followed by one or more PascalCase segments. Examples: `com.warmhub.ResearchKnowledge`, `io.example.MyTool`.
## manifest.json
### Top-level structure
Every manifest needs a `component` object (`id`, `name`, `version`) plus the `shapes`, `credentials`, `subscriptions`, `seeds`, `health`, and `teardown` sections — those six may be empty arrays or objects. `$schema`, `runtimeAccess`, and `cli` are optional.
Earlier manifests also had an `actions` section. It has been removed from the manifest format: an `actions` key in an older manifest is ignored — install and reconcile never read it.
```json
{
"$schema": "https://docs.warmhub.ai/schema/component-manifest.v1.json",
"component": {
"id": "com.warmhub.MyComponent",
"name": "my-component",
"version": "1.0.0"
},
"shapes": [],
"credentials": [],
"subscriptions": [],
"seeds": [],
"health": {},
"teardown": {}
}
```
The `component` section must match the corresponding fields in `component.json`.
### runtimeAccess
Declare the WarmHub runtime scopes the component's minted runtime token needs at execution time. This is used only for registered-component setup flows that mint tokens; local installs can still include the field for portability.
```json
"runtimeAccess": {
"reads": ["Paper", "ComponentConfig"],
"writes": ["Consensus"]
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `reads` | string[] | Yes | Shape names the runtime token may read. |
| `writes` | string[] | Yes | Shape names the runtime token may write. |
Rules:
- Every shape must be declared in `shapes[]` or be a known built-in (`ComponentInstall`, `ComponentConfig`).
- A shape may appear in both `reads` and `writes` when the runtime needs read-before-write behavior, such as revising existing component-owned assertions.
- Omitting `runtimeAccess` is equivalent to no minted runtime token.
### Provisioning modes
`shapes`, `credentials`, and `subscriptions` support a per-resource `provisioning` field:
- `manifest` — default. The normal manifest installer creates the resource in the target repo.
- `setup` — the registered component's setup endpoint creates the resource. When minted tokens are enabled, WarmHub includes a setup token the endpoint can use for those writes.
This allows a manifest to mix installer-created and setup-created resources in one package.
### shapes
Shapes the component needs in the target repo. Created at install time if they don't exist.
```json
"shapes": [
{
"name": "Paper",
"fields": { "title": "string", "url": "string", "score": "number" }
}
]
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Shape name. |
| `fields` | object | Yes | Field definitions mapping names to WarmHub type descriptors. |
| `description` | string | No | Human-readable description of the shape. |
| `provisioning` | `"manifest"` \| `"setup"` | No | Which side creates the shape. Defaults to `manifest`. |
Field types: `string`, `number`, `boolean`, `wref` (a WarmHub reference string such as `Shape/name` or `Shape/name@v3`), arrays, nested objects. See [Shapes](/data-modeling/shapes/) for details.
### credentials
Credential sets the component needs for external API access. With the default `provisioning: "manifest"`, the installer creates the set and the user populates its required keys afterward. With `provisioning: "setup"`, the registered component's setup endpoint creates and fills the set.
```json
"credentials": [
{
"name": "github-creds--",
"description": "GitHub API access for fetching repos",
"provisioning": "manifest",
"requiredKeys": [
{ "key": "github_token", "description": "Personal access token with repo scope" }
]
}
]
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Credential set name. |
| `description` | string | No | What these credentials are for. |
| `requiredKeys` | array | Yes | Keys that must be set. |
| `provisioning` | `"manifest"` \| `"setup"` | No | Which side creates the credential set. Defaults to `manifest`. |
| `requiredKeys[].key` | string | Yes | Key name. |
| `requiredKeys[].description` | string | No | What the key is used for. |
Credential names may include the template tokens:
- ``
- ``
These are resolved at install/setup time. For example, `veritas-webhook--` becomes `veritas-webhook-acme-world`.
After install, populate keys with:
```bash
wh credential set github-creds-acme-world github_token --value ghp_... --repo acme/world
```
### subscriptions
Subscriptions that trigger webhooks on write events.
```json
"subscriptions": [
{
"name": "rk/on-paper-add",
"trigger": { "kind": "event", "shape": "Paper" },
"kind": "webhook",
"webhookUrl": "https://handler.example.com/on-paper",
"credentials": ["github-creds--"]
}
]
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Subscription name. Convention: `/`. |
| `trigger` | object | Yes | What fires the subscription. |
| `kind` | `"webhook"` | No | Optional advisory field retained for readability. The CLI derives the effective subscription kind from `trigger.kind` and ignores this field at install time. |
| `webhookUrl` | string | Yes | Destination URL for deliveries. |
| `credentials` | string[] | No | Credential sets to bind. Currently limited to one. |
| `fallbackWebhookUrl` | string | No | Optional fallback delivery target. |
| `provisioning` | `"manifest"` \| `"setup"` | No | Which side creates the subscription. Defaults to `manifest`. |
**Event trigger**:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `kind` | `"event"` | Yes | |
| `shape` | string | Yes | Shape to watch for changes. |
| `filter` | object | No | Additional filter criteria. |
### seeds
Initial data created at install time.
```json
"seeds": [
{
"kind": "thing",
"shape": "ComponentConfig",
"name": "my-component",
"data": { "version": "1.0.0", "enabled": true }
}
]
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `kind` | `"thing"` | Yes | Currently only `"thing"` is supported. |
| `shape` | string | Yes | Shape name. Must be declared in `shapes` or be a built-in (`ComponentConfig`). |
| `name` | string | Yes | Thing name within the shape. |
| `data` | object | Yes | Field values. Must conform to the shape's field definitions. |
`ComponentConfig` is a built-in shared shape — you can seed things into it without declaring it in `shapes`.
### health
Configuration reserved for richer `wh component doctor` diagnostics in a future CLI version.
```json
"health": {
"requires": {
"shapes": ["Paper", "PaperSummary"],
"things": ["ComponentConfig/my-component"],
"subscriptions": ["rk/on-paper-add"]
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `requires.shapes` | string[] | Reserved for future doctor checks. |
| `requires.things` | string[] | Reserved for future doctor checks. |
| `requires.subscriptions` | string[] | Reserved for future doctor checks. |
All fields are optional. The current CLI accepts this section in the manifest schema, but `wh component doctor` does not read `health.requires.*` yet. Today, doctor checks the shapes, subscriptions, credentials, and seeds declared elsewhere in the manifest.
### teardown
Behavior when the component is disabled. The current CLI teardown flow reads `subscriptions.onDisable`; `onUninstall` is schema-valid but not executed yet.
```json
"teardown": {
"subscriptions": {
"onDisable": "pause",
"onUninstall": "delete"
}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `subscriptions.onDisable` | `"pause"` | Action when component is disabled. |
| `subscriptions.onUninstall` | `"pause"` \| `"delete"` | Reserved for a future uninstall flow. |
### cli
Optional. Declares a CLI surface the component exposes to operators. When present, the listed methods become invokable through [`wh component exec `](/cli-reference/commands/#component--component-management) (or the shorthand `wh `) once the component is installed. Omit the section if the component has no operator-facing commands.
```json
"cli": {
"description": "Look up and refresh reputation scores",
"methods": [
{
"name": "reputation-get",
"description": "Fetch the current reputation for a subject",
"credentialSet": "reputation-api",
"method": "GET",
"requiresPermission": "repo:read",
"args": [
{ "name": "subject", "type": "string", "required": true, "description": "Subject id to look up" }
]
}
]
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `description` | string | No | One-line summary of the CLI surface, shown in `wh --help`. |
| `methods` | array | Yes | The invokable methods. May be empty. |
| `methods[].name` | string | Yes | Method name (lowercase kebab, unique within `methods`). |
| `methods[].description` | string | No | What the method does. |
| `methods[].credentialSet` | string | Yes | Name of an entry in this manifest's `credentials[]` that holds the method's auth. The referenced set must declare a supported CLI auth scheme. |
| `methods[].args` | array | Yes | Declared arguments (see below). May be empty. |
| `methods[].method` | `"GET"` \| `"POST"` \| `"PUT"` \| `"PATCH"` \| `"DELETE"` | No | Defaults to `POST`. Use `GET` for read-only lookups. |
| `methods[].path` | string | No | URL path the method routes to, decoupling the route from the method `name`. One or more lowercase-kebab segments joined by `/` (e.g. `reputations/get`). Defaults to the method `name`. Two methods may share a `path` only if their `method` (verb) differs. |
| `methods[].requiresPermission` | `"repo:read"` \| `"repo:write"` \| `"repo:configure"` \| `"repo:admin"` | No | Repo scope the operator must hold on the install repo. Defaults to `repo:read`. |
| `methods[].args[].name` | string | Yes | Arg name (lowercase kebab, unique within the method). |
| `methods[].args[].type` | `"string"` \| `"integer"` \| `"number"` \| `"boolean"` | Yes | Arg type. |
| `methods[].args[].required` | boolean | No | Whether the arg must be supplied. |
| `methods[].args[].default` | string \| number \| boolean | No | Default value when the arg is omitted. |
| `methods[].args[].min` / `max` | number | No | Numeric bounds for `integer`/`number` args. |
| `methods[].args[].pattern` | string | No | Regex the value must match, for `string` args. |
| `methods[].args[].description` | string | No | What the arg is for. |
When `methods` is non-empty, `runtimeAccess.writes` must include `ComponentConfig`. Without it, the component's CLI methods won't be callable after install.
## Complete example
```json
{
"$schema": "https://docs.warmhub.ai/schema/component-manifest.v1.json",
"component": {
"id": "com.warmhub.E2eEcho",
"name": "e2e-echo",
"version": "1.0.0"
},
"shapes": [
{ "name": "EchoInput", "fields": { "message": "string", "priority": "number" } },
{ "name": "EchoOutput", "fields": { "echo": "string", "processedAt": "string" } }
],
"credentials": [
{
"name": "echo-api-creds",
"description": "API credentials for echo service",
"requiredKeys": [{ "key": "api_key", "description": "Echo API key" }]
}
],
"subscriptions": [
{
"name": "echo/process-input",
"trigger": { "kind": "event", "shape": "EchoInput" },
"kind": "webhook",
"webhookUrl": "https://echo.example.com/process",
"credentials": ["echo-api-creds"]
}
],
"seeds": [
{
"kind": "thing",
"shape": "EchoInput",
"name": "welcome",
"data": { "message": "Hello from e2e-echo component", "priority": 1 }
}
],
"health": {},
"teardown": { "subscriptions": { "onDisable": "pause" } }
}
```
## Validation
Run offline validation before installing:
```bash
wh component validate ./my-component
```
This checks:
- JSON syntax and required fields
- Cross-references (subscriptions reference valid credentials, seeds reference valid shapes)
- Duplicate names within sections
- Subscription trigger and webhook validation
- Component ID format (reverse-DNS)
- Consistency between `component.json` and `manifest.json`
---
# Components
> Reusable, installable packages that extend WarmHub repos with shapes, subscriptions, credentials, and seed data.
Components are self-contained packages that add functionality to a WarmHub repo. A component declares the shapes, subscriptions, credentials, and seed data it needs, and installation applies the manifest-provisioned resources in one operation. Registered components can also delegate selected resources to a setup callback.
:::note[Other uses of "component"]
The word *component* names a few related things. This page covers the **installable package** — the unit of distribution, defined by `component.json` and `manifest.json`. The rest build on it:
- [`componentId` attribution](/sdk/component-identity/) — tags SDK writes and subscriptions to an installed component.
- [Components tab](/web-ui/repo-tabs/components/) — the web UI view listing a repo's installed components.
- [`client.component.*`](/sdk-reference/classes/warmhubclient/#component) — the SDK methods that install, list, and manage these packages.
:::
## What a component does
When you install a component, WarmHub:
1. Reads the component's **manifest** (`warmhub/manifest.json`) to learn what resources it needs
2. Creates manifest-provisioned **shapes** for the data types the component works with
3. Sets up manifest-provisioned **subscriptions** that deliver to your webhook endpoints when data changes
4. Seeds **initial data** the component needs to operate
5. Creates manifest-provisioned **credential sets** for any external API keys the component requires
6. Calls the setup endpoint for registered components that declare setup-provisioned resources
All resources are tracked under a `ComponentInstall` record so the CLI can manage, diagnose, and tear down the component later.
## Key concepts
**Manifest-driven**: Components use a declarative JSON manifest rather than imperative scripts. The manifest describes *what* resources the component needs, and the CLI handles creation, reconciliation, and teardown.
**Ownership**: Resources created by a component are tagged with the component's ID. This prevents accidental modification and enables clean teardown. Users can still create things under component-owned shapes — ownership protects schema, not data.
**State tracking**: Each installed component has a state that reflects its health:
| State | Meaning |
|-------|---------|
| `ready` | All resources exist, all credentials populated |
| `credentials-required` | Resources created, but one or more credential keys need values |
| `degraded` | One or more declared resources are missing or inactive |
| `paused` | All subscriptions paused (via teardown) |
| `installing` | Install in progress |
| `error` | Install failed |
## Registered components
Registered components are org-owned component identities installed with `wh component install `. Registering publishes the component's manifest to WarmHub, so installs resolve the stored manifest snapshot directly through the registry. They are useful for sharing a component across repos in an org and for installs that need a setup callback to create external service state.
Authors enable this model by registering the component identity with `wh component register --org --manifest `, and optionally adding a `setupUrl`. `--source-url` is optional documentation metadata. Manifest resources marked `provisioning: "setup"` are skipped by the local installer and must be created by that setup service.
See [Manifest Reference](/components/manifest-reference/) for `provisioning` and `runtimeAccess`, [CLI Commands](/cli-reference/commands/#component--component-management) for registration commands, and the [`client.component` API reference](/sdk-reference/classes/warmhubclient/#component) for the registry methods.
## Bundled system components
WarmHub also ships reserved system components that install by id from the
backend. The current installable bundled component is:
```bash
wh component install com.warmhub.identity --repo myorg/myrepo
```
This path does not require `wh component register`, does not create a registry
entry, and does not use a local or GitHub component package. The backend
resolves the bundled manifest and installs the component into the target repo.
Lowercase system ids are reserved for WarmHub; third-party component id
validation is unchanged.
## CLI commands
```bash
# Install WarmHub's bundled Identity system component
wh component install com.warmhub.identity --repo myorg/myrepo
# Validate your package offline, then register it from its manifest and install by identity
wh component validate ./my-component
wh component register component-name --org org --manifest ./warmhub/manifest.json
wh component install org/component-name --repo myorg/myrepo
# List installed components
wh component list --repo myorg/myrepo
# View component details
wh component view my-component --repo myorg/myrepo
# Run health checks
wh component doctor my-component --repo myorg/myrepo
# Pause all component subscriptions
wh component teardown my-component --repo myorg/myrepo
```
## Example
A component that watches for new `Paper` things and summarizes them:
```
my-summarizer/
warmhub/
component.json # Identity: id, name, version
manifest.json # Resources: shapes, subscriptions, credentials, seeds
```
Registering and installing it:
```bash
# Register the identity from its manifest, then install by /
wh component register my-summarizer --org myorg --manifest ./warmhub/manifest.json
wh component install myorg/my-summarizer --repo myorg/research
# Installed my-summarizer v1.0.0
# Set the required API key
wh credential set summarizer-creds api_key --value sk-...
# Verify health
wh component doctor my-summarizer --repo myorg/research
```
## Next steps
| Need | Page |
|------|------|
| The full manifest format (`provisioning`, `runtimeAccess`) | [Manifest Reference](/components/manifest-reference/) |
| Install, update, doctor, or teardown an installed component | [Component Lifecycle](/components/lifecycle/) |
| Build your own component | [Authoring Components](/components/authoring/) |
| The offline validation workflow before publishing | [Testing Components](/components/testing/) |
| Registration and install commands | [CLI: component management](/cli-reference/commands/#component--component-management) |
---
# Testing Components
> Validate, diagnose, and end-to-end test your components.
Components have three tiers of testing: local validation, doctor diagnostics, and end-to-end verification.
## Local Validation
Before installing, validate the manifest offline:
```bash
wh component validate ./my-component
```
This checks:
1. **Schema validation** — `component.json` and `manifest.json` structure
2. **Cross-reference validation** — subscriptions reference existing credential sets, seeds reference declared or built-in shapes, and CLI methods reference existing credential sets
3. **ID format validation** — reverse-DNS format check
4. **Consistency check** — `component.json` id/name must match `manifest.json` component section
Validation does not read or check any action references or action file paths — the manifest format has no `actions` section.
## Install for Testing
Components install by identity, so register the manifest, then install by `/` against a test repo:
```bash
wh component register my-component --org org --manifest ./my-component/warmhub/manifest.json
wh component install org/my-component --repo org/test-repo
```
:::note
Components install by registered identity or bundled system id. After editing the manifest, bump `component.version` (re-publishing requires a strictly-greater semver), re-publish with `wh component registry update --manifest `, and re-run `wh component update ` (or reinstall) to pick up the change.
:::
## Doctor Diagnostics
After install, run doctor to verify all declared resources exist:
```bash
wh component doctor my-component --repo org/test-repo
```
Doctor checks:
- **Shapes** — exist and are active
- **Subscriptions** — exist and are active
- **Credential sets** — exist with all required keys populated
Doctor operates on the installed manifest snapshot. It does not compare against the source or auto-remediate. If resources are missing, reinstall or create them manually.
## End-to-End Testing
The full verification path:
### 1. Install the Component
```bash
# Register the manifest, then install by identity
wh component register my-component --org org --manifest ./my-component/warmhub/manifest.json
wh component install org/my-component --repo org/test-repo
```
### 2. Bind Credentials
```bash
wh credential set my-creds api_key --value "sk-..." --repo org/test-repo
```
### 3. Verify Doctor
```bash
wh component doctor my-component --repo org/test-repo
# Should report "ready"
```
### 4. Trigger the Action
For **event-driven** subscriptions, create a thing on the trigger shape:
```bash
wh commit submit --repo org/test-repo \
--ops '[{"operation":"add","kind":"thing","name":"Echo/test-1","data":{"message":"hello","tags":["test"]}}]'
```
### 5. Verify Output
Check that the expected output things were created:
```bash
wh thing query --shape EchoResult --repo org/test-repo
```
Check subscription delivery logs:
```bash
wh sub log se/echo-handler --repo org/test-repo
```
## Update Test
After install passes, verify the update path picks up manifest changes:
```bash
# Bump component.version in the manifest (re-publish requires a higher semver),
# then re-publish to the registry
wh component registry update org/my-component --manifest ./my-component/warmhub/manifest.json
# Pull the latest published manifest into the install
wh component update my-component --repo org/test-repo
# Run doctor again
wh component doctor my-component --repo org/test-repo
```
---
# Assertions
> Claims about things — with immutable targets and version pinning.
An **assertion** is a thing that makes a claim *about* another thing. It has its own shape, name, and version history — but it also carries an `about` reference linking it to its subject.
## When to Use Assertions
Assertions are the right choice when:
- **Attribution matters** — you need to know *who* said something about an entity, not just the current state
- **Multiple perspectives coexist** — different agents or sources have different views on the same thing
- **Confidence varies** — assertions carry uncertainty, evidence, or scores that evolve over time
- **History of assertions matters** — you want to trace how understanding of an entity changed
In practice, assertions are the primary way to model agent observations, opinion-bearing assertions, relationships between things (via [collections](/data-modeling/collections/)), evaluations and judgments, and any attributed metadata about an existing entity.
Assertions may be unnecessary when:
- You're storing **plain facts** with a single source of truth — a regular thing with revisions may be simpler
- The data **doesn't need attribution** — if no one will ever ask "who said this?", a thing is enough
- You're modeling **static reference data** that rarely changes — shapes and things handle this well on their own
:::tip[Things vs assertions]
Not everything needs to be an assertion. A lookup table of country codes is fine as plain things. An agent's assessment of whether a competitor is a threat — that's an assertion. See [Things — When to Use Things](/data-modeling/things/#when-to-use-things) for the other side of this decision.
:::
## Assertions with subjective-logic opinions
When you want to capture *how confident* an agent is about an assertion — not just what it asserted — attach a **Subjective Logic opinion** to the assertion. The opinion is a tuple `(b, d, u, α)` — belief, disbelief, uncertainty, and a base rate — and requires the assertion's underlying proposition to be binary (true or false).
[Veritas](/veritas/overview/) is the WarmHub component that consumes and revises these opinions across sources. Its **`Certainty`** shape is the canonical form for opinions Veritas writes about another assertion.
Keep opinion metadata in a separate assertion from the data being asserted — see [Opinions as Separate Assertions](/data-modeling/patterns/#opinions-as-separate-assertions) for the full pattern and the binomial-opinion constraint.
## Creating Assertions
The same `add` assertion operation works on every surface — the CLI exposes `--shape`, `--about`, and `--data` as flags, while the SDK and MCP nest the same values inside a commit operation. See [Write operations](/writes/operations/) for the full contract.
```bash
# CLI
wh assertion create --shape Observation --name cave-safe --about Location/cave --data '{"safe": true, "confidence": 0.8}'
```
```ts
// SDK
await client.commit.apply("myorg", "world", "Assert cave safety", [
{
operation: "add",
kind: "assertion",
name: "Observation/cave-safe",
about: "Location/cave",
data: { safe: true, confidence: 0.8 },
},
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{
"operation": "add",
"kind": "assertion",
"name": "Observation/cave-safe",
"about": "Location/cave",
"data": { "safe": true, "confidence": 0.8 }
}
]
}
}
```
The `confidence: 0.8` field in this example is generic payload data, not a Subjective Logic opinion — see [Assertions with subjective-logic opinions](#assertions-with-subjective-logic-opinions) for that pattern.
The `about` field specifies which thing this assertion is about. It accepts local wrefs (`Location/cave`), canonical wrefs (`wh:org/repo/Location/cave`), or [inline collection syntax](#inline-syntax) (`{ "pair": ["Location/A", "Location/B"] }`). Assertion names follow the same [naming conventions](/data-modeling/naming-as-navigation/) as things — hierarchical names work here too.
## Immutable About
The `about` target is set at creation and **cannot be changed**. On revise, you can update the assertion's data, but never its about reference (use [retract](/writes/operations/#retract-operations) to mark an assertion inactive):
```bash
# CLI — update the assertion's data; about stays the same
wh assertion revise Observation/cave-safe --data '{"safe": false, "confidence": 0.3}' -m "Update observation"
```
```ts
// SDK
await client.commit.apply("myorg", "world", "Update observation", [
{ operation: "revise", kind: "assertion", name: "Observation/cave-safe", data: { safe: false, confidence: 0.3 } },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "revise", "kind": "assertion", "name": "Observation/cave-safe", "data": { "safe": false, "confidence": 0.3 } }
]
}
}
```
This immutability is a core design principle — it guarantees that the relationship between an assertion and its subject is stable and auditable.
**If you assert to the wrong target by mistake**, the recovery path is:
1. **Retract** the mis-targeted assertion
2. **Create a new assertion** pointing at the correct target
```bash
# CLI
wh assertion retract Observation/cave-safe --reason "Wrong target — meant Location/dungeon"
wh assertion create --shape Observation --name dungeon-safe --about Location/dungeon --data '{"safe": true, "confidence": 0.8}'
```
```ts
// SDK — both steps in one commit
await client.commit.apply("myorg", "world", "Re-target observation", [
{ operation: "retract", name: "Observation/cave-safe", reason: "Wrong target — meant Location/dungeon" },
{ operation: "add", kind: "assertion", name: "Observation/dungeon-safe", about: "Location/dungeon", data: { safe: true, confidence: 0.8 } },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "retract", "name": "Observation/cave-safe", "reason": "Wrong target — meant Location/dungeon" },
{ "operation": "add", "kind": "assertion", "name": "Observation/dungeon-safe", "about": "Location/dungeon", "data": { "safe": true, "confidence": 0.8 } }
]
}
}
```
The retracted assertion remains in the version history for auditability, but is hidden from default queries.
## Version Pinning
When you create an assertion, the `about` target is **version-pinned** to the exact version of the subject at commit time:
- `about: "Location/cave"` — bare wref, auto-pinned to current HEAD version
- `about: "Location/cave@v3"` — explicit pin to version 3
- `about: "Location/cave@HEAD"` — resolved to current HEAD version number
This is the **write-path** behavior — bare wrefs in commit operations always resolve to `@HEAD`. Read operations may resolve bare wrefs differently depending on the endpoint (see [Version Modifiers](/data-modeling/wrefs/#version-modifiers) for full defaults).
The pinned version is recorded alongside the assertion as the precise version the assertion was made about. This means you can always answer "which version of cave was this assertion about?"
Because the target is version-pinned, retracting or renaming it never orphans the assertion — see [Retract, Rename & Schema Changes](/data-modeling/retract-rename-schema-changes/).
## Querying Assertions
List all assertions about a specific thing:
```bash
wh thing about Location/cave
```
From the SDK, `client.thing.about` returns `{ target, assertions, nextCursor }` — the array is named **`assertions`**, not `items`. (`HeadResult`, `FilterResult`, `SearchResult`, `RefsResult`, and `LogResult` all use `items`; `AboutResult` is the outlier.)
```ts
const { target, assertions } = await client.thing.about("acme", "world", "Location/cave");
for (const a of assertions) {
console.log(a.wref, a.shapeName);
}
```
Filter by shape:
```bash
wh thing about Location/cave --shape Observation
```
Include child assertions about the returned assertions:
```bash
wh thing about Location/cave --depth 2
```
`wh assertion list --about Location/cave` is the equivalent assertion-domain form when you are already browsing assertions.
Browse all assertions in HEAD:
```bash
wh assertion list
wh assertion list --shape Observation
```
To inspect one assertion's full details, use `wh assertion view`
(or equivalently `wh thing view`, since assertions are things):
```bash
wh thing about Location/cave --shape Observation
wh assertion view Observation/cave-safe
wh thing view Observation/cave-safe # equivalent
```
## Inline Syntax
The `about` field supports structured collection objects for making assertions about groups of things:
```json
{
"operation": "add",
"kind": "assertion",
"name": "Distance/A-B",
"about": { "pair": ["Location/A", "Location/B"] },
"data": { "value": 5 }
}
```
This automatically creates a `Pair` collection thing and pins the assertion to it. See [Collections](/data-modeling/collections/) for the full syntax.
## Designing About References
The `about` reference is more than a foreign key — it's a **modeling decision** that determines how assertions cluster around subjects. Choose your about targets thoughtfully.
### The about ref defines your navigation axis
The about target determines what you'll browse by. If you assert about `Company/acme`, you can later query "everything we've asserted about Acme." If you instead assert about `Filing/acme/10-k/2024`, your assertions cluster around individual filings — a different navigation axis.
Choose based on how agents and humans will explore the data: **what will you most often want to ask "what do we know about X?" for?**
### About targets must exist
The write pipeline resolves the `about` wref and verifies that the target thing exists. If the target doesn't exist, the assertion operation fails with a NOT_FOUND error. This means you cannot assert about an entity before it has been added.
If you need to create a thing and immediately assert about it, put both operations in one write request — the thing is created before the assertion is resolved:
```bash
wh commit submit --ops '[
{"operation": "add", "kind": "thing", "name": "Company/acme", "data": {"industry": "fintech"}},
{"operation": "add", "kind": "assertion", "name": "Thesis/acme-bull", "about": "Company/acme", "data": {"outlook": "bullish"}}
]' -m "Add company with initial thesis"
```
### Put identifying data in the payload
Don't rely solely on the about wref to carry key identifiers. If an assertion is about `Company/acme`, include the company name or ticker in the assertion's data too — this makes the assertion self-describing when read in isolation, without requiring a follow-up query to resolve the about target.
---
# Collections
> Pair, Triple, Set, and List — built-in collection shapes for grouping things.
Collections turn groups of [things](/data-modeling/things/) into first-class entities. A Pair of two things is itself a thing — with its own [shape](/data-modeling/shapes/), version history, and [wref](/data-modeling/wrefs/). Because collections are things, they can be the target of [assertions](/data-modeling/assertions/) via the standard `about` model.
:::note[`type` picks the collection shape, not an entity tag]
On a collection operation, `type` is one of four fixed values — `"pair"`, `"triple"`, `"set"`, `"list"` — and selects which built-in collection shape to use. It is **not** an entity classification or a repo-scoped tag registry. The SDK type alias `CollectionTag` mirrors this union exactly. If you want to classify or tag entities, use a regular field on the thing's shape instead.
:::
## Collection Types
| Type | Ordered? | Unique? | # of Things | Field Names |
|------|----------|---------|-------------|-------------|
| Pair | yes | no | 2 | first, second |
| Triple | yes | no | 3 | first, second, third |
| Set | no | yes | 1+ | members |
| List | yes | no | 1+ | items |
The four built-in collection shapes (Pair, Triple, Set, List) are auto-created on first use. They cannot be created or revised manually. See [Shapes — Built-in Shapes](/data-modeling/shapes/#built-in-shapes) for the full list of core built-in shapes.
## Inline Syntax
Inline collection syntax lets you create both a collection and an assertion about that collection in a single operation, instead of two. It appears in the `about` field on assertions, which accepts either a wref string or a tagged collection object:
```typescript
about: "Location/A" // single thing
about: { pair: ["Location/A", "Location/B"] } // Pair
about: { triple: ["Player/alice", "Cell/1-1", "Item/g"] } // Triple
about: { set: ["Cell/0-0", "Cell/0-1", "Cell/1-0"] } // Set
about: { list: ["Cell/0-0", "Cell/0-1", "Cell/0-0"] } // List
```
The tag is always explicit. Each tagged object must have exactly one key.
## Explicit Creation
Collections can be created without an assertion using `kind: "collection"`. There is no dedicated collection verb — submit the same operation through the CLI, SDK, or MCP. See [Collection operations](/writes/operations/#collection-operations) for the full contract.
```bash
# CLI
wh commit submit --ops '[{"operation":"add","kind":"collection","type":"pair","members":["Location/A","Location/B"]}]' -m "Pair A and B"
```
```ts
// SDK
await client.commit.apply("myorg", "world", "Pair A and B", [
{ operation: "add", kind: "collection", type: "pair", members: ["Location/A", "Location/B"] },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "add", "kind": "collection", "type": "pair", "members": ["Location/A", "Location/B"] }
]
}
}
```
With a custom name:
```json
{
"operation": "add",
"kind": "collection",
"type": "pair",
"name": "my-custom-pair",
"members": ["Location/A", "Location/B"]
}
```
If `name` is omitted, it's auto-generated from the members.
## Retracting a Collection
Collections have no manual revise — membership is fixed at creation, so a different set of members is a *different* collection with its own canonical name, not a new version of this one. To withdraw a collection, retract it by wref like any other thing:
```bash
# CLI
wh thing retract Pair/Location/Av1+Bv1 -m "No longer related"
```
```ts
// SDK
await client.commit.apply("myorg", "world", "Retract pair", [
{ operation: "retract", name: "Pair/Location/Av1+Bv1" },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "retract", "name": "Pair/Location/Av1+Bv1" }
]
}
}
```
## Canonical Naming
Collection names are derived deterministically from their members. All members are version-pinned.
### Homogeneous (all members share one shape)
```
Pair/Location/Av1+Bv1 — Pair of Location/A@v1, Location/B@v1
Set/Cell/0-0v1+0-1v1+1-0v1 — Set of 3 Cells (sorted)
List/Location/Av1+Bv1+Cv1+Av1 — List with duplicates preserved
```
The wref starts with the collection type, then the shared member shape, then member names joined by `+` with version pins:
`CollectionShape/MemberShape/bareNameVersionPin+joined+by+plus`
### Heterogeneous (members have different shapes)
```
Pair/Player+Location/alicev1+cavev1 — Player/alice@v1, Location/cave@v1
Triple/Player+Cell+Item/alicev1+cavev1+goldv1 — three different shapes
```
When members have different shapes, the shape names are joined by `+` before the `/`, followed by the member names in the same order:
`CollectionShape/Shape1+Shape2+.../name1v1+name2v1+...`
## Version Pinning
All collection members are always version-pinned. Bare wrefs and `@HEAD` are auto-resolved to the current HEAD version at commit time:
```typescript
// Bare wrefs — auto-pinned to HEAD
about: { pair: ["Location/A", "Location/B"] }
// If A=v2, B=v1 → creates Pair/Location/Av2+Bv1
// Explicit version pins — pass through unchanged
about: { pair: ["Location/A@v1", "Location/B@v3"] }
// Creates Pair/Location/Av1+Bv3
// Intra-commit ADD — auto-pinned to @v1
operations: [
{ operation: "add", kind: "thing", name: "Location/X", data: { x: 1 } },
{ operation: "add", kind: "assertion", name: "D/x",
about: { pair: ["Location/X", "Location/Y"] }, data: { v: 1 } },
]
// Location/X (created in same commit) → auto-pinned to @v1
```
Because members are pinned by identity, retracting or renaming a member never breaks the collection — the member still resolves, and a rename surfaces under the new name automatically. See [Retract, Rename & Schema Changes](/data-modeling/retract-rename-schema-changes/).
## Normalization Rules
- **Set**: members are deduplicated and sorted lexicographically. `{B, A, A}` becomes `{A, B}`.
- **List**: order preserved, duplicates kept. `[A, B, A]` stays `[A, B, A]`.
- **Pair**: exact — `(A, B)` is not `(B, A)`.
- **Triple**: exact — order matters for all three positions.
## Idempotent Behavior
Same members at the same versions produce the same collection thing:
```typescript
// Commit 1: creates Pair/Location/Av1+Bv1
{ about: { pair: ["Location/A", "Location/B"] } }
// Same commit or later (members not revised): reuses the same Pair thing
{ about: { pair: ["Location/A", "Location/B"] } }
// After revising Location/B to v2: creates NEW Pair/Location/Av1+Bv2
{ about: { pair: ["Location/A", "Location/B"] } }
```
## Composability
Collections are things — they can be members of other collections:
```typescript
// Create a pair of pairs
{ about: { pair: ["Pair/Location/Av1+Bv1", "Pair/Location/Cv1+Dv1"] } }
```
## Directional vs Symmetric
Use **Pair** for directional relationships (order matters):
```typescript
// alice → bob trust (different from bob → alice)
{ about: { pair: ["Player/alice", "Player/bob"] }, data: { trust: 0.8 } }
```
Use **Set** for symmetric relationships (order doesn't matter):
```typescript
// Adjacency is symmetric: {A,B} = {B,A}
{ about: { set: ["Cell/0-0", "Cell/0-1"] }, data: { direction: "north" } }
```
## Querying Through Collections
By default, querying `--about Location/A` only returns assertions that directly target `Location/A`. Assertions about a Pair or Set containing `Location/A` are not included.
Use `--resolve-collections` to also include assertions about collections containing the target:
```bash
# Direct assertions about Location/A only
wh thing about Location/A
# Also includes assertions about Pair/Location/Av1+Bv1, Set/Location/Av1+Bv1+Cv1, etc.
wh thing about Location/A --resolve-collections
```
Collection resolution is HEAD-only — it finds collections that **currently** contain the thing. It does not resolve historical memberships for versioned queries (`@vN`).
Supported on: `wh thing about`, `wh thing query`, `wh thing history`, `wh thing search` (text mode), and `wh assertion list --about`.
## Token Integration
[Batch tokens](/data-modeling/wrefs/#batch-tokens) (`$N` and `#N`) work in collection member wrefs, allowing you to create things and reference them in collection assertions within the same commit. Tokens are resolved before version pinning and collection expansion.
---
# Content Shape (built-in)
> The built-in Content shape provides three well-known instance names for conventional repo markdown — Readme, Agents, and LlmsTxt.
WarmHub provides a single built-in `Content` shape for conventional repo markdown. Three well-known instance names are recognized:
| Name | Stored / Synthesized | Purpose |
|-----------|----------------------|----------------------------------|
| `Readme` | Stored | Human-facing README |
| `Agents` | Stored | AI-agent guidance (AGENTS.md) |
| `LlmsTxt` | Synthesized | Sitemap per [llmstxt.org](https://llmstxt.org) |
The `Content` shape has a single field: `content: string`.
> **Before you write or read `Content/LlmsTxt`:** it is read-only — write attempts are rejected, because its content is generated from live repo data rather than stored. Unauthenticated reads return only the basic body (no reference sections). `Content/Readme` and `Content/Agents` are writable by any authenticated caller with `things:write` scope. See [Synthesized `Content/LlmsTxt`](#synthesized-contentllmstxt) for the full behavior.
## Fetch matrix
Every surface supports the same three names with parallel verbs:
| Surface | `Content/Readme` | `Content/Agents` | `Content/LlmsTxt` |
|-----------|-----------------------------------------|-----------------------------------------|----------------------------|
| CLI | `wh repo content {get,set,prompt} --kind readme` | `wh repo content {get,set,prompt} --kind agents` | `wh repo content get --kind llms-txt` |
| SDK | `client.repo.getReadme/setReadme` | `client.repo.getAgents/setAgents` | `client.repo.getLlmsTxt` |
| MCP | `warmhub_repo_content_{get,set}` (`kind: readme`) | `warmhub_repo_content_{get,set}` (`kind: agents`) | `warmhub_repo_content_get` (`kind: llms-txt`) |
| Raw HTTP | `GET /{org}/{repo}/readme.md` | `/{org}/{repo}/agents.md` | `/{org}/{repo}/llms.txt` |
## Empty-stub semantics
Every read endpoint returns 200 with content — possibly empty. Until the first write, the response is a *synthesized* empty stub rather than an absent or null body:
```json
{
"synthesized": true,
"shape": "Content",
"name": "Readme",
"data": { "content": "" },
"active": true
}
```
After the first commit, subsequent reads return the stored content thing.
> **SDK note.** `client.repo.getReadme()` and `client.repo.getAgents()` can return `null` by contract. In practice the backend returns the synthesized empty stub described above instead of null, but TypeScript callers should still null-check defensively to match the type contract.
## Discovery via `repo.describe`
The repo describe aggregate (`warmhub_repo_describe` MCP tool) returns an `additionalInformation` array pointing at the three well-known wrefs:
```json
"additionalInformation": [
{ "name": "Readme", "wref": "Content/Readme", "synthesized": false },
{ "name": "Agents", "wref": "Content/Agents", "synthesized": false },
{ "name": "LlmsTxt", "wref": "Content/LlmsTxt", "synthesized": true }
]
```
Use this field to discover where conventional content lives in any repo without hardcoding wrefs.
## Synthesized `Content/LlmsTxt`
`Content/LlmsTxt` is rendered server-side per request from live repo data. It is read-only — writes are rejected (error code: `READ_ONLY_BUILTIN_CONTENT`).
The rendered body follows the [llmstxt.org](https://llmstxt.org) convention:
```
# org/repo
> repo description
## Shapes
- [ShapeName](Shape/ShapeName): description
## Things by shape (sample)
- [Shape/name](Shape/name)
## Outbound references — same org
## Outbound references — cross-org
## Inbound references — same org
## Inbound references — cross-org
```
Cross-org refs the caller cannot read are omitted entirely. Unauthenticated callers receive the basic body (H1, description, shapes) without the ref sections.
The `refs` field in the SDK response carries the structured reference data for authenticated callers:
```ts
const result = await client.repo.getLlmsTxt('org', 'repo')
// result.data.content — rendered markdown body
// result.refs?.outbound.sameOrg — same-org outbound refs (authenticated only)
// result.refs?.outbound.crossOrg — cross-org outbound refs
// result.refs?.inbound.sameOrg — same-org inbound refs
// result.refs?.inbound.crossOrg — cross-org inbound refs
```
## CLI examples
```bash
# Fetch Readme
wh repo content get org/repo --kind readme
# Set Readme from a file
wh repo content set org/repo --kind readme --file readme.md
# Set Readme inline
wh repo content set org/repo --kind readme --content '# My Repo'
# Print an agent-ready prompt to draft a Readme locally (no hosted LLM).
# Your own agent drafts the markdown, then you save it with `set` below.
wh repo content prompt org/repo --kind readme
# Fetch AGENTS.md guidance
wh repo content get org/repo --kind agents
# Set AGENTS.md
echo '# Agent Guide' | wh repo content set org/repo --kind agents
# Fetch synthesized llms.txt
wh repo content get org/repo --kind llms-txt
```
## SDK examples
```ts
import { WarmHubClient } from '@warmhub/sdk-ts'
const client = new WarmHubClient({ auth: { getToken: async () => process.env.WH_TOKEN } })
// Read
const readme = await client.repo.getReadme('acme', 'world')
console.log(readme.data?.content)
const agents = await client.repo.getAgents('acme', 'world')
const llmsTxt = await client.repo.getLlmsTxt('acme', 'world')
// Write
await client.repo.setReadme('acme', 'world', '# World\n\nThis repo tracks game world state.')
await client.repo.setAgents('acme', 'world', '# Agent Guide\n\nRead shapes before writing.')
```
WarmHub no longer hosts README/AGENTS generation. To draft content with your own
agent, run `wh repo content prompt --kind readme`, let your agent write
the markdown, then save it with `client.repo.setReadme(...)` or `wh repo content set`.
## MCP examples
```json
// Fetch Readme
{ "name": "warmhub_repo_content_get", "arguments": { "orgName": "acme", "repoName": "world", "kind": "readme" } }
// Set AGENTS.md
{ "name": "warmhub_repo_content_set", "arguments": { "orgName": "acme", "repoName": "world", "kind": "agents", "content": "# Agent Guide" } }
// Fetch synthesized llms.txt
{ "name": "warmhub_repo_content_get", "arguments": { "orgName": "acme", "repoName": "world", "kind": "llms-txt" } }
```
## Preflight gates
The commit pipeline enforces a closed set at write time:
- Writing `Content/LlmsTxt` is rejected — it is synthesized and cannot be stored (error code: `READ_ONLY_BUILTIN_CONTENT`).
- Writing `Content/` (outside the three well-known names) is rejected (error code: `UNKNOWN_CONTENT_NAME`).
- Writing `Content/Readme` or `Content/Agents` is allowed to any authenticated user with `things:write` scope.
- `Content/Readme` and `Content/Agents` values are limited to 64 KiB (`65,536` UTF-8 bytes).
---
# Naming as Navigation
> How hierarchical thing names create a navigable knowledge structure — and why this matters for agents.
Names in WarmHub — for [things](/data-modeling/things/) and [assertions](/data-modeling/assertions/) alike — can contain `/` to create hierarchical paths, like directories in a filesystem. This isn't a cosmetic feature. **Naming is navigation.** A well-designed namespace turns a flat bag of records into a structure that agents can explore, predict, scope, and react to.
This hierarchy doesn't start at the thing name — it extends all the way up through WarmHub's URL structure:
```
app.warmhub.ai/orgs/acme/repos/catalog/Product/electronics/phones/pixel-9
───┬─ ──┬─── ──┬─── ─────────┬──────────────
org repo shape thing name
```
Organization, repository, [shape](/data-modeling/shapes/), and thing name compose into one continuous path. The slashes in your thing names are a natural extension of the hierarchy that already exists — from org to repo to shape to the deepest leaf of your data.
One important difference from a filesystem: **intermediate path segments don't need to exist as their own entities.** If you have `Product/electronics/phones/pixel-9`, there is no requirement for a thing called `Product/electronics` or `Product/electronics/phones` to exist. The hierarchy lives in the naming convention, not in a tree of parent objects. Glob queries like `Product/electronics/**` still work — they match against the name string, not against a chain of parent entities. This means you can design deep, descriptive paths without needing to populate every level.
## Two Ways to Move Through Knowledge
A WarmHub repo gives you two navigation axes:
**Vertical — the hierarchy.** Slash-separated names create parent-child relationships. If you're looking at `Product/electronics/phones/pixel-9`, you can move *up* to see all phones, all electronics, or every product in the catalog. This is tree navigation — the same mental model as a filesystem.
**Lateral — assertions.** Assertions link things to other things across the hierarchy. A `Review` assertion might connect `Product/electronics/phones/pixel-9` to `Brand/google`. That link cuts across the tree, connecting two branches that hierarchy alone can't reach.
Together, these form a **web** — not a flat list, not a rigid tree, but a navigable structure with both predictable paths (hierarchy) and cross-cutting connections (assertions).
```
Product/ Brand/
├── electronics/ ├── google ◄─────────────────┐
│ ├── phones/ ├── apple │
│ │ ├── pixel-9 ─────────────────│── Review assertion ─────────┘
│ │ └── iphone-16 └── samsung
│ └── laptops/
│ ├── macbook-air Supplier/
│ └── thinkpad-x1 ├── asia/
├── home/ │ ├── foxconn
│ ├── kitchen/ │ └── tsmc
│ └── lighting/ └── na/
└── outdoor/ └── intel
```
An agent exploring this structure can navigate the product tree to find a specific item, then follow assertions laterally to discover its brand — and from that brand, follow *their* assertions to find other products, ratings, or supplier relationships. The hierarchy gets you to the right neighborhood; assertions let you traverse the graph.
## Why This Matters for Agents
If you're an AI agent working with a WarmHub repo, hierarchical naming changes what's possible within your context window.
### Scoped exploration
You can't load an entire repo. But you can narrow to a subtree:
```bash
# Everything in electronics
wh thing list --match "Product/electronics/**"
# Just phones
wh thing list --match "Product/electronics/phones/*"
# All categories, just 2024 models
wh thing list --match "Product/**/2024/*"
```
This is the difference between searching and navigating. Searching requires you to know what you're looking for. Navigation lets you explore a structure and find things you didn't know existed.
### Predictive navigation
If you see `Sensor/building-a/floor-3/temp`, you can predict that `Sensor/building-a/floor-2/temp` probably exists. If you see `Report/2024-q1/summary`, you can guess at `Report/2024-q2/summary`. Hierarchy makes the unknown discoverable by analogy — you can infer the namespace conventions from a few examples and then navigate confidently to things you haven't seen yet.
Flat naming (`sensor-building-a-floor-3-temp`) carries the same information for humans, but gives agents no structure to parse, no segments to substitute, no tree to traverse.
### Self-documenting addresses
The [wref](/data-modeling/wrefs/) `Article/tech/2024/transformer-scaling` tells you what it is before you read any data. The hierarchy *is* metadata — category, year, topic — encoded in the address itself. An agent can reason about what a thing represents purely from its name, without fetching its data.
### Reactive subtree watching
[Subscriptions](/subscriptions/overview/) can filter on `match` with the same glob syntax used by `wh thing list --match`, so you can set up Actions that watch entire branches of the hierarchy — or any subset expressible as a glob:
```json
{
"all": [
{ "operation": "add" },
{ "kind": "thing" },
{ "match": "Sensor/building-a/**" }
]
}
```
Any new thing added under `Sensor/building-a/` — any floor, any sensor type — triggers the action. The hierarchy becomes an event routing structure, not just an address scheme. You can build pipelines that react to *categories* of data without enumerating every possible thing name, and you can use globstars (`Sensor/**/temp`), single-segment wildcards (`Product/*/phones/*`), or brace expansion (`Sensor/hq/floor-{3,4}/*`) to express exactly the subset you care about.
## Designing Your Namespace
### Lead with the natural grain of the domain
The best hierarchies follow how the data is actually organized, queried, and explored. Ask: "What are the most common ways someone will want to narrow into this data?"
**Product catalog** — the natural grain is category, then subcategory, then item:
```
Product/electronics/phones/pixel-9
Product/electronics/laptops/macbook-air
Product/home/kitchen/instant-pot-duo
Product/outdoor/camping/rei-half-dome
```
This lets you query by top-level category (`Product/electronics/**`), by subcategory (`Product/electronics/phones/*`), or across categories for a specific type (`Product/**/camping/*`).
**IoT sensor network** — the natural grain is physical location, then sensor type:
```
Sensor/hq/floor-3/conference-a/temp
Sensor/hq/floor-3/conference-a/humidity
Sensor/hq/floor-3/hallway/motion
Sensor/warehouse/zone-b/temp
```
An agent monitoring building conditions can glob `Sensor/hq/floor-3/**` to get everything on that floor, or `Sensor/**/temp` to compare temperatures across all locations.
**Research corpus** — the natural grain is source, then year, then topic:
```
Paper/arxiv/2024/attention-mechanisms-survey
Paper/arxiv/2023/transformer-efficiency
Paper/acl/2024/multilingual-rag
```
### Put the most-filtered dimension first
The first segments after the shape should be the dimensions you'll filter on most often. If you almost always query by region, put region first. If you almost always query by date, lead with date.
```
# Category-first — good when most queries browse by type
Product/electronics/phones/pixel-9
# Region-first — good when most queries are geography-scoped
Product/us/electronics/phones/pixel-9
```
Neither is universally right. Pick the order that matches how agents will actually explore the data.
### Keep segments meaningful and stable
Each segment should carry information. Avoid segments that are just structural padding:
```
# Every segment tells you something
Company/acme/filing/10-k/2024
# "data" and "records" are noise
Company/acme/data/records/filing/10-k/2024
```
Use stable identifiers — not timestamps, not UUIDs (unless you have a reason). Names can be changed, but stable names make wrefs readable and wrefs are how agents refer to things in conversation. A rename also breaks any wref that uses the old name, so favor stable names once others depend on them — see [Retract, Rename & Schema Changes](/data-modeling/retract-rename-schema-changes/).
### Stay flat when hierarchy doesn't earn its keep
Not everything needs deep nesting. If a shape has a few dozen things with no natural grouping, flat names are fine:
```
# No need for hierarchy here
Currency/usd
Currency/eur
Currency/jpy
```
Hierarchy should emerge from the domain, not from a desire for tidiness. Two levels is often enough. Five levels should make you pause and ask whether the extra depth is actually helping agents navigate.
### Use hierarchy for things, assertions for relationships
Hierarchy models *containment* — a product belongs to a category and subcategory. Assertions model *connections* — a product is reviewed by an agent, or a paper cites another paper.
Don't try to encode relationships in the name:
```
# Don't do this — the relationship belongs in an assertion
Product/electronics/made-by-samsung/phones/galaxy-s24
# Do this — clean hierarchy, with assertions for relationships
Product/electronics/phones/galaxy-s24
└── SuppliedBy assertion → about: Supplier/asia/samsung
└── Review assertion → about: Product/electronics/phones/galaxy-s24
```
Hierarchy answers "where does this live?" Assertions answer "what is this connected to?"
## Querying the Hierarchy
### Glob matching
The `--match` flag on query commands supports full glob syntax:
| Pattern | Meaning |
|---------|---------|
| `Product/electronics/*` | Direct children of `Product/electronics/` (one segment) |
| `Product/electronics/**` | All descendants of `Product/electronics/` (any depth) |
| `Product/*/phones/*` | All phones across all top-level categories |
| `Sensor/**/temp` | All temperature sensors at any location depth |
| `Product/{electronics,home}/**` | Electronics and home products (brace expansion) |
| `Company/acme/filing/10-*` | Partial segment match (10-K, 10-Q) |
```bash
# Explore a subtree
wh thing list --match "Sensor/hq/floor-3/**"
# Cross-cut: all temperature readings everywhere
wh thing list --match "Sensor/**/temp"
# Multiple categories
wh thing list --match "Product/{electronics,home,outdoor}/**"
```
Glob patterns also work in MCP queries and the SDK's query methods — not just the CLI.
### Combining with assertions
The real power shows when you combine hierarchical queries with assertion traversal:
```bash
# Step 1: Find all phones in the catalog
wh thing list --match "Product/electronics/phones/*"
# Step 2: For a specific product, find what's been said about it
wh thing about Product/electronics/phones/pixel-9
# Step 3: Follow a link to the brand
wh thing view Brand/google
# Step 4: What else do we know about this brand?
wh thing about Brand/google
```
This is tree-then-graph navigation: use hierarchy to get to the right neighborhood, then follow assertions to traverse the web.
### Subscription match
For reactive pipelines, `match` on subscription filters uses the same glob vocabulary as query `--match`, so the patterns you learn for navigation translate directly to what you subscribe to:
```json
{
"all": [
{ "operation": "add" },
{ "kind": "thing" },
{ "match": "Product/electronics/**" }
]
}
```
This fires whenever any new thing is added under `Product/electronics/` — phones, laptops, tablets, anything. And unlike a plain prefix, the glob lets you slice by shape patterns too: `Sensor/**/temp` to react to temperature sensors wherever they live in the tree, or `Product/{electronics,home}/phones/*` to watch phones across multiple top-level categories.
Array form OR-combines patterns (`{"match": ["A/**", "B/**"]}` matches either branch); wrap two predicates in `all` to require both.
## Patterns in Practice
### Ingestion pipeline
A scraper pulls product listings from multiple sources, organized by category:
```
Product/electronics/phones/pixel-9
Product/electronics/phones/iphone-16
Product/electronics/laptops/macbook-air
Product/home/kitchen/instant-pot-duo
```
A subscription watches `Product/electronics/` and triggers a classification action for each new item. That action creates assertions — `Review` assessments, `PriceTrack` observations, `SimilarTo` comparisons linking to other products. Now agents can navigate the product tree to find items, then follow assertions to discover relationships, ratings, and alternatives.
### Multi-agent research
Three agents investigate companies from different angles:
```
# Things — the entities
Company/acme
Company/globex
Filing/acme/10-k/2024
Filing/globex/10-k/2024
# Assertions — each agent's perspective
Thesis/acme-growth (about: Company/acme) — agent-1's growth thesis
RiskFlag/acme-debt (about: Company/acme) — agent-2's risk assessment
Comparison/acme-v-globex (about: {pair: [Company/acme, Company/globex]}) — agent-3
```
An agent reviewing Acme can:
1. Glob `Filing/acme/**` to see all filings
2. Query assertions about `Company/acme` to see every agent's perspective
3. Follow the `Comparison` assertion to discover Globex as a peer, then explore `Filing/globex/**`
The hierarchy organized the filings. The assertions connected the perspectives. The agent navigated both.
### Sensor monitoring with escalation
```
Sensor/hq/floor-3/conference-a/temp
Sensor/hq/floor-3/conference-a/co2
Alert/hq/floor-3/conference-a/co2-high-2024-03-15
```
A subscription with `match: "Sensor/hq/**"` triggers a threshold-check action for any sensor data committed under HQ. When CO2 exceeds limits, the action commits an `Alert` thing — named in the same hierarchy so it's discoverable alongside the sensor it relates to. A separate subscription with `match: "Alert/**"` triggers notifications.
The naming hierarchy is doing double duty: organizing the data *and* routing the events.
## Anti-Patterns
**Encoding relationships in names.** If you find yourself putting one thing's identity inside another thing's name (`Product/reviewed-by-agent-1/phones/pixel-9`), use an assertion instead. Names should reflect containment and categorization, not cross-references.
**UUID-heavy names.** `Product/f47ac10b-58cc-4372-a567-0e02b2c3d479` is valid but opaque. Agents can't infer anything from it. If you need generated names, use [batch tokens](/data-modeling/wrefs/#batch-tokens) (`$N`/`#N`), but prefer human-readable names when the domain has natural identifiers.
**Inconsistent depth.** If most products are `Product/category/subcategory/name` but some are `Product/name`, agents can't predict the structure. Keep the same shape's things at consistent depth, or at least document the convention. Note that consistent depth doesn't mean every level needs things at it — `Product/electronics/phones/pixel-9` works fine even if nothing exists at `Product/electronics/phones`. The point is that things of the same kind should live at the same depth so glob patterns work predictably.
**Too deep.** Every segment should earn its place. `Data/raw/ingested/pipeline-v2/batch-003/electronics/phones/pixel-9` has structural noise that doesn't help navigation. Flatten to what agents will actually filter on.
**Overloading hierarchy when a shape would work.** If you're using hierarchy to separate fundamentally different kinds of data (`Entity/person/jones` vs `Entity/company/acme`), consider whether those should be separate shapes (`Person/jones`, `Company/acme`). Shapes give you schema-level structure; hierarchy gives you instance-level organization. Use both.
---
# Modeling Overview
> How to think about modeling with WarmHub — core principles that guide every design decision.
WarmHub gives you three primitives — [shapes](/data-modeling/shapes/), [things](/data-modeling/things/), and [assertions](/data-modeling/assertions/) — connected by [wrefs](/data-modeling/wrefs/). The usual flow is to define a shape and create a thing, then — when attribution, confidence, or multiple perspectives matter — assert about it. Not every model needs that last step.
## The modeling lifecycle
`Company` and `Assessment` here are example shapes you define — not built-ins.
```bash
# 1. Define the shapes — one for the entity, one for a claim about it
wh shape create Company --repo myorg/myrepo --fields '{"name":"string","domain":"string"}'
wh shape create Assessment --repo myorg/myrepo --fields '{"verdict":"string","confidence":"number"}'
# 2. Create a thing — an entity with one canonical state
wh thing create Company/acme --repo myorg/myrepo --data '{"name":"Acme Corp","domain":"acme.com"}'
# 3. Assert about it — attribution and confidence, kept separate from the entity's state
wh assertion create --shape Assessment --about Company/acme --repo myorg/myrepo --data '{"verdict":"competitor","confidence":0.8}'
```
Each write is versioned, and the assertion's `about` target is fixed at creation — see the principles below. From here, the detail pages cover each primitive in depth.
## Core Principles
A few principles make these primitives work together.
**Everything is versioned, nothing is deleted.** Every change to a thing's data produces a new version — an in-place [rename](/data-modeling/retract-rename-schema-changes/) is the exception, since it edits identity, not data. Retraction hides an entity from default queries but preserves its full history. This means you can always ask "what did we know at time T?" — so don't be afraid to write data early, even if it's uncertain.
**Writes are versioned per operation.** A write can contain multiple operations. Use this to keep related changes close together — add a thing and its initial assertions in the same write instead of spreading them across separate requests.
**Names are stable, not permanent.** Names identify things and appear in [wrefs](/data-modeling/wrefs/) (references like `Shape/name`), but they can be changed — even on retracted things. When a thing is renamed, identity-based references — assertion targets, collection members, and durable ids — follow it automatically, but a wref that uses the old name stops resolving. Things are linked by identity, not by name — the name is a human-readable label on top of that link. Choose names that are meaningful and stable, and prefer [durable ids](/data-modeling/wrefs/#durable-ids) for long-lived keys. See [Retract, Rename & Schema Changes](/data-modeling/retract-rename-schema-changes/) for the full rename behavior and [Naming as Navigation](/data-modeling/naming-as-navigation/) for designing effective names.
**The `about` reference is immutable.** An assertion's target is set at creation and cannot be changed. If you assert about the wrong thing, retract it and create a new assertion. This immutability guarantees that the relationship between an assertion and its subject is stable and auditable.
## Choosing Your Primitives
The most common modeling question is: **should this be a thing or an assertion?**
Use **things** when you're modeling entities with a single canonical state — the objects your system reasons about. A company, a sensor, a document. If there's one truth about this entity, it's a thing. See [Things — When to Use Things](/data-modeling/things/#when-to-use-things) for details.
Use **assertions** when attribution, confidence, or multiple perspectives matter — when you need to know *who* said something about an entity, not just the current state. An agent's assessment, a probabilistic claim, a scored evaluation. See [Assertions — When to Use Assertions](/data-modeling/assertions/#when-to-use-assertions) for details.
Not everything needs to be an assertion. A lookup table of country codes is fine as plain things. An agent's assessment of whether a competitor is a threat — that's an assertion.
## What's in This Section
| Page | What it covers |
|------|---------------|
| [Shapes](/data-modeling/shapes/) | Schema definitions with typed fields and constraints |
| [Things](/data-modeling/things/) | Named, versioned entities — the core data objects |
| [Naming as Navigation](/data-modeling/naming-as-navigation/) | How hierarchical names create navigable knowledge structures |
| [Assertions](/data-modeling/assertions/) | Claims about things — with immutable targets and version pinning |
| [Wrefs](/data-modeling/wrefs/) | The addressing system — how to reference any entity |
| [Collections](/data-modeling/collections/) | Pair, Triple, Set, and List — built-in grouping |
| [Retract, Rename & Schema Changes](/data-modeling/retract-rename-schema-changes/) | What happens to references when you retract, rename, or evolve a shape |
| [Patterns & Recipes](/data-modeling/patterns/) | Common modeling patterns that work well in practice |
---
# Patterns & Recipes
> Common modeling patterns for agent memory, multi-agent collaboration, and evolving understanding.
This page collects modeling patterns that work well in practice. Each pattern builds on the core primitives — [shapes](/data-modeling/shapes/), [things](/data-modeling/things/), [assertions](/data-modeling/assertions/), and [wrefs](/data-modeling/wrefs/).
## Agent Memory
An agent writes observations as assertions about entities it encounters:
1. Define a `Location` shape (or whatever your domain entities are)
2. Define an `Observation` shape with fields like `confidence`, `source`, `evidence`
3. The agent adds things as it discovers entities
4. The agent creates assertions about those things as it discovers them
5. Other agents (or humans) query those assertions to build on that knowledge
```bash
# Agent discovers a new entity
wh commit submit --add cave --shape Location --data '{"x": 3, "y": 7}' -m "Discovered cave"
# Agent records what it observed
wh assertion create --shape Observation --about Location/cave \
--data '{"safe": true, "confidence": 0.8, "source": "agent-1"}' -m "Initial observation"
```
Knowledge compounds — each session builds on the previous one. The agent doesn't start from zero because its prior assertions persist.
## Multi-Agent Collaboration
Multiple agents write to the same repo. Each commit is attributed, so you can trace who said what:
- Agent A and Agent B can both assert about the same thing
- Their assertions coexist — WarmHub doesn't force consensus
- A downstream process can compare, reconcile, or aggregate their views
```bash
# Agent 1's assessment
wh assertion create --shape Thesis --about Company/acme \
--data '{"outlook": "bullish", "confidence": 0.7}' -m "Agent 1 thesis"
# Agent 2's competing assessment
wh assertion create --shape Thesis --about Company/acme \
--data '{"outlook": "bearish", "confidence": 0.6}' -m "Agent 2 thesis"
# Both coexist — query to compare
wh thing about Company/acme --shape Thesis
```
This pattern works because assertions are attributed and non-exclusive. There's no conflict — just different perspectives.
## Evolving Understanding
As knowledge changes, revise assertions rather than retracting and recreating:
- `revise` updates the assertion's data while preserving its identity and history
- The `about` reference stays the same (it's immutable)
- You can query the full version history to see how understanding evolved
```bash
# Initial observation (explicit name so we can revise it later)
wh assertion create --shape Observation --name cave-safe --about Location/cave \
--data '{"safe": true, "confidence": 0.8}' -m "First visit"
# Confidence changes after new evidence
wh assertion revise Observation/cave-safe --data '{"safe": false, "confidence": 0.3}' -m "Found hazard"
```
Only retract when an assertion is truly no longer valid — not just outdated. Revision preserves the full history of the assertion.
## Opinions as Separate Assertions
When modeling subjective logic opinions `(b, d, u, α)` — belief, disbelief, uncertainty, and base rate (serialized as the `a` field in JSON) — keep opinion metadata **separate from the data being asserted**. These values describe *trust in an assertion*, not properties of the thing being described.
:::note[Opinions require binary propositions]
A subjective-logic opinion `(b, d, u, α)` is a **binomial opinion** — it models belief over a two-outcome frame. The assertion it is attached to must be a **binary proposition**: a statement that is either true or false. Attaching an opinion to an open-ended claim (e.g. *"how tall is Greg?"*) produces numbers that satisfy `b+d+u=1` but are semantically meaningless. Phrase the underlying claim as a binary proposition first (e.g. *"Greg is at least 5'4\""*).
:::
**Don't do this** — mixing finding data with opinion metadata:
```json
{
"shape": "DocFinding",
"data": {
"severity": "high",
"category": "security",
"b": 0.8,
"d": 0.1,
"u": 0.1
}
}
```
**Do this** — a finding thing and a separate opinion assertion about it, in one write request:
```bash
wh commit submit --ops '[
{"operation": "add", "kind": "thing", "name": "DocFinding/finding-$1",
"data": {"severity": "high", "category": "security"}},
{"operation": "add", "kind": "assertion", "name": "Opinion/finding-opinion-$2",
"about": "DocFinding/finding-#1",
"data": {"b": 0.8, "d": 0.1, "u": 0.1, "source": "scanner-agent"}}
]' -m "Security finding with scanner confidence"
```
Why separate?
- The finding's severity is a fact — it doesn't vary by who observed it
- The opinion (`b: 0.8`) is the scanner agent's *confidence* in that finding — another agent might disagree
- Multiple agents can hold different opinions about the same finding
- You can query all opinions about a finding and fuse them without touching the finding data
This separation follows the same principle as things vs assertions: **data about the world goes in things; opinions about that data go in assertions**.
If you want WarmHub to consolidate opinions across sources and weight them by track record, install [Veritas](/veritas/overview/) — its `Certainty`, `Support`, `Opposition`, and `Consensus` shapes implement this pattern directly. Veritas's installed shapes use the long-form field names `belief`, `disbelief`, `uncertainty`, and `alpha`. The `(b, d, u, α)` notation in this section is the mathematical form; user-defined opinion shapes (like the `DocFinding`/`Opinion` example above) typically serialize those as the short JSON keys `b`, `d`, `u`, and `a`. Pick the field-name convention that matches the shape you're writing against.
## Under-Grouping
:::caution[Group related operations]
Five separate write requests for five related changes means five separate coordination points. Group related operations in one request when later operations naturally depend on earlier ones, and handle partial success: each operation reports its own status and a later failure does not roll back earlier successes. A request can still fail before any per-operation results are returned (for example, auth or malformed input), so handle both per-op result rows and request-level errors. See [Atomicity](/writes/overview/#atomicity).
```bash
wh commit submit --ops '[
{"operation": "add", "kind": "thing", "name": "Company/acme", "data": {"industry": "fintech"}},
{"operation": "add", "kind": "assertion", "name": "Thesis/acme-growth", "about": "Company/acme", "data": {"outlook": "bullish"}}
]' -m "Add company with initial thesis"
```
:::
---
# Retract, Rename & Schema Changes
> What happens to your data — and everything pointing at it — when you retract, rename, or change a shape.
WarmHub never destroys data. You retract things instead of deleting them, you rename things in place, and you evolve shapes over time. Each of these changes ripples outward to the wrefs, [assertions](/data-modeling/assertions/), and [collections](/data-modeling/collections/) that point at what you changed.
This page covers exactly what happens to those references in each case. It assumes you already know [things](/data-modeling/things/), [wrefs](/data-modeling/wrefs/), and [assertions](/data-modeling/assertions/).
| Change | New version? | Effect on references |
|--------|--------------|----------------------|
| Retract a thing | Yes — a retract version | None — references keep pointing at the preserved version |
| Rename a thing | No — an in-place identity edit | Identity-pinned references follow it; a wref using the old name breaks |
| Revise a shape | Yes — a new shape version | None — existing things are unchanged until their next write |
| Retract a shape | Yes — a retract version | None — things validated against it still resolve |
The throughline: assertions and collections pin the exact **version** they referenced, so a retraction or a rename can never orphan them. Only a wref that addresses a thing by its **name** is fragile, and only across a rename.
## Retraction
Retracting a thing marks it inactive. Its name, data, and full version history are preserved, and it is hidden from default queries. Retraction is the only way to make something inactive — there is no hard delete. See [Operations — Retract](/writes/operations/#retract-operations) for the operation itself and [Things — Active/Inactive Lifecycle](/data-modeling/things/) for the lifecycle.
What retraction does **not** do is cascade. Retracting a thing leaves everything that points at it in place:
| Reference to the retracted thing | What happens |
|-----------------------------------|--------------|
| Its own wref | Hidden from default reads and listings — a plain `wh thing view Location/cave` returns not-found. Its data and full version history are preserved and stay readable with [`--include-retracted`](/queries/overview/#active-vs-retracted) or a pinned `@vN` version. |
| An assertion about it | Untouched. The assertion still resolves and still points at the exact target version it was [pinned to](/data-modeling/assertions/#version-pinning) when created. |
| A collection that includes it | Untouched. The collection keeps its [version-pinned](/data-modeling/collections/#version-pinning) member reference, which still resolves to that member version. No dangling reference, no new collection version. |
| A subscription | A retract is a write operation, so a subscription whose filter matches `retract` fires on it. See [Subscription Filters](/subscriptions/filter-json/). |
Because assertions and collections pin the exact version they referenced, a retraction can never orphan them — they keep pointing at a version that still exists.
:::note[Re-adding at a retracted name]
Once a thing is retracted, you can add a new thing at the same name. It is a fresh identity with its own new version history — not a revival of the retracted one. See [Operations — Retract](/writes/operations/#retract-operations).
:::
## Rename
Renaming changes a thing's name in place — `wh thing rename Location/cave cavern` (the new name is bare; the shape stays the same), or `client.thing.rename` in the SDK. It does **not** create a new version, a commit, or a history entry, and there is no record of the old name afterward.
That makes the rule simple: **references that point by identity follow the rename; references that use the old name break.**
| Reference to the renamed thing | What happens |
|---------------------------------|--------------|
| An assertion about it | Follows the rename. Assertions link to their target by identity, so reading the assertion — or querying what it is about — resolves to the thing under its new name automatically. |
| A collection that includes it | Follows the rename. Collection members are pinned by identity, so the member resolves to the new name automatically. Nothing dangles. |
| Its [durable id](/data-modeling/wrefs/#durable-ids) | Unaffected. A durable id names a thing by identity, so it survives every rename. |
| A wref that uses the old name | Breaks. The old name no longer resolves — a lookup returns not-found. There is no redirect from the old name to the new one. |
| A subscription | Not re-evaluated. A rename is not a write operation, so it never triggers subscription matching (see below). |
:::caution[A rename has no redirect]
Anything that stored the old wref string as a long-term key — an external system, a cache, a saved query — sees a rename as the old thing disappearing and a new one appearing. If you need a key that survives renames, store the thing's [durable id](/data-modeling/wrefs/#durable-ids) instead of its wref.
:::
Shapes rename the same way: `wh shape rename` (or `client.shape.rename`) patches the shape's name in place, preserves its history, and creates no new version. Things keep validating against it, because they reference their shape by identity.
### Rename and subscriptions
[Subscriptions](/subscriptions/overview/) match write operations as they happen — an add, a revise, or a retract carrying a name, shape, and kind. A rename is not a write operation, so it produces nothing for a subscription to match.
The practical consequence: renaming a thing into or out of a subscription's name pattern — a [glob match](/data-modeling/naming-as-navigation/#subscription-match) like `Product/electronics/**` — does not notify subscribers. The renamed thing matches that subscription again only the next time it is written (its next revise or retract, under the new name). Until then, the rename is invisible to subscriptions.
## Schema changes
### Revising a shape
Revising a shape replaces its field definitions — it is a full replacement, like any [revise](/writes/operations/#revise-operations). You can add fields (optional or required), remove fields, or rename them.
Existing things are **not** re-validated when the shape changes. They keep their data and stay valid against the shape version they were written under. The new shape is enforced the next time each thing is written.
Adding a **required** field is therefore backwards-incompatible in a specific way: existing things stay valid until you next revise one, and that next revise must satisfy the new shape. [Shapes — Back-Filling Required Fields](/data-modeling/shapes/#back-filling-required-fields) covers the two migration strategies: back-fill on next revise, or mass-revise every thing immediately. To avoid the incompatibility entirely, [add the field as optional](/data-modeling/shapes/#optional-fields).
### Retracting a shape
Shapes can be retracted, with one restriction: the [built-in shapes](/data-modeling/shapes/#built-in-shapes) (Pair, Triple, Set, List, Content) cannot be. Retracting a shape you defined marks the shape inactive. The things validated against it are not deleted or changed, and they still resolve. As with any name, you can add a new shape at the same name afterward — a fresh identity.
## Idempotent revise
One change deliberately does nothing: revising a thing with data identical to its current version. WarmHub returns a no-op and creates no new version, so re-submitting an unchanged revise is always safe. See [Operations — Idempotent revise](/writes/operations/#idempotent-revise) for details.
---
# Shapes
> Define schemas for things and assertions with typed fields and versioning.
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
The same `add` shape operation works on every surface — the CLI takes `--fields`, while the SDK and MCP wrap the field map in the operation's `data.fields`. See [Write operations](/writes/operations/#add-shape) for the full contract.
```bash
# CLI
wh shape create Location --fields '{"x": "number", "y": "number", "label": "string"}'
```
```ts
// SDK
await client.commit.apply("myorg", "world", "Add Location shape", [
{ operation: "add", kind: "shape", name: "Location", data: { fields: { x: "number", y: "number", label: "string" } } },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "add", "kind": "shape", "name": "Location", "data": { "fields": { "x": "number", "y": "number", "label": "string" } } }
]
}
}
```
## 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](#array-fields)) | `["a", "b"]` |
This vocabulary is **closed**: `string`, `number`, `boolean`, `wref`, and `array` are the only field types, each optionally nullable with a trailing `?` (see [Optional Fields](#optional-fields)). Any other type **name** — such as `text`, `integer`, `json`, or `date` — is rejected. The check is authoritative on the server at apply time, and the CLI and SDK also run it locally when you create or revise a shape (`wh shape create` / `wh shape revise`, `client.shape.create` / `client.shape.revise`, or the SDK `OperationBuilder`) so the write fails before it leaves your machine. Either way the verdict is identical:
```
Invalid type at "fields.": "integer" (expected number|string|boolean|wref|array, optionally with ? suffix)
```
Rich values are still fully supported — use `number` for integers and USD amounts, `boolean` for flags, `array` for lists, and `wref` for references. The restriction is on the type-name vocabulary only, not on the values those types can hold.
### Optional Fields
There are two equivalent syntaxes for optional fields — append `?` to either the field name or the type:
```json
{ "x": "number", "y": "number", "label?": "string" }
```
```json
{ "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
Wrap the type in an array to define a list field:
```json
{ "tags": ["string"], "scores": ["number"] }
```
Typed array objects provide an alternative syntax with constraints and descriptions:
```json
{
"tags": { "type": "array", "items": "string", "minItems": 1, "maxItems": 10 },
"scores": { "type": "array", "items": "number", "description": "Player scores" }
}
```
See [Field constraints](#field-constraints) for the full set of array constraint keys.
### Nested Objects
Use a plain sub-object to define nested structure:
```json
{
"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
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"]`.
```json
{ "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](#field-constraints) 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.
:::caution[Misspelled constraint keys can be silent]
A misspelled constraint key (e.g., `"minLegnth"` instead of `"minLength"`) causes the object to be treated as a nested object instead of a typed field. Whether this is silent depends on the misspelled key's value:
- **Silent reinterpretation** — if the value is a valid type spec (like `"string"` or `["number"]`), the object is quietly treated as nested with no error.
- **Caught with an error** — if the value is not a valid type spec (e.g., a bare number like `1`), validation rejects it.
If a constraint seems to be ignored, check for typos in key names.
:::
:::note[Advanced: how ambiguous objects are classified]
When all keys in an object are recognised type-spec keys but the constraint values have wrong types, the parser catches this as a **validation error** — for example, `{ "type": "string", "minLength": "number" }` is rejected because `minLength` must be a number.
When an object has **unrecognised keys**, it is treated as a nested object. The values of those unrecognised keys are validated recursively, so non-type-spec values (like bare numbers) are still caught.
:::
### 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, at most `65,536`) — minimum string length
- `maxLength` (non-negative integer, at most `65,536`) — maximum string length; must be ≥ `minLength`
- `pattern` (string) — regular expression the value must match; WarmHub accepts a safe subset and rejects constructs that can cause excessive backtracking
- `enum` (non-empty string[]) — list of allowed values; must contain at least one entry
All string field values are also capped at 64 KiB (`65,536` UTF-8 bytes) at commit time. `minLength` and `maxLength` are string-length constraints, not byte-size checks; Unicode-heavy strings can hit the UTF-8 byte cap before they reach `65,536` characters.
`pattern` supports literals, anchors, character classes, common character escapes, bounded quantifiers, groups, named captures, and deterministic alternation. It rejects backreferences, lookaround, deeply nested groups, excessive alternation fan-out, and ambiguous repeated subpatterns that can make validation time grow explosively.
**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`
```json
{
"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
Shapes can have an optional top-level `description` that documents the shape's purpose:
```bash
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](/agent-integration/mcp-tools-reference).
### CLI `--fields` Examples
All field types work in `--fields` JSON:
```bash
# 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 }
}'
```
## Versioning
Shapes are versioned just like things. Revising a shape creates a new version:
```bash
# CLI
wh shape revise Location --fields '{"x": "number", "y": "number", "z": "number", "label": "string"}'
```
```ts
// SDK
await client.commit.apply("myorg", "world", "Add z to Location", [
{ operation: "revise", kind: "shape", name: "Location", data: { fields: { x: "number", y: "number", z: "number", label: "string" } } },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "revise", "kind": "shape", "name": "Location", "data": { "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.
### Back-Filling Required Fields
Adding a required field is a backwards-incompatible revise: existing things still validate against the older shape version they were created under, but the **next** time you revise one of those things, validation runs against the new shape and rejects the operation if the new required field is missing.
For example, given an initial shape with two required fields:
```bash
wh shape create Product --fields '{"name": "string", "price": "number"}'
wh commit submit --add Product/widget --data '{"name": "Widget", "price": 5}'
```
Adding a third required field at the shape level is fine on its own:
```bash
wh shape revise Product --fields '{"name": "string", "price": "number", "category": "string"}'
```
But the next revise of `Product/widget` must include the new field — passing the old payload alone fails:
```bash
# Fails: Missing required field "category"
wh commit submit --revise Product/widget --data '{"name": "Widget", "price": 6}'
# Works: include all current required fields
wh commit submit --revise Product/widget --data '{"name": "Widget", "price": 6, "category": "tools"}'
```
You have two options when adding a required field to a shape that already has things:
1. **Back-fill on next revise.** Each subsequent revise must include the new field. Sensible when revises are infrequent and you can fold the new value into normal updates.
2. **Mass-revise immediately.** Issue a revise for every existing thing in the same write so all instances are valid against the new shape going forward.
Option 1 keeps history clean (versions only when data actually changes), but leaves an "implicit migration" — until each thing is revised, it carries data validated only under the older shape version. Option 2 is louder in version history but eliminates the latent failure mode.
If you don't have a value to back-fill, declare the field optional instead (`"category?": "string"` or `"category": "string?"`).
## Retracting a Shape
Shapes are retracted by name, like any other entity. Existing things keep the shape version they were validated against; the shape is hidden from default reads. See [Retract, Rename & Schema Changes](/data-modeling/retract-rename-schema-changes/) for what this does to dependent things.
```bash
# CLI
wh shape retract Location --reason "replaced by the v2 Location model" -m "retract"
```
```ts
// SDK
await client.commit.apply("myorg", "world", "Retract Location shape", [
{ operation: "retract", kind: "shape", name: "Location", reason: "replaced by the v2 Location model" },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "retract", "kind": "shape", "name": "Location", "reason": "replaced by the v2 Location model" }
]
}
}
```
## Listing and Viewing Shapes
```bash
# 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
```
## 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](/components/) for details.)
**Collection shapes** — see [Collections](/data-modeling/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:**
- **`Content`** — built-in content shape (`content: string`) with three well-known instance names: `Readme` (human-facing README), `Agents` (AI-agent guidance), and `LlmsTxt` (synthesized sitemap, read-only). See [Content Shape](/data-modeling/content-shape/) for the full fetch matrix and per-surface examples.
## Designing Shapes
Shapes define structure, but WarmHub is deliberately flexible about what that structure looks like. Some guidance:
**Start simple.** You can always revise a shape to add fields. You don't need to anticipate every future field up front.
**One shape per concept, not per use case.** An `Observation` shape that captures confidence and evidence can be used across many contexts. You don't need `CaveObservationShape` and `PlayerObservationShape` — use one `Observation` shape and let the `about` reference distinguish what each assertion is about.
**Use `wref` fields for relationships.** If a thing references another thing, use a `wref`-typed field. This creates a trackable link that WarmHub can resolve and validate.
**Don't cargo-cult the examples.** The docs use game-world examples (`Location/cave`, `Observation/cave-safe`) to illustrate concepts. Your shapes should reflect *your* domain. A financial analyst might have `Company`, `Earnings`, and `Forecast` shapes. A research agent might have `Paper`, `Claim`, and `Evidence` shapes. The modeling primitives are the same — the names and fields are yours.
:::caution[Don't treat shapes as tables]
A shape is a type definition, not a SQL table. You don't need a shape for every noun — you need shapes for the *kinds of entities* in your domain.
:::
:::caution[Don't overload shape names across case]
Use PascalCase for all shape names. Don't create both `Observation` and `observation` — use a single shape and distinguish variants with `kind` or other fields.
:::
### Undeclared Fields
The commit pipeline accepts fields beyond what a shape declares — this is intentional flexibility for agents that want to attach extra context. Undeclared fields are stored, returned by the API, and visible in the web UI's raw data view. However, **only declared fields get structured rendering** — typed columns, filtering, and shape-aware display. Undeclared fields appear only in the raw JSON.
If a field matters for structured browsing or filtering, declare it in the shape. If it's purely for downstream agent consumption, undeclared fields work fine — just know they won't get first-class UI treatment.
:::tip[Watch for the warning line at write time]
WarmHub flags undeclared fields right at write time, so you don't have to discover them later when the UI renders defaults. The warning line appears in the main CLI write flows, including `wh commit submit`, `wh thing create`, `wh thing revise`, `wh assertion create`, and `wh assertion revise`. In UTF-8 terminals it uses `⚠`; in ASCII fallback mode it uses `!`. Each affected operation prints a dim warning line under its name:
```
+ DocFinding/issue-001@v1
⚠ 9 fields not declared in shape DocFinding: status, filePath, category, reviewSource, signalType, b, d, u, alpha
```
The same payload is available in CLI `--json` output, in the SDK result from `client.commit.apply()`, and in the `warmhub_commit_submit` MCP tool result, so programmatic callers can react to it too. The warning is non-blocking — the operation can still succeed, or return a no-op result when nothing changed — but it tells you exactly what to add via `wh shape revise` if those fields should be part of the structured shape.
:::
## Shape Names in Wrefs
The shape name is always the first segment in a [wref](/data-modeling/wrefs/). 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. For guidance on how thing names (the part after the shape) create navigable hierarchies, see [Naming as Navigation](/data-modeling/naming-as-navigation/).
---
# Things
> Named, versioned entities — the core data objects in WarmHub.
A **thing** is a named, versioned entity within a repository. Things are the core data objects in WarmHub — they represent the entities in your domain.
## When to Use Things
Things are the foundation. Use them for:
- **Entities in your domain** — the objects your system reasons about (locations, players, companies, documents)
- **Reference data** — stable records that serve as targets for assertions
- **Anything with a single canonical state** — if there's one truth about this entity, it's a thing
You don't need assertions just to track changes over time — things are already versioned, so `revise` gives you full history on its own.
:::tip[Things vs assertions]
Not everything needs to be an assertion. If you're the only writer and there's no uncertainty, a plain thing is simpler. Save assertions for when attribution, confidence, or multiple perspectives matter. See [Assertions — When to Use Assertions](/data-modeling/assertions/#when-to-use-assertions) for the other side of this decision.
:::
## Creating Things
Things are created through [writes](/writes/overview/). The same `add` thing operation works on every surface — the CLI offers a shorthand, while the SDK and MCP take the operation directly. See [Write operations](/writes/operations/) for the full contract.
```bash
# CLI
wh commit submit --add acme --shape Company --data '{"industry": "fintech", "stage": "series-b"}' -m "Add company"
```
```ts
// SDK
await client.commit.apply("myorg", "world", "Add company", [
{ operation: "add", kind: "thing", name: "Company/acme", data: { industry: "fintech", stage: "series-b" } },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "add", "kind": "thing", "name": "Company/acme", "data": { "industry": "fintech", "stage": "series-b" } }
]
}
}
```
The thing's wref is `Company/acme` — the shape name followed by the thing name.
## Naming
Thing names can contain `/` for hierarchical organization. See [Naming as Navigation](/data-modeling/naming-as-navigation/) for a deep dive on designing effective names — how hierarchy affects agent navigation, scoped queries, and subscription routing.
```
Location/dungeon/room-1
Location/dungeon/room-2
GameState/round-1/state
```
The first segment is always the shape name. Everything after the first `/` is the thing name. Names cannot start or end with `/`, and cannot contain `//`.
## Versioning
Every mutation that changes a thing creates a new version. Versions are numbered sequentially starting at `v1`:
```bash
# CLI — v1 initial creation, then v2 revised data
wh commit submit --add cave --shape Location --data '{"x": 3, "y": 7}'
wh thing revise Location/cave --data '{"x": 5, "y": 3}' -m "Move cave"
```
```ts
// SDK — revise produces v2
await client.commit.apply("myorg", "world", "Move cave", [
{ operation: "revise", kind: "thing", name: "Location/cave", data: { x: 5, y: 3 } },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "revise", "kind": "thing", "name": "Location/cave", "data": { "x": 5, "y": 3 } }
]
}
}
```
Each version is immutable — old versions are preserved and queryable:
```bash
# View specific version
wh thing view Location/cave --version 1
# View current (HEAD) version
wh thing view Location/cave
```
Revising a thing with data identical to its current HEAD is a no-op — no new version is created. See [Operations](/writes/operations/#conditional-operations) for details.
## Active/Inactive Lifecycle
Things have an `active` flag. By default, things are active. Retracting a thing hides it from HEAD queries and other default queries, but preserves its name, data, and full version history. A retracted thing retains its name and can still be renamed. For exactly what retracting or renaming a thing does to the assertions, collections, and wrefs that point at it, see [Retract, Rename & Schema Changes](/data-modeling/retract-rename-schema-changes/).
```bash
# CLI — retract marks inactive, records optional reason
wh thing retract Location/cave --reason "superseded by Location/cave-v2" -m "retract"
```
```ts
// SDK
await client.commit.apply("myorg", "world", "Retract cave", [
{ operation: "retract", name: "Location/cave", reason: "superseded by Location/cave-v2" },
])
```
```json
// MCP — warmhub_commit_submit
{
"name": "warmhub_commit_submit",
"arguments": {
"orgName": "myorg",
"repoName": "world",
"operations": [
{ "operation": "retract", "name": "Location/cave", "reason": "superseded by Location/cave-v2" }
]
}
}
```
## Querying Things
```bash
# Snapshot of active things and assertions.
# System-managed component infrastructure records are hidden by default.
wh thing list
# Filter by shape — shows all things in that shape, including component-seeded ones
wh thing list --shape Location
# Hide all component-owned records (stricter than default)
wh thing list --exclude-components
# View a specific thing
wh thing view Location/cave
# Version history
wh thing history Location/cave --limit 10
# Filtered query
wh thing query --shape Location --limit 50
# Query only component-owned records
wh thing query --component com.acme.research --limit 50
```
## Reading Thing Data in TypeScript
`ThingDetail.data` is typed as `unknown` on the SDK so the compiler refuses
property access until the caller narrows the payload to its shape. There is no
`Thing` generic — narrow with a cast or a runtime parse before
reaching for fields:
```ts
import type { ThingDetail } from "@warmhub/sdk-ts";
type LocationData = { x: number; y: number; label?: string };
const detail = await client.thing.get("acme", "world", "Location/cave");
if (!detail) throw new Error("not found");
// Option 1 — trust the shape and cast (cheapest)
const data = detail.data as LocationData;
console.log(data.x, data.y);
// Option 2 — validate at the boundary (e.g. with zod)
// const data = LocationSchema.parse(detail.data);
// History items carry the same `data?: unknown` field on each version row.
const history = await client.thing.history("acme", "world", "Location/cave");
const v1 = history.versions[0];
const previous = v1?.data as LocationData | undefined;
```
If the shape is known statically, prefer the cast for ergonomics. If the
payload is untrusted or shape evolution is in play, parse it through a
schema (zod, valibot, ajv) so the runtime check stays close to the read.
## Kinds
Every entity in a repo is one of four kinds, set by the `kind` field on a write:
| Kind | Description |
|------|-------------|
| `shape` | Schema definition — fields and types |
| `thing` | Named entity with data |
| `assertion` | Claim about another thing (has `about` reference) |
| `collection` | Built-in grouping of things — a [Pair, Triple, Set, or List](/data-modeling/collections/) |
They share one lifecycle: each has a wref, is versioned, and is created or modified through [writes](/writes/overview/).
---
# Wrefs
> How to address any entity in WarmHub — wrefs are the references that fields like committer and about use to point at other entities.
A **wref** (WarmHub reference) is a human-readable address for any entity in WarmHub — shapes, things, and assertions.
Wrefs come in two name-based forms — **local** and **canonical** (below). A thing can also be addressed by its identity-based [durable id](#durable-ids), which names it regardless of its current name.
## Name-Based Forms
### Local Wrefs
Within the current repo, use the short form:
```
Location # shape
Location/cave # thing
Observation/cave-safe # assertion
GameState/round-1/state # thing with hierarchical name
```
The first segment is always the **shape name**. Everything after the first `/` is the **thing name**. Thing names can contain `/` for hierarchical organization — see [Naming as Navigation](/data-modeling/naming-as-navigation/) for how this hierarchy enables scoped queries, predictive navigation, and event routing.
### Canonical Wrefs
For cross-repo references, use the fully qualified form:
```
wh:org/repo/Location # shape in another repo
wh:org/repo/Location/cave # thing in another repo
```
Canonical wrefs always start with `wh:` followed by `org/repo/` and then the local wref.
#### Visibility gate on resolution
The canonical-wref **syntax** is universal — any client can construct one. **Resolving** one through a read surface requires effective `repo:read` permission on the target repo. Public repos are readable by anyone. For private repos, callers without that access see an error — except cross-repo search and batch lookup, which fold unreadable results into `{ items: [] }` or `missing[]` entries to keep search and batch streaming-friendly.
See [Getting Access — Repository Visibility](/auth/getting-access/#repository-visibility) for the full read-permission rules.
## Version Modifiers
Append `@` followed by a version modifier to target a specific version:
| Modifier | Meaning | Example |
|----------|---------|---------|
| `@HEAD` | Current (latest) version only | `Location/cave@HEAD` |
| `@vN` | Pinned to version N | `Location/cave@v3` |
| `@ALL` | All versions | `Location/cave@ALL` |
When no modifier is specified (a "bare" wref), the default depends on the operation:
- **Most reads** resolve bare wrefs to the **current version** — equivalent to `@HEAD`.
- **About queries** (SDK `thing.about()`, CLI `wh thing about` / `wh assertion list --about`, MCP `warmhub_thing_about`) resolve bare wrefs to **`@ALL`** — returning assertions across all versions of the target.
- **Writes** (commit operations) resolve bare wrefs to **`@HEAD`** — the operation targets the current version.
The `@ALL` modifier is rejected on write paths.
About queries match the supplied target identity by default. A bare wref broadens version matching for that target identity, but it does not automatically include assertions about Pair, Triple, Set, or List collection things that contain the target. Use `resolveCollections:true` (MCP/SDK/HTTP) or `--resolve-collections` (CLI) when collection-member assertions should be included for identity-scoped inputs. Current-state about filters keep pinned `@vN` inputs version-exact and do not expand collection members. History about filters are different: `thing history --about Shape/name@vN` resolves the target identity, so history can still match assertions across versions of that identity and, with collection resolution, assertions about collections containing it.
## Durable Ids
A wref is readable, but it is **mutable**: renaming a thing — or its org or repo — rewrites the wref. A consumer that stores wrefs as long-term keys sees a rename as a delete followed by a brand-new record.
A **durable id** avoids that. It is an opaque token that names a thing by its identity rather than its name, and it never changes — across renames, revisions, and retraction. Thing read results include one in their `metadata.durableId` field (see [Read Result Metadata](/sdk/read-semantics/#read-result-metadata)).
Durable ids have three useful properties:
- **Stable** — the same thing always has the same durable id, so it is safe to use as a long-lived key for mirroring, deduplication, or joins.
- **Self-routing** — a durable id carries its own location, so read surfaces can resolve it without being told its org or repo.
- **Self-verifying** — a corrupted or truncated durable id is rejected rather than resolving to the wrong thing.
A durable id is accepted anywhere a wref is accepted as input, and it carries the same version modifiers:
```
# current version
@v3 # pinned to version 3
@ALL # all versions (reads only)
```
As with any wref, `@ALL` is read-only and is rejected on write paths.
Resolving a durable id is subject to the same [visibility gate](#visibility-gate-on-resolution) as a canonical wref: you can only read ids for repos you have access to. See [Durable ids on `wh thing` reads](/cli-reference/commands/#durable-ids-on-thing-reads) for the CLI surface.
For the full picture of what a rename does to the references pointing at a thing — which follow it and which break — see [Retract, Rename & Schema Changes](/data-modeling/retract-rename-schema-changes/).
## Path Segment Rules
Wref path segments have the following constraints:
- Segments must be **non-empty** — no `//` allowed
- Names cannot **start or end** with `/`
- Segments cannot contain: `?`, `#`, `@`, `:`, `$`, or whitespace
- The `+` character is reserved for collection canonical names
## Batch Tokens
In commit operations, special tokens enable creating and referencing entities in a single commit:
### `$N` — Allocate
Used in the **last segment** of an ADD operation's name. Generates an opaque commit-scoped identifier:
```json
{ "operation": "add", "kind": "thing", "name": "Location/loc-$1", "data": { "x": 1 } }
```
This might create something like `Location/loc-a1b2c3d4e5f6a7b8`.
### `#N` — Reference
Used **anywhere** to reference the value allocated by the corresponding `$N`:
```json
[
{ "operation": "add", "kind": "thing", "name": "Location/loc-$1", "data": { "x": 1 } },
{ "operation": "add", "kind": "assertion", "name": "Observation/b-$2",
"about": "Location/loc-#1", "data": { "safe": true } }
]
```
The `#1` resolves to the same generated value as `$1`, so the assertion targets the thing created in the same commit.
### Token Rules
- `$N` can only appear in the **last segment** of an ADD name
- `#N` can appear anywhere — in names, about fields, and collection members
- Tokens are resolved **before** version pinning and wref resolution
- Generated values are opaque identifiers; callers should not infer ordering from them
- Each `$N` must have a unique N within the commit
- `#N` must reference a `$N` that appears earlier in the operations list
## Resolving Wrefs
To resolve a wref to its canonical identity:
```bash
wh thing resolve Location/cave
```
Via MCP:
```json
{ "name": "warmhub_wref_resolve", "arguments": { "wref": "Location/cave" } }
```
MCP `warmhub_wref_resolve` returns identifying fields only: `name`, `kind`, `active`, `version`, and `shapeName`. SDK `client.thing.resolve(...)` returns the full `thing.get` payload; `wh thing resolve` shows the same identifying fields by default and the full payload with `--json`. See [Wref Resolution](/queries/filtering/#wref-resolution).
## Examples
```bash
# Shape-only wref
Location
# Thing wref
Location/cave
# Pinned to version 3
Location/cave@v3
# Canonical (cross-repo)
wh:warmhub-data/congress-members/Legislator/p000197
# Hierarchical thing name
GameState/round-1/turn-5/state
# Collection canonical name
Pair/Location/Av1+Bv1
# With token allocation
Location/loc-$1
```
---
# Core Concepts
> The mental model behind WarmHub — orgs, repos, shapes, things, assertions, and writes
WarmHub organizes knowledge into a hierarchy of concepts. Understanding these building blocks is essential to working with the platform.
Before diving into the hierarchy, a few key terms used throughout WarmHub:
- **`wh`** — the WarmHub CLI command
- **wref** — a WarmHub reference, the human-readable address for any entity (e.g., `Location/cave`). See [Wrefs](/data-modeling/wrefs/) for the full reference.
## The Hierarchy
```
Organization
└── Repository
└── Things (all entities share one table)
├── kind: shape — schema definition
├── kind: thing — named entity with data
└── kind: assertion — claim about another thing
└── about → references any thing
```
All changes to entities happen through **writes** — add, revise, and retract operations that append version history.
## Organization
An **org** is the top-level namespace. It groups related repositories under a single identity.
```bash
wh org create acme --display-name "Acme Corp"
```
## Repository
A **repo** lives inside an org and contains all your data — shapes, things, assertions, and write history. It's the primary container for data isolation.
```bash
wh repo create acme/world -d "Game world knowledge base"
```
## Shape
A [**shape**](/data-modeling/shapes/) defines the data structure (schema) for things and assertions. Think of it as a type definition. Shapes specify what fields an entity's data can contain and what types those fields are.
```bash
wh shape create Location --fields '{"x": "number", "y": "number", "label": "string"}'
```
Field types include `string`, `number`, `boolean`, and `wref` (a reference to another thing). Shapes are versioned — you can revise a shape's fields and existing data remains tied to the version it was validated against.
## Thing
A [**thing**](/data-modeling/things/) is a named, versioned entity within a repo. Every thing belongs to a shape. Things are the core data objects — they represent the entities in your domain.
```bash
wh commit submit --add cave --shape Location --data '{"x": 3, "y": 7, "label": "Dark Cave"}'
```
Things are identified by their wref: `Shape/name`. For example, `Location/cave` refers to the thing named `cave` under the `Location` shape.
## Assertion
An [**assertion**](/data-modeling/assertions/) is a claim *about* another thing. It's also a thing itself (with its own shape, name, and versions), but it carries an additional `about` reference linking it to its subject.
```bash
wh assertion create --shape Observation --about Location/cave --data '{"confidence": 0.8, "source": "agent-1"}'
```
Key properties of assertions:
- The `about` target is [immutable](/data-modeling/assertions/#immutable-about) — set at creation and cannot be changed
- `about` can point to things in the same repo (local wrefs) or other repos (canonical wrefs)
- Multiple assertions can be about the same thing — this is how you model multiple perspectives or attributes
## Writes
A [**write**](/writes/overview/) is one or more operations submitted to WarmHub. All mutations use the same write pipeline. A request can contain multiple `add`, `revise`, and `retract` operations.
```bash
wh commit submit --ops '[
{"operation": "add", "kind": "thing", "name": "Location/cave", "data": {"x": 3, "y": 7}},
{"operation": "add", "kind": "assertion", "name": "Observation/cave-safe", "about": "Location/cave", "data": {"safe": true}}
]'
```
A successful write returns:
- **Per-operation status** — each add, revise, or retract reports success, noop, or failure
- **Version updates** — successful operations append version history for the affected thing
- **Timestamped history** — `thing history` shows when each version was created
## How They Fit Together
Here's a concrete example — modeling a game world where an agent explores and records what it discovers:
1. **Create shapes** to define your data types: `Location`, `Player`, `Observation`
2. **Add things** under those shapes: `Location/cave`, `Player/alice`
3. **Make assertions** about things: an `Observation` assertion about `Location/cave` recording that the cave is safe (with a confidence of 0.8)
4. **Write** related changes together — multiple operations can go in one request
5. **Query** the current state with `thing list`, or trace history with `thing history`
Every entity is versioned. Every write appends thing history. Every assertion knows its subject. Knowledge compounds — each interaction builds on everything that came before, and nothing is lost.
---
# Principles & Patterns
> Modeling guidance has moved to the Data Modeling section.
This content has been distributed into the [Data Modeling](/data-modeling/overview/) section where each topic lives alongside its reference material.
| Topic | New location |
|-------|-------------|
| Core principles (versioning, atomicity, immutability) | [Modeling Overview](/data-modeling/overview/) |
| When to use things vs assertions | [Things — When to Use Things](/data-modeling/things/#when-to-use-things) and [Assertions — When to Use Assertions](/data-modeling/assertions/#when-to-use-assertions) |
| Shape design | [Shapes — Designing Shapes](/data-modeling/shapes/#designing-shapes) |
| About reference design | [Assertions — Designing About References](/data-modeling/assertions/#designing-about-references) |
| Naming patterns | [Naming as Navigation](/data-modeling/naming-as-navigation/) |
| Common patterns (agent memory, multi-agent, evolving understanding) | [Patterns & Recipes](/data-modeling/patterns/) |
| Pitfalls | Distributed as callout boxes in [Assertions](/data-modeling/assertions/), [Shapes](/data-modeling/shapes/), and [Patterns](/data-modeling/patterns/) |
---
# Quickstart
> Connect to WarmHub via MCP, SDK, or CLI and run your first query — under 5 minutes.
From signup to your first query in under five minutes. [Authenticate once](#authentication), then pick the surface that fits your workflow — connect and run your first query.
- **[Connect via MCP](#connect-via-mcp)** — fastest path if you already use Claude Code, Cursor, or another MCP-compatible client. OAuth in most clients, no token mint required.
- **[Connect via SDK](#connect-via-sdk)** — TypeScript apps, custom agents, programmatic access.
- **[Connect via CLI](#connect-via-cli)** — terminal-first exploration and scripts.
The [first query](#your-first-query) section at the bottom shows how each surface reads from the public `warmhub-data/congress-trading` repo. MCP returns a natural-language summary; SDK and CLI fetch the raw records so your code or terminal can read them directly.
**Prerequisites:** A WarmHub account. See [Getting Access](/auth/getting-access/) if you don't have one.
## Authentication
WarmHub authenticates one of two ways, depending on the surface and environment:
- **OAuth** (interactive) — MCP clients like Claude Code and Cursor handle login on first use, and the `wh` CLI signs in with `wh auth login`. This is the default in Claude Code and Cursor.
- **Personal access token (PAT)** — used by the SDK and required in CI/CD, headless environments, WSL2, and any MCP client without OAuth.
To mint a PAT, install the [`wh` CLI](#install-the-cli), log in, and create one:
```bash
wh auth login
wh token create --name my-agent
```
The token is printed once. Copy it now and export it:
```bash
export WH_TOKEN=eyJhbGciOi...
```
See [Personal Access Tokens](/auth/personal-access-tokens/) for scopes and rotation. Each Connect-via section below opens with the auth it expects.
## Connect via MCP
Connect any [MCP](https://modelcontextprotocol.io/)-compatible client — Claude Code, Cursor, VS Code Copilot Chat, or anything else that speaks HTTP MCP.
**Auth:** OAuth by default in Claude Code and Cursor — nothing to set up. On WSL2 or in headless clients, use a [PAT](#authentication) via the [`mcp-remote` bridge](/auth/personal-access-tokens/#using-pats-with-mcp-clients).
### 1. Add the MCP server
Pick your client and drop the WarmHub server into its MCP config. The examples below use the global endpoint (`/mcp`); for a single-repo agent, swap in `/mcp//` instead — see [Global vs Repo-Scoped](/agent-integration/mcp-server/#global-vs-repo-scoped).
#### Claude Code
```bash
claude mcp add --scope user --transport http warmhub https://api.warmhub.ai/mcp
```
That registers WarmHub user-wide. Drop `--scope user` to register it for the current project only. Or commit a `.mcp.json` in the project root for teammates:
```json
{
"mcpServers": {
"warmhub": {
"transport": "http",
"url": "https://api.warmhub.ai/mcp"
}
}
}
```
Claude Code handles OAuth automatically on first call. For PAT auth, use the [`mcp-remote` bridge config](/auth/personal-access-tokens/#using-pats-with-mcp-clients).
#### Cursor
Add to `~/.cursor/mcp.json` (user-wide) or `.cursor/mcp.json` (project-scoped):
```json
{
"mcpServers": {
"warmhub": {
"transport": "http",
"url": "https://api.warmhub.ai/mcp"
}
}
}
```
Restart Cursor after editing. Cursor handles OAuth on first call.
#### VS Code (GitHub Copilot Chat)
Add to `.vscode/mcp.json` in your workspace:
```json
{
"servers": {
"warmhub": {
"type": "http",
"url": "https://api.warmhub.ai/mcp"
}
}
}
```
VS Code prompts for auth on first call. See [VS Code's MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for the latest schema.
#### Other clients
Any MCP client that supports HTTP transport works. Point it at `https://api.warmhub.ai/mcp` (global) or `https://api.warmhub.ai/mcp//` (repo-scoped). For PAT auth in clients that don't support OAuth, use the [`mcp-remote` stdio bridge](/auth/personal-access-tokens/#using-pats-with-mcp-clients).
### 2. Try it
Skip down to [Your first query](#your-first-query) for the prompt to try.
## Connect via SDK
Build TypeScript apps and agents with `@warmhub/sdk-ts`.
**Auth:** Pass an access token when you create the client — a [PAT](#authentication) is the simplest option. The example below supplies it from `WH_TOKEN` via `auth.getToken`.
### 1. Install the SDK and create a client
The SDK is published to the public npm registry. No registry config or extra token required:
```bash
npm install @warmhub/sdk-ts
```
Create a client:
```ts
import { WarmHubClient } from "@warmhub/sdk-ts";
const client = new WarmHubClient({
auth: { getToken: async () => process.env.WH_TOKEN },
});
```
### 2. Try it
Skip down to [Your first query](#your-first-query) for the call to make.
## Connect via CLI
Use the `wh` CLI for terminal exploration and scripting.
**Auth:** `wh auth login` (interactive) signs the CLI in directly — no PAT needed for everyday use. For CI or headless environments, use a [PAT](#authentication) instead.
### 1. Install and log in
The CLI saves your credentials and auto-refreshes them.
#### Install the CLI
The CLI is published to the public npm registry and requires **Node 22+**. No registry config or extra token required:
```bash
npm install -g @warmhub/cli
wh --version
```
#### Log in
```bash
wh auth login
wh auth status
```
`wh auth login` opens your browser — sign in with email, Google, or GitHub. For CI/CD or headless environments, see [Getting Access](/auth/getting-access/) for non-interactive options.
### 2. Set a target repo
Most `wh` commands operate on a specific org/repo. Set it once with `wh use` so you don't need `--repo` on every command:
```bash
wh use warmhub-data/congress-trading
```
This writes a `.wh` file in the current directory. You can also specify a repo per-command with `--repo`, or set the `WARMHUB_REPO` environment variable. Priority: `--repo` flag > `WARMHUB_REPO` env > `.wh` file.
### 3. Try it
Skip down to [Your first query](#your-first-query) for the command to run.
## Your first query
We'll use the public [`warmhub-data/congress-trading`](https://app.warmhub.ai/orgs/warmhub-data/repos/congress-trading) repo — it has [`StockTrade`](/data-modeling/shapes/) things tracking U.S. congressional stock trades disclosed under the STOCK Act. Let's pull a sample of disclosures.
### Via MCP
In your client, ask:
> Look at the `warmhub-data/congress-trading` data on WarmHub. Show me a sample of congressional stock trade disclosures.
The agent will call `warmhub_repo_describe` to learn the `StockTrade` [shape](/data-modeling/shapes/), then `warmhub_thing_query` to fetch `StockTrade` items and summarize the disclosures.
### Via SDK
```ts
const trades = await client.thing.head("warmhub-data", "congress-trading", {
shape: "StockTrade",
kind: "thing",
limit: 10,
});
console.log(trades.items);
```
Each `StockTrade` carries `legislator_name`, `ticker`, `amount` (a disclosure tier like `"$50,001 - $100,000"`), and `tx_type` (purchase/sale). See [Queries](/queries/overview/) for filter patterns.
#### Live updates
To watch new trades land in real time, wrap the same query with `client.live.thingHead`:
```ts
const handle = await client.live.thingHead(
"warmhub-data",
"congress-trading",
{ shape: "StockTrade", kind: "thing", limit: 20 },
(snapshot) => console.log(`${snapshot.items.length} trades at HEAD`),
);
// later, to stop the stream:
handle.close();
```
See [client.live](/sdk/client/#clientlive) for raw event streaming and other refreshed-query helpers.
### Via CLI
```bash
wh thing list --shape StockTrade --kind thing --limit 10 --repo warmhub-data/congress-trading
```
Copy any `StockTrade/...` reference from the output and view the full record. For example:
```bash
wh thing view StockTrade/2024/20025368/1 --repo warmhub-data/congress-trading
```
Add `--json` for machine-readable output, or `--live` to watch for real-time changes.
### Other public repos to explore
The [warmhub-data org](https://app.warmhub.ai/orgs/warmhub-data) hosts several public repos:
- `warmhub-data/congress-trading` — congressional stock trade disclosures filed under the STOCK Act, linked to legislator and security records in the companion repos below.
- `warmhub-data/congress-committees` — committees and member assignments.
- `warmhub-data/congress-members` — current and historical members of Congress.
- `warmhub-data/sec-equities` — NYSE and Nasdaq listings.
## Next Steps
- [Core Concepts](/get-started/core-concepts/) — the mental model behind orgs, repos, shapes, things, assertions, and writes.
- [Data Modeling](/data-modeling/wrefs/) — wrefs, shapes, things, and assertions in detail.
- [MCP Tool Walkthrough](/agent-integration/mcp-tool-walkthrough/) — full tool sequence (`warmhub_capabilities` → `warmhub_repo_describe` → reads → writes).
- [MCP Server](/agent-integration/mcp-server/) — endpoints, OAuth, and protocol details.
- [SDK Overview](/sdk/overview/) — client options, surfaces, and error kinds.
- [CLI Reference](/cli-reference/overview/) — full command reference for all `wh` commands.
---
# What is WarmHub?
> Knowledge platform for AI agents — versioned, structured, auditable
WarmHub is a **knowledge platform for AI agents** — purpose-built so that knowledge your agents gain persists, compounds, and stays yours. Think of it as what GitHub did for code, applied to knowledge: versioned, attributed, auditable, and shareable.
## Why WarmHub?
Traditional databases store rows. WarmHub stores **knowledge about the world** — versioned, attributed, and queryable. Every write creates version history. Every entity carries its full history. Every assertion knows what it's about.
What sets WarmHub apart is that **assertions carry confidence, evidence, and attribution** — so you can model not just what was recorded, but how strongly it's held and who holds it. Multiple agents can record different assertions about the same thing, and the system preserves all of them with full provenance. Knowledge compounds across sessions, agents, and teams — each agent builds on what previous agents discovered.
This makes WarmHub ideal for:
- **Knowledge that persists** — Agents write observations and decisions as structured assertions. Knowledge persists across sessions, so agents don't start from zero every time.
- **Multi-agent coordination** — Multiple agents working on the same problem write to a shared repo. Each version is attributed, so you can trace who said what and when.
- **Confidence and evidence** — Model confidence, evidence, and competing perspectives as first-class data. Assertions from different agents coexist — you can compare, reconcile, or let them evolve independently.
- **Auditable repos** — Every version of every entity is preserved. You can always ask "what did we know at time T?" or "who changed this and why?"
## Key Design Principles
**Everything is versioned.** Things, assertions, and shapes carry a version history, so you can trace how they changed over time.
**Names are stable, not permanent.** Names identify things and appear in [wrefs](/data-modeling/wrefs/) (references like `Shape/name`), but they can be changed. When a thing is renamed, identity-based references (such as assertion targets) follow it automatically, but a wref that uses the old name stops resolving. Things are linked by identity, not by name — the name is a human-readable label on top of that link.
**Agents are first-class.** WarmHub exposes an MCP server so AI agents can read and write data using the Model Context Protocol. The `wh prime` command gives agents a complete context dump in one call.
## How to Get Started
Start with [Core Concepts](/get-started/core-concepts/) to understand the mental model — orgs, repos, shapes, things, assertions, and writes. Then pick the interface that fits your use case:
| | SDK | CLI | MCP |
|--|-----|-----|-----|
| **Best for** | TypeScript apps, custom agents, programmatic access | Terminal exploration, shell scripts, quick operations | AI agents with MCP-compatible clients |
| **Type safety** | Full TypeScript types | JSON output via `--json` | Tool schemas |
| **Setup** | `npm install` + client constructor | `npm install -g @warmhub/cli` | Configure MCP endpoint |
| **Write pattern** | `client.commit.apply(...)` or `OperationBuilder` | `wh commit submit ...` | `warmhub_commit_submit` tool |
| **Read pattern** | `client.thing.head(...)` | `wh thing list` | `warmhub_thing_head` tool |
| **Real-time** | `client.live.subscribe(...)` | `wh thing list --live` | Claude Code only, via `wh channel` (research preview) |
| **Get started** | [SDK Quickstart](/get-started/quickstart/#connect-via-sdk) | [CLI Quickstart](/get-started/quickstart/#connect-via-cli) | [Connect MCP](/get-started/quickstart/#connect-via-mcp) |
All three interfaces share the same backend and concepts — [shapes](/data-modeling/shapes/), [things](/data-modeling/things/), [assertions](/data-modeling/assertions/), [writes](/writes/overview/), and [wrefs](/data-modeling/wrefs/).
---
# Actions
> Mounted HTTP endpoints for inspecting action runs, attempt history, notifications, and posting callback status updates.
This page covers the HTTP endpoints you can use to inspect subscription delivery runs, inspect attempt history, read repo-scoped action notifications, and post callback status updates from your webhook handler.
To create, update, pause, or remove subscriptions, use the [CLI and MCP workflows](/subscriptions/managing/) or the [SDK `client.subscription`](/sdk/client/#clientsubscription) surface. Subscription management REST endpoints under `/api/repos/:orgName/:repoName/subs` are not currently available.
## `GET /api/repos/:orgName/:repoName/actions/runs`
List action runs for a repository, optionally filtered by status.
**Auth:** Required — `repo:configure` scope. Anonymous callers, under-scoped tokens, and missing repositories all return an opaque `404` so existence is not disclosed; authenticate with `repo:configure` to see real responses.
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `subscriptionName` | string | No | Filter by subscription name |
| `status` | string | No | Filter by status |
| `since` | integer | No | Runs created after this epoch-milliseconds timestamp |
| `limit` | integer | No | Maximum runs to return. Capped at 200 — values above 200 are rejected with a validation error. |
### Response `200`
```json
[
{
"subscriptionName": "signal-hook",
"runId": "019d90f0-1111-7000-8000-000000000001",
"status": "succeeded",
"matchedOperationIndexes": [0, 1],
"attemptCount": 1,
"maxAttempts": 5,
"createdAt": 1741132800000,
"updatedAt": 1741132801000
}
]
```
Failed runs also include `lastErrorCode` and `lastErrorMessage` fields.
### Example
```bash
curl -H "Authorization: Bearer $WH_TOKEN" \
"https://api.warmhub.ai/api/repos/myorg/myrepo/actions/runs?status=failed_terminal"
```
## `GET /api/repos/:orgName/:repoName/actions/runs/:runId/attempts`
Get the attempt history for a specific action run.
**Auth:** Required — `repo:configure` scope. Anonymous callers, under-scoped tokens, and missing repositories all return an opaque `404` so existence is not disclosed. Authenticated callers with access receive a structured `404` when the requested run does not exist.
### Path Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `runId` | string | Action run identifier |
### Example
```bash
curl -H "Authorization: Bearer $WH_TOKEN" \
"https://api.warmhub.ai/api/repos/myorg/myrepo/actions/runs/019d90f0-1111-7000-8000-000000000001/attempts"
```
## `GET /api/repos/:orgName/:repoName/actions/notifications`
List repo-scoped action notification records.
**Auth:** Required — `repo:configure` scope. Anonymous callers, under-scoped tokens, and missing repositories all return an opaque `404` so existence is not disclosed; authenticate with `repo:configure` to see real responses.
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `since` | integer | No | Notifications after this epoch-milliseconds timestamp |
| `limit` | integer | No | Maximum notifications to return. Capped at 200 — values above 200 are rejected with a validation error. |
### Example
```bash
curl -H "Authorization: Bearer $WH_TOKEN" \
"https://api.warmhub.ai/api/repos/myorg/myrepo/actions/notifications?limit=20"
```
## `POST /api/action-runs/:runId/callback`
Report progress or completion for an asynchronous action run. This endpoint is not repo-prefixed.
In practice, `runId` comes from the original webhook payload, and most handlers can use the provided `callback_url` directly instead of constructing the path themselves. See [Webhook Payload](/subscriptions/creating/#webhook-payload).
**Auth:** Required, with write access to the run's repository.
### Body Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `status` | string | Yes | One of `processing`, `success`, `failure`, or `retry_requested` |
| `message` | string | No | Optional status detail or response snippet |
| `error` | string | No | Optional human-readable error message |
Callback statuses are input commands: `processing` maps to stored run status `processing`, `success` maps to `succeeded`, `failure` maps to `failed_terminal`, and `retry_requested` maps to `retry_wait`.
### Example
```bash
curl -X POST "https://api.warmhub.ai/api/action-runs/${RUN_ID}/callback" \
-H "Authorization: Bearer ${WH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"status": "failure",
"error": "Unexpected field '\''category'\'' on line 5."
}'
```
---
# Authentication
> Bearer token authentication for mounted HTTP endpoints.
WarmHub HTTP endpoints authenticate requests with a Bearer token in the `Authorization` header:
```bash
curl -H "Authorization: Bearer " \
https://api.warmhub.ai/api/repos/myorg/myrepo/head
```
WarmHub accepts interactive session JWTs and personal access tokens (PATs), subject to the user's organization and repository role.
## Personal Access Tokens
PAT management REST endpoints such as `POST /api/pats`, `GET /api/pats`, and `DELETE /api/pats/:name` are not currently mounted.
Create and manage PATs with the CLI (`wh token`) or the SDK (`client.token.*`) instead. Both paths accept an interactive user session — which manages all your tokens — or a PAT, which manages only the tokens it created (and tokens those created in turn). See [Personal Access Tokens](/auth/personal-access-tokens/) for the full guide.
## Scopes
PATs can carry resource-scoped permissions: `repo:read`, `repo:write`, `repo:configure`, `repo:admin`, `org:read`, `org:configure`, and `org:admin`. Scopes are independent — request the specific permissions your token needs. For what each scope grants, which role includes it, and the minimum scope per task, see the [access reference](/auth/access-reference/). JWT tokens do not use PAT scope narrowing, but are still limited by the user's role.
---
# Component Registry
> HTTP routes for resolving and setting up registered component installs.
These routes back the registered-component install flow used by `wh component install `. Registry management itself is not exposed as REST: create, list, view, update, and unregister use the SDK/tRPC `client.component.registry.*` surface.
All routes are mounted under:
```text
/api/component-registry/:orgName/:componentName
```
The mounted routes are `resolve` and `setup-call` — the two-step install handshake documented below. A third route, `cli/:method`, is the transport behind [`wh component exec`](/cli-reference/commands/#component--component-management); it dispatches an installed component's [CLI methods](/components/manifest-reference/#cli) rather than serving the install flow, so it is not documented as a standalone REST call here — see the component CLI reference for how those methods are defined and invoked.
The caller must have the `repo:write` scope on the install repo named in the request body. Private registered components are installable by members of the owner org into any repo they have write access to.
## POST `/api/component-registry/:orgName/:componentName/resolve`
Check whether a registered component can be installed into a repo, return the latest published manifest snapshot, and mint a fresh install id. Registered installs resolve the stored manifest snapshot directly through the registry.
### Request
```json
{
"installRepo": "acme/world"
}
```
### Response
```json
{
"manifest": { "component": { "id": "com.warmhub.Veritas", "name": "veritas", "version": "1.2.0" } },
"manifestHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b",
"hasSetup": true,
"installId": "019e..."
}
```
`manifest` is the full resolved manifest object; `manifestHash` identifies the published version and must be echoed back to `setup-call` as `expectedManifestHash`.
### Errors
- `400 VALIDATION_ERROR` — malformed `installRepo`
- `401 UNAUTHENTICATED`
- `403 FORBIDDEN` — caller lacks install-repo write access, or private registration is being installed into a foreign org
- `404 NOT_FOUND` — registration does not exist
- `409 CONFLICT` — registration has no published manifest version
## POST `/api/component-registry/:orgName/:componentName/setup-call`
Ask the backend to dispatch the optional setup callback for a registered component install.
### Request
```json
{
"installId": "019e...",
"installRepo": "acme/world",
"expectedManifestHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b"
}
```
All three fields are required. Pass the `installId` minted by `resolve` and the `manifestHash` it returned as `expectedManifestHash`. WarmHub uses that resolved manifest version to derive setup/runtime token scope. If the latest published manifest has moved since `resolve`, the call is rejected with `409 CONFLICT` — re-run `resolve` (reinstall) to pick up the new version.
### Response
```json
{
"ok": true,
"status": 202,
"warnings": []
}
```
The WarmHub HTTP response status matches the setup dispatch result: `200` when `ok` is `true`, otherwise the non-2xx `status` shown in the JSON body.
On non-2xx setup dispatch:
```json
{
"ok": false,
"status": 400,
"body": "Webhook target is not reachable or not allowed",
"warnings": []
}
```
Before outbound dispatch begins, the route can also return the standard WarmHub error envelope:
```json
{
"error": {
"code": "FORBIDDEN",
"message": "Missing required permissions"
}
}
```
Handle this envelope for validation, auth, missing setup URL, private-registration visibility, conflict, and manifest/token-preparation failures.
### Notes
- The backend reads `SETUP_*` keys from the registered credential set.
- When the registration has `mintedTokens: true`, the setup payload may include a minted setup token and a runtime token derived from the manifest's `runtimeAccess`. Re-running `setup-call` rotates these tokens against the resolved manifest version and revokes the prior ones.
- `setup-call` is invoked after the CLI has resolved the manifest and applied the local install.
- Custom HTTP clients should pass the `installId` and `manifestHash` from the preceding `resolve` response as `installId` and `expectedManifestHash`.
### Setup credential keys
The registered credential set can include these setup-auth keys:
| Key | Effect |
|-----|--------|
| `SETUP_BEARER_TOKEN` | Sends `Authorization: Bearer ` |
| `SETUP_API_KEY` | Sends an API key header |
| `SETUP_API_KEY_HEADER` | Header name for `SETUP_API_KEY`; defaults to `X-API-Key` |
| `SETUP_BASIC_USERNAME` + `SETUP_BASIC_PASSWORD` | Sends HTTP Basic auth when no bearer token is set |
| `SETUP_SIGNING_SECRET` | Sends `X-WarmHub-Signature` and `X-WarmHub-Timestamp`; signature is HMAC-SHA256 over `.` |
---
# Credentials
> Credential surfaces available over HTTP.
WarmHub does not currently mount REST endpoints for credential set CRUD under `/api/repos/:orgName/:repoName/credentials/sets`.
Use the [CLI](/cli-reference/commands/#credential--credential-sets), SDK, or MCP subscription tooling to create and manage credential sets. Bound credential sets are delivered to webhook subscriptions as HTTP auth headers — webhook consumers do not call a credential export endpoint.
---
# Endpoints
> The full map of mounted HTTP endpoints, their auth requirements, and the surfaces handled through the SDK, CLI, and MCP instead.
This page maps WarmHub's mounted REST endpoints for repository reads, action observability, component installs, and the MCP and streaming transports. Most rows link to the page that documents the endpoint in detail. A few mounted routes are owned by other pages — see [Documented elsewhere](#documented-elsewhere) below. For base URL, authentication, request and response format, idempotency, and pagination, see the [Overview](/http-api/overview/).
Endpoints marked **None** are publicly accessible for public repositories. Private repositories require a valid Bearer token.
## Repository Reads
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| `GET` | [`/api/repos/:org/:repo/head`](/http-api/queries/#get-head) | HEAD snapshot | None |
| `GET` | [`/api/repos/:org/:repo/about/:wref`](/http-api/queries/#get-about) | Assertions about a thing | None |
| `GET` | [`/api/repos/:org/:repo/query`](/http-api/queries/#get-query) | Filtered query | None |
## Action Observability
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| `GET` | [`/api/repos/:org/:repo/actions/runs`](/http-api/actions/#get-apireposorgnamereponameactionsruns) | List action runs | `repo:configure` |
| `GET` | [`/api/repos/:org/:repo/actions/runs/:runId/attempts`](/http-api/actions/#get-apireposorgnamereponameactionsrunsrunidattempts) | Get run attempts | `repo:configure` |
| `GET` | [`/api/repos/:org/:repo/actions/notifications`](/http-api/actions/#get-apireposorgnamereponameactionsnotifications) | List action notifications (terminal failures, plus successes when `notifyOnSuccess` is enabled) | `repo:configure` |
| `POST` | [`/api/action-runs/:runId/callback`](/http-api/actions/#post-apiaction-runsrunidcallback) | Report async action progress or completion | `repo:write` |
## MCP And Streaming
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| `POST` | [`/mcp`](/agent-integration/mcp-server/) | MCP HTTP transport (GET returns `405`) | Bearer token |
| `POST` | [`/mcp/:orgName/:repoName`](/agent-integration/mcp-server/) | Repo-scoped MCP HTTP transport (GET returns `405`) | Bearer token |
| `GET` | `/sse` | Server-sent invalidation stream with a live ticket | Live ticket |
## Component Registry
The first two routes are the install handshake. `cli/:method` is the transport behind [`wh component exec`](/cli-reference/commands/#component--component-management) — see the CLI reference for how to invoke component methods.
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| `POST` | [`/api/component-registry/:orgName/:componentName/resolve`](/http-api/component-registry/#post-apicomponent-registryorgnamecomponentnameresolve) | Resolve the latest manifest, check install eligibility, and mint an install id | `repo:write` on the install repo |
| `POST` | [`/api/component-registry/:orgName/:componentName/setup-call`](/http-api/component-registry/#post-apicomponent-registryorgnamecomponentnamesetup-call) | Dispatch the optional registered-component setup callback | `repo:write` on the install repo |
| `POST` | [`/api/component-registry/:orgName/:componentName/cli/:method`](/cli-reference/commands/#component--component-management) | Dispatch an installed component's CLI method (via `wh component exec`) | Owner-org member; `repo:read`, plus any per-method permission the method declares |
## Not Mounted As REST
These surfaces are intentionally documented through SDK, CLI, and MCP workflows rather than REST endpoint references:
| Surface | Use Instead |
|---------|-------------|
| Commit writes | [SDK commit APIs](/sdk/client/#clientcommit), [`wh commit submit`](/cli-reference/write-submit-deep-dive/), or [`warmhub_commit_submit`](/agent-integration/mcp-tools-reference/#warmhub_commit_submit) |
| Shape management | [SDK shape APIs](/sdk/client/#clientshape), [`wh shape`](/cli-reference/commands/#shape--shape-management), or commit writes |
| Organization and repository management | [CLI org/repo commands](/cli-reference/commands/) or [SDK org/repo APIs](/sdk/client/) |
| Subscription management | [Subscription CLI/MCP workflows](/subscriptions/managing/) |
| Credential set management | [Credential CLI workflows](/cli-reference/commands/#credential--credential-sets) |
| PAT management | [Personal Access Tokens guide](/auth/personal-access-tokens/) |
## Documented elsewhere
A few other mounted routes are documented on the pages that own those surfaces, so they are not repeated here:
| Routes | Documented on |
|--------|---------------|
| MCP OAuth metadata — `/.well-known/oauth-protected-resource`, `/.well-known/oauth-authorization-server` | [MCP Server](/agent-integration/mcp-server/) |
| Raw repository content — `/:org/:repo/readme.md`, `/:org/:repo/agents.md`, `/:org/:repo/llms.txt` | [Content shapes](/data-modeling/content-shape/) |
---
# Organizations & Repositories
> Manage organizations and repositories via the CLI and SDK.
Organization and repository management is available via the CLI and SDK. REST API endpoints for these resources are not currently mounted.
:::note[Organization management]
Organization creation, listing, and retrieval are available via the CLI and SDK:
- **CLI**: `wh org create`, `wh org view`, `wh org list` — see [CLI commands](/cli-reference/commands/#org--organization-management)
- **SDK**: `client.org.*` methods — see [SDK client](/sdk/client/#clientorg)
:::
:::note[Repository management]
Repository listing, creation, retrieval, and deletion are available via the CLI and SDK:
- **CLI**: `wh repo create`, `wh repo view`, `wh repo list`, `wh repo delete` — see [CLI commands](/cli-reference/commands/#repo--repository-management)
- **SDK**: `client.repo.*` methods — see [SDK client](/sdk/client/#clientrepo)
Soft-delete hides the repository immediately and permanently removes it after 30 days. Hard-delete removes it immediately and is irreversible.
```bash
# Soft-delete — hidden immediately, purged after 30 days
wh repo delete /
# Hard-delete — irreversible, immediate removal
wh repo delete