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 / --hard ``` ```ts await client.repo.delete(orgName, repoName) // soft, returns { graceExpiresAt: Date } await client.repo.hardDelete(orgName, repoName) // hard, returns void ``` ::: :::note[Membership management] Org membership is available via the CLI and SDK: - **CLI**: `wh org member add`, `wh org member set-role`, `wh org member list`, `wh org member remove` — see [CLI commands](/cli-reference/commands/#org--organization-management) - **SDK**: `client.org.addMember()`, `client.org.listMembers()`, `client.org.changeMemberRole()`, `client.org.removeMember()` — see [SDK client](/sdk/client/#clientorg) ::: --- # HTTP API Overview > Base URL, authentication, request and response format, idempotency, and OpenAPI posture for the WarmHub HTTP API. The WarmHub HTTP API exposes mounted JSON endpoints for repository reads, action observability, callbacks, MCP HTTP transport, and SSE. Repository data writes and management operations are not REST CRUD endpoints. Use the [SDK](/sdk/client/), [CLI](/cli-reference/commands/), or [MCP tools](/agent-integration/mcp-tools-reference/) for those workflows. WarmHub does not currently publish an OpenAPI specification. For exact request and response shapes, see the documentation in this section or the [SDK reference](/sdk-reference/classes/warmhubclient/). ## A minimal request The most common HTTP task is reading a repository's current state — its HEAD. This is a `GET`, so it is safe to retry; reads on a public repo also work without a token. ```bash curl -H "Authorization: Bearer " \ "https://api.warmhub.ai/api/repos/warmhub-data/congress-trading/head?shape=StockTrade&kind=thing&limit=1" ``` ```json { "items": [ { "wref": "StockTrade/20030868/0", "name": "20030868/0", "kind": "thing", "active": true, "shapeName": "StockTrade", "version": 3, "data": { "ticker": "ABBNY", "amount": "$1,001 - $15,000", "tx_type": "sale" } } ], "nextCursor": "whc1_eyJ2Ijox..." } ``` The rest of this page covers the base URL, auth, formats, and retries that apply across endpoints; the routing table points to each endpoint family. ## Base URL Most REST endpoints use the `/api` prefix: ``` https://api.warmhub.ai/api/ ``` Root-mounted transports use the origin without `/api`: ``` https://api.warmhub.ai/mcp https://api.warmhub.ai/sse ``` For example, the repository HEAD endpoint is: ``` https://api.warmhub.ai/api/repos/acme/world/head ``` ## Authentication Authenticated endpoints require an `Authorization` header with a Bearer token: ```bash curl -H "Authorization: Bearer " \ https://api.warmhub.ai/api/repos/acme/world/head ``` Repository data read endpoints (`/head`, `/about/:wref`, `/query`) on public repositories can be read anonymously; the same endpoints on private repos require authentication. Action-observability endpoints (`/api/repos/:org/:repo/actions/*`) and MCP `POST` endpoints (`POST /mcp`, `POST /mcp/:org/:repo`) always require authentication regardless of repo visibility — see the per-endpoint Auth column on the [Endpoints](/http-api/endpoints/) page. PAT management REST endpoints are not currently mounted. Create and manage PATs with the CLI (`wh token`) or the SDK (`client.token.*`); see [Authentication](/http-api/authentication/). ## Request Format - **Content-Type:** `application/json` for JSON request bodies - **Path parameters:** URL-encoded (for example, `Shape%2FMyShape` for `Shape/MyShape`) - **Query parameters:** Standard URL query string format ## Response Format Repository read and action observability endpoints return JSON on success. The `/sse` endpoint streams `text/event-stream`, and MCP endpoints follow JSON-RPC over HTTP. Most REST errors use this envelope: ```json { "error": { "code": "NOT_FOUND", "message": "Thing not found: Sensor/temp-1" } } ``` | HTTP Status | Common Error Codes | |-------------|--------------------| | `400` | `VALIDATION_ERROR`, `SHAPE_MISMATCH`, `RESERVED_NAME`, `ILLEGAL_OP_SEQUENCE` | | `401` | `UNAUTHENTICATED` | | `403` | `FORBIDDEN` | | `404` | `NOT_FOUND` | | `409` | `CONFLICT`, `ARCHIVED` | | `413` | `PAYLOAD_TOO_LARGE` | | `429` | `RATE_LIMITED` | | `500` | `INTERNAL_ERROR` | SDK callers receive these as `WarmHubError.code` / `kind`. See the [`ErrorKind` reference](/sdk-reference/type-aliases/errorkind/) for retryability and corrective actions per kind. Repository read endpoints (`/head`, `/about/:wref`, `/query`) collapse missing-repo and unauthorized-private-repo cases into the same `404` so repo existence stays opaque. Action observability endpoints also collapse anonymous, under-scoped, and missing-repo deny paths to opaque `404` responses. See [Queries](/http-api/queries/) and [Actions](/http-api/actions/) for the full deny-path behavior. Transport-specific endpoints can use their own error shape. For example, early `/sse` failures return a simple string-valued `error` object. When REST endpoints return `429 RATE_LIMITED`, they may include a `Retry-After` response header. Clients should back off until that interval has elapsed. A `413 PAYLOAD_TOO_LARGE` is returned when a request body exceeds the endpoint's size limit. The SDK's write helpers stay under the limit automatically, so you generally only see this on oversized requests built by hand. ## Idempotency and retries `GET` endpoints — repository reads and action observability — are idempotent. Retrying one returns the same result with no side effects, so they are always safe to retry after a network error. When a request returns `429 RATE_LIMITED`, the response may carry a `Retry-After` header. The same value is mirrored in the body as `error.retryAfter` (in seconds). Back off until that interval has elapsed before retrying. The action callback endpoint, `POST /api/action-runs/:runId/callback`, is safe to retry with the same `status`: re-reporting a status the run has already reached succeeds without repeating side effects such as notifications. Reporting a status that conflicts with the run's current state returns `409 CONFLICT`. A `retry_requested` status is the exception — it is a command, not a report, so each accepted one queues a new delivery attempt and it is rejected once the run's attempts are exhausted. WarmHub does not accept an inbound `Idempotency-Key` header. For action delivery, WarmHub sends your webhook handler an `X-WarmHub-Idempotency-Key` header so the handler can recognize and discard duplicate deliveries. ## Pagination Repository query endpoints support cursor-based pagination with `limit` and `cursor`. Action endpoints support `limit` and `since` filtering. | Parameter | Description | |-----------|-------------| | `limit` | Maximum number of items to return per page. | | `cursor` | Opaque pagination token from a previous response's `nextCursor` field. | | `since` | Filter action records after this epoch-milliseconds timestamp. | ## Endpoint reference | Need | Page | |------|------| | The complete map of mounted endpoints + per-endpoint auth | [Endpoints](/http-api/endpoints/) | | Authenticate requests and manage tokens | [Authentication](/http-api/authentication/) | | Read repository data (HEAD, about, query) | [Queries](/http-api/queries/) | | Action observability and the callback endpoint | [Actions](/http-api/actions/) | | Rate limits and backoff behavior | [Rate Limiting](/http-api/rate-limiting/) | [Endpoints](/http-api/endpoints/) is the complete map — including the surfaces handled through the SDK, CLI, and MCP rather than REST. --- # Queries > Read HEAD snapshots, about graphs, and filtered data. Query endpoints read data from a repository without modifying it. Public repositories can be queried without authentication. To keep repository existence opaque, every deny path on these endpoints — anonymous requests against private repos, authenticated callers without sufficient scope, and lookups against missing repos — collapses to the same `404` response with `Vary: Authorization`. Authenticate with a token that has access to the repo to see real responses. All paths are prefixed with `/api/repos/:orgName/:repoName`. The currently mounted repository read routes are: - `GET /head` - `GET /about/:wref` - `GET /query` --- ## Anonymous Pagination Caps Anonymous callers (no Bearer token) on the routes below are subject to narrowed paging to discourage bulk crawling of public repos: - `limit` is capped at **25** items per page. Requests with `limit > 25` return `400` with a `VALIDATION_ERROR`. When `limit` is omitted, anonymous callers get a 25-item page (authenticated callers continue to get the helper's 50-item default). - The page size is bound to the cursor. An anonymous follow-up that supplies `nextCursor` must either omit `limit` (and inherit the cursor's page size) or pass the same `limit` used for the previous page. Mixing a different `limit` with `nextCursor` returns `400` with a `VALIDATION_ERROR`. - Anonymous pagination stops after **2 pages**. Following `nextCursor` past the second page returns `404`. Authenticate to continue paging. Authenticated callers see no narrowing — they continue to use `limit` up to 500 and may follow `nextCursor` indefinitely. --- ## Filtered Read Freshness The endpoints on this page — `GET /head`, `GET /about/:wref`, and `GET /query` — all accept a `match` parameter for glob-filtered reads. A scoped Bearer token can also limit which wrefs a caller can see. Filtered reads may lag briefly after a write while WarmHub updates its read indexes. Subsequent reads after the indexes catch up will see the new state. Unfiltered reads made with an unscoped token reflect the latest written data immediately. --- ## `GET /head` Return the current HEAD snapshot — the latest version of every active thing in the repository. Filtered `/head` reads — either with an explicit `match` or on a scoped Bearer token — are subject to the freshness note above. ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `shape` | string | No | Filter by shape name | | `kind` | string | No | Filter by kind: `thing`, `assertion`, `shape`, `collection` | | `match` | string | No | Glob pattern to filter wrefs (`*` = one segment, `**` = zero or more) | | `limit` | integer | No | Maximum items per page (max 500). When omitted, WarmHub returns up to 50 items. Anonymous callers are capped — see [Anonymous Pagination Caps](#anonymous-pagination-caps). | | `cursor` | string | No | Opaque pagination cursor from a previous `nextCursor` response. Anonymous callers are capped at two pages — see [Anonymous Pagination Caps](#anonymous-pagination-caps). | ### Response `200` ```json { "items": [ { "wref": "StockTrade/20030868/0", "name": "20030868/0", "kind": "thing", "active": true, "shapeName": "StockTrade", "version": 3, "data": { "owner": "spouse", "ticker": "ABBNY", "amount": "$1,001 - $15,000", "tx_type": "sale", "trade_date": "2025-08-08", "legislator_name": "Adam Smith" } } ], "nextCursor": "whc1_eyJ2Ijox..." } ``` When there are more results, `nextCursor` contains an opaque token to pass as the `cursor` parameter on the next request. When all results have been returned, `nextCursor` is omitted. For assertions, the response includes an `aboutWref` field referencing the target thing. `aboutWref` is omitted when the assertion's target lives in a soft-deleted source repo — the canonical wref is suppressed rather than leaking the hidden repo's identity. ### Example ```bash # All things in the StockTrade shape curl "https://api.warmhub.ai/api/repos/warmhub-data/congress-trading/head?shape=StockTrade" # All shapes in the repository curl "https://api.warmhub.ai/api/repos/warmhub-data/congress-trading/head?kind=shape" # Paginate through results curl "https://api.warmhub.ai/api/repos/warmhub-data/congress-trading/head?shape=StockTrade&limit=20&cursor=whc1_eyJ2Ijox..." ``` ## `GET /about/:wref` Get a thing and all assertions about it. Filtered `/about` reads — either with an explicit `match` or on a scoped Bearer token — are subject to the [freshness note](#filtered-read-freshness) above. Unfiltered `/about` requests reflect the latest written state immediately. When the URL-encoded `wref` is canonical (`wh:org/repo/...`) and points at a private repo the caller cannot read, the route returns `404` — the response is intentionally indistinguishable from a missing target. The same gate applies to `/query` reads with an `about=wh:...` filter. See [Wrefs — Visibility gate on resolution](/data-modeling/wrefs/#visibility-gate-on-resolution). ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `wref` | string | URL-encoded wref of the target thing | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `shape` | string | No | Filter assertions by shape name | | `match` | string | No | Glob pattern to filter assertion wrefs (`*` = one segment, `**` = zero or more) | | `includeRetracted` | boolean | No | Resolve a retracted target and include retracted assertions (default `false`) | | `resolveCollections` | boolean | No | Include assertions about collections containing the target thing (default `false`) | | `depth` | integer | No | Nesting depth for recursive assertion lookup | | `limit` | integer | No | Maximum assertions to return (max 500). Anonymous callers are capped — see [Anonymous Pagination Caps](#anonymous-pagination-caps). | | `cursor` | string | No | Opaque pagination cursor from a previous `nextCursor` response. Anonymous callers are capped at two pages — see [Anonymous Pagination Caps](#anonymous-pagination-caps). | ### Response `200` The example below uses `IngestRecord` + `Assessment` because `/about` returns assertions about a thing, and that's the shape pair in `warmhub-data/congress-trading` that carries those relationships (a per-ingest data-quality check). ```json { "target": { "wref": "IngestRecord/qc-run/2026-05-15/r411b00", "name": "qc-run/2026-05-15/r411b00", "kind": "thing", "active": true, "shapeName": "IngestRecord", "version": 1 }, "assertions": [ { "wref": "Assessment/2026-04-10/ticker-extraction-rate", "name": "2026-04-10/ticker-extraction-rate", "kind": "assertion", "active": true, "shapeName": "Assessment", "version": 1, "data": { "passed": true, "check_name": "ticker-extraction-rate", "rationale": "89.5% of trades have a ticker symbol (6841/7644)", "confidence": 0.8 }, "aboutWref": "IngestRecord/qc-run/2026-05-15/r411b00@v1", "children": [] } ], "nextCursor": "whc1_eyJ2Ijox..." } ``` ### Errors | Code | Status | Description | |------|--------|-------------| | `NOT_FOUND` | 404 | Target thing not found, or the caller cannot access the repo | | `VALIDATION_ERROR` | 400 | Missing or invalid wref | ### Example ```bash # All assertions about a thing curl "https://api.warmhub.ai/api/repos/warmhub-data/congress-trading/about/IngestRecord%2Fqc-run%2F2026-05-15%2Fr411b00" # Only Assessment-shape assertions, including retracted curl "https://api.warmhub.ai/api/repos/warmhub-data/congress-trading/about/IngestRecord%2Fqc-run%2F2026-05-15%2Fr411b00?shape=Assessment&includeRetracted=true" ``` --- ## `GET /query` Query things by filters. Returns items matching the specified criteria. Filtered `/query` reads — either with an explicit `match` or on a scoped Bearer token — are subject to the [freshness note](#filtered-read-freshness) above. ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `shape` | string | No | Filter by shape name | | `about` | string | No | Filter assertions by target wref | | `kind` | string | No | Filter by kind: `thing`, `assertion`, `shape`, `collection` | | `match` | string | No | Glob pattern to filter wrefs (`*` = one segment, `**` = zero or more) | | `resolveCollections` | boolean | No | When `about` is set, also include assertions about collections containing the target (default `false`) | | `includeRetracted` | boolean | No | Include retracted items (default `false`) | | `limit` | integer | No | Maximum items to return (max 500). Anonymous callers are capped — see [Anonymous Pagination Caps](#anonymous-pagination-caps). | | `cursor` | string | No | Opaque pagination cursor from a previous `nextCursor` response. Anonymous callers are capped at two pages — see [Anonymous Pagination Caps](#anonymous-pagination-caps). | ### Response `200` ```json { "items": [ { "wref": "StockTrade/20030868/0", "name": "20030868/0", "kind": "thing", "active": true, "shapeName": "StockTrade", "version": 3, "data": { "ticker": "ABBNY", "amount": "$1,001 - $15,000", "tx_type": "sale" } } ], "nextCursor": "whc1_eyJ2Ijox..." } ``` ### Example ```bash # All things in the StockTrade shape curl "https://api.warmhub.ai/api/repos/warmhub-data/congress-trading/query?shape=StockTrade&kind=thing" # Assertions about a specific thing curl "https://api.warmhub.ai/api/repos/warmhub-data/congress-trading/query?about=IngestRecord%2Fqc-run%2F2026-05-15%2Fr411b00&kind=assertion" ``` --- # Rate Limiting > WarmHub's request rate limits and how to handle 429 responses. ## Anonymous requests Unauthenticated requests are capped per IP. The cap: | Rate | Burst | |------|-------| | 1000 / hour | 500 | Reads and writes both count. Authenticate to remove the per-IP cap; authenticated writes are governed by the limits below. ### Burst capacity The 500-token burst means you can fire 500 requests back-to-back when the bucket is full; after that you refill at ~17 requests per minute. - **Steady polling under ~16/minute** works forever. - **A burst of 500 requests** works once, then you wait ~30 minutes for the bucket to refill. - **Anonymous polling at 50/second** drains the burst in 10 seconds, then 429s until the bucket refills. - **Many users sharing one IP** (corporate networks, VPNs) — everyone shares the same budget. Authenticate to escape the shared cap. ### Anonymous 429 response When the budget is exhausted, the API returns `429 Too Many Requests` with a `Retry-After` header and this body: ```json { "error": { "code": "RATE_LIMITED", "message": "Anonymous request limit reached. Authenticate at https://docs.warmhub.ai/auth/getting-access for higher limits.", "hint": "Authenticate at https://docs.warmhub.ai/auth/getting-access for higher limits.", "retryAfter": 4 }, "scope": "ip", "authenticated": false } ``` - **`error.retryAfter`** matches the `Retry-After` header (seconds). After fully draining the bucket this is typically `3` to `4` seconds. - **`scope`** is always `"ip"` for anonymous 429s. - **`authenticated`** is always `false` — authenticated requests bypass this cap and never produce a 429 here. ### Getting higher limits Authenticate via the [getting-access flow](/auth/getting-access) and use a personal access token (PAT) or session token. If you have a legitimate scraping or monitoring use case that needs to remain anonymous and run above 1000/hour from a single IP, contact support — IP-level whitelisting is available case-by-case. ## Authenticated writes Some authenticated write operations are capped per user and per organization — the same limits apply whichever WarmHub client you use to issue the write. The table under [Rate-limited operations](#rate-limited-operations) below has the full list. Other authenticated writes and all authenticated reads aren't currently capped. ### How it works | Scope | Key | Description | |-------|-----|-------------| | **Per-user** | User ID | Protects against individual abuse. On the free tier, this applies to repo-scoped writes. On paid tiers, the org limit governs instead for shared repo write budgets such as commits. Standalone operations such as org creation and PAT creation, plus destructive repo delete operations, are always per-user regardless of tier. | | **Per-org** | Org ID | A shared budget across all org members. Higher tiers receive higher limits. | ### Rate-limited operations Only resource-creating and high-cost write operations are rate limited. Many operations on existing resources — such as pause, revoke, and rename — are not rate limited, because they are self-limiting: you can only act on what already exists. However, some write operations on existing resources are still rate limited. Revising or removing shapes, for example, draws from the same write permission budget as creation. Repository delete is also rate limited separately because it is irreversible and high impact. | Operation | Scope | Strategy | |-----------|-------|----------| | Writes to [things](/data-modeling/things) and [assertions](/data-modeling/assertions) — including commits and content updates (README, AGENTS, and generated content) | Per-user + per-org | Token bucket | | Shape creation | Per-org | Token bucket | | Shape revise / remove | Per-org | Token bucket | | Subscription creation | Per-org | Token bucket | | Credential set creation | Per-org | Fixed window | | Repository creation | Per-org | Fixed window | | Repository delete | Per-user | Fixed window | | Organization creation | Per-user | Fixed window | | PAT (Personal Access Token) creation | Per-user | Fixed window | ### Tier-based limits All organizations start on the **free tier**. Limits increase with higher tiers: | Tier | Commits/min | Repos/hr | Repo deletes/hr | Shapes/min | Subscriptions/min | Credentials/hr | Orgs/hr | PATs/hr | |------|-------------|----------|-----------------|------------|-------------------|----------------|---------|---------| | Free | 120 (user) / 600 (org) | 20 | 20 (user) | 40 | 20 | 20 | 10 | 20 | | Pro | 1,000 (org) | 50 | 20 (user) | 60 | 30 | 50 | 10 | 20 | | Enterprise | 5,000 (org) | 200 | 20 (user) | 200 | 100 | 200 | 10 | 20 | On paid tiers, shared repo write limits are generally enforced at the org level. Organization creation, PAT creation, and repository delete remain per-user on all tiers. ### Write-limit 429 response When a per-user or per-org limit is exceeded, the API returns HTTP `429` with a `RATE_LIMITED` error: ```http HTTP/1.1 429 Too Many Requests Retry-After: 12 Content-Type: application/json { "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded", "retryAfter": 12 } } ``` The `Retry-After` response header carries the number of seconds to wait before retrying. The same value is serialized into `error.retryAfter` so clients can read it without parsing headers. ## Retry strategy When you receive a 429: 1. **Read the `Retry-After` header** — wait at least that many seconds before retrying. 2. **Use exponential backoff** as a fallback if the header is missing. 3. **Don't retry immediately** — rapid retries consume tokens and extend the wait. ## What is not rate limited - **Authenticated read operations** — `head`, `query`, and `about` lookups; action-run reads. - **Simple operations on existing resources** — pause, resume, rename, revoke. --- # Shapes > Shape access from HTTP-oriented clients. WarmHub does not currently mount REST endpoints under `/api/repos/:orgName/:repoName/shapes`. Use these supported surfaces instead: | Task | Surface | |------|---------| | Read repository data filtered by shape | [`GET /api/repos/:orgName/:repoName/head?shape=...`](/http-api/queries/#get-head) or [`GET /api/repos/:orgName/:repoName/query?shape=...`](/http-api/queries/#get-query) | | Create, revise, or retract shapes | [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) | | Manage shapes with higher-level helpers | [SDK shape APIs](/sdk/client/#clientshape) or [`wh shape`](/cli-reference/commands/#shape--shape-management) | Shape writes are repository data writes. They flow through the same operation pipeline as thing and assertion writes, so they share the same validation, history, and per-operation result contract across CLI, SDK, and MCP. See [Atomicity](/writes/overview/#atomicity) for the full contract. --- # Writes > Write surfaces for HTTP-oriented clients. WarmHub does not currently mount a REST write endpoint. HTTP clients should use one of the supported write surfaces: | Surface | Method | |---------|--------| | SDK | [`client.commit.apply(...)`](/sdk-reference/classes/warmhubclient/#commit) | | CLI | [`wh commit submit`](/cli-reference/write-submit-deep-dive/) | | MCP | [`warmhub_commit_submit`](/agent-integration/mcp-tools-reference/#warmhub_commit_submit) | For operation fields and examples, see [Commit Operations](/cli-reference/write-submit-deep-dive/) and the SDK [`AddOperation` reference](/sdk-reference/interfaces/addoperation/). The three surfaces share one operation pipeline and return per-operation results. Domain failures are reported in `operations[]` with `status: "error"` and `partial: true`; request-level failures such as auth, malformed input, or infrastructure errors still return a top-level error. See [Atomicity](/writes/overview/#atomicity) for the full contract. Writes can be attributed to an installed component by passing `componentId` on the SDK or MCP call. See [Component Identity](/sdk/component-identity/) for the token rules, action-token behavior, and current SDK/CLI surface coverage. The REST HTTP API currently exposes repository read endpoints such as [`head`, `query`, and `about`](/http-api/queries/). Shape management is available through SDK, CLI, and commit-based workflows rather than REST shape endpoints. --- # Filtering and Lookup > Targeted queries — by wref, shape, about target, kind, version history, and text search. Beyond HEAD snapshots, WarmHub provides targeted query functions for specific lookups, filtered searches, history, and batch operations. ## Get by Wref Fetch a single thing by its exact wref. Use this when you know the reference and want its current state or a pinned version. ```bash wh thing view Location/cave wh thing view Location/cave --version 3 ``` ```json { "name": "warmhub_thing_get", "arguments": { "wref": "Location/cave", "version": 3 } } ``` The HTTP API does not currently mount a single-wref thing lookup route. Use the CLI, SDK, or MCP surfaces for direct wref lookups. Returns the thing's name, wref, shape, kind, version, active state, data, `committerWref`, and (for assertions) about reference. Within `data`, fields typed as `wref` rehydrate on read: same-repo refs return their local form (e.g. `Loc/cave@v1`); cross-repo refs return their canonical form (e.g. `wh:org/repo/Loc/cave@v1`). When a cross-repo `wref` points at a thing in a soft-deleted source repo, the field is returned as `null` — the hidden repo's identity is suppressed rather than leaked through the read surface. The same suppression applies to assertion `aboutWref` and other ref fields surfaced by `/head`, `/about`, and SDK reads. ## Query by Filters Query a repo by combinable filters such as shape, kind, `about` target, and wref glob. Use this when you don't have an exact wref. Filters are optional and they stack. ```bash # By shape wh thing query --shape Location # By kind wh thing query --kind assertion # By about target (assertions about a specific thing) wh thing query --about Location/cave # Combined filters wh thing query --shape Observation --about Location/cave --limit 20 # Resolve through collections — include assertions about collections containing the target wh thing query --about Location/cave --resolve-collections wh thing query --shape Observation --about Location/cave --resolve-collections # With glob pattern (match applies to full wrefs: Shape/name) wh thing query --shape Location --match "Location/dungeon/*" # Count matching items (no pagination, returns { count: N }) wh thing query --shape Location --count wh thing query --kind assertion --about Location/cave --count ``` ```json { "name": "warmhub_thing_query", "arguments": { "shape": "Observation", "about": "Location/cave", "kind": "assertion", "match": "Observation/dungeon/*", "includeRetracted": false, "limit": 50 } } ``` Count via MCP: ```json { "name": "warmhub_thing_query", "arguments": { "shape": "Observation", "about": "Location/cave", "count": true } } ``` The HTTP example below uses the public `warmhub-data/congress-trading` repo so you can copy-paste it directly; the CLI and MCP blocks above use generic `Location`/`Observation` names that you'd swap for your repo's own shapes. ``` GET /api/repos/warmhub-data/congress-trading/query?shape=StockTrade&kind=thing&match=StockTrade%2F2024%2F20025368%2F*&limit=25 ``` Anonymous callers reading public repos are capped at `limit=25` per page; authenticated callers can request up to `limit=500`. See [Anonymous Pagination Caps](/http-api/queries/#anonymous-pagination-caps). All filter parameters are optional. Combine them to narrow results. The HTTP API does not currently mount a count-only route; use CLI/MCP count surfaces for count-only reads. ## Field-Value Predicates Shape, kind, `about` target, and wref glob narrow by a record's identity. To filter on the **values of fields inside a thing's data** — `state`, `severity`, a nested `address.county` — add `where` predicates. `where` is supported across the typed read surfaces: the CLI (`wh thing query`, `wh thing list`), the SDK (`client.thing.query`, `thing.head`, `thing.about`, `thing.count`), and MCP (`warmhub_thing_query`, `warmhub_thing_head`, `warmhub_thing_about`). No HTTP read route parses `where`, and `thing.search` does not accept it — use `thing.query` for structured field filtering. ```bash # Equality wh thing query --shape Observation --where "status=active" # Numeric comparison (also >, <, <=, !=) wh thing query --shape Observation --where "severity>=3" # Prefix match on a string field (trailing * is optional) wh thing query --shape Location --where "region~north" # Set membership — value is one of the list wh thing query --shape Location --where "biome in:[forest,desert,tundra]" # Field existence wh thing query --shape Observation --where "resolvedAt?" # Multiple predicates are ANDed (up to 8 per query) wh thing query --shape Observation --where "status=active" --where "severity>=3" ``` Each `--where` flag is one predicate, written as a field path, an operator, and (except for `exists`) a value: | CLI form | Operator | Matches | |----------|----------|---------| | `field=value` | `eq` | equal | | `field!=value` | `ne` | not equal | | `field>value` / `field>=value` | `gt` / `gte` | greater than (or equal to) | | `field 1, each assertion may include a `children` array. ## Version History See every version of a thing, or filter history by shape or `about` target. Use this when you need how an item changed over time, not just its current state: ```bash wh thing history Location/cave wh thing history Location/cave --limit 10 ``` Query history by shape or about filters (without a specific wref): ```bash wh thing history --shape Observation --limit 20 wh thing history --about Location/cave --limit 5 # Include collection assertions in history filtering wh thing history --about Location/cave --resolve-collections ``` ```json { "name": "warmhub_thing_history", "arguments": { "wref": "Location/cave", "limit": 10 } } ``` At least one of `wref`, `shape`, or `about` is required. Each version entry includes: `version`, `operation` (`add`/`revise`/`retract`), `active`, `createdAt`, and `committerWref` — the committer's local or canonical wref when the originating commit recorded one, omitted otherwise. ## Cross-repo visibility Cross-repo wref lookups 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 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](/auth/getting-access/) for the precise rules. ## Wref Resolution Resolve a wref to its canonical identity. Use this when you have a local or version-relative reference and need its stable canonical form — CLI and SDK also return the resolved version's data: ```bash wh thing resolve Location/cave ``` ```json { "name": "warmhub_wref_resolve", "arguments": { "wref": "Location/cave" } } ``` - CLI `wh thing resolve` and SDK `client.thing.resolve(...)` return the same payload as `thing.get`. The default CLI render shows identity columns only; pass `--json` for the full payload. - MCP `warmhub_wref_resolve` returns only `name`, `kind`, `active`, `version`, and `shapeName`. Pair with `warmhub_thing_get` to fetch data. ## Batch Lookup Fetch up to 500 things by wref in a single call. On the CLI, `wh thing view` is variadic — pass multiple wrefs or use `--file` to trigger batch mode. Also available via the [TypeScript SDK](/sdk/client/#clientthing) (`client.thing.getMany`) and MCP — all three enforce the same 500-wref cap and version-qualify missing entries the same way. The CLI examples below use a couple of [wref conventions](/data-modeling/wrefs/): `Shape/name` reads HEAD, `Shape/name@v3` pins to version 3, and `--file=-` is the standard Unix marker for "read newline-delimited input from stdin." ```bash # CLI — wrefs come from positional args, --file , or piped stdin (any combination) wh thing view Location/cave Location/forest Player/alice cat wrefs.txt | wh thing view # one wref per line, piped stdin wh thing view --file wrefs.txt --version 1 # newline-delimited file, pin all to @v1 wh thing view --file=- < wrefs.txt --format jsonl # one JSON line per *deduped* requested wref ``` ```ts // SDK const result = await client.thing.getMany( 'acme', 'world', ['Location/cave', 'Location/forest', 'Player/alice'], 1, // optional fallback version for unpinned wrefs { includeRetracted: true }, // optional — return retract versions when reading a historical version (otherwise retract versions are excluded) ) // result: { requested, items, missing } ``` ```jsonc // MCP equivalent { "name": "warmhub_thing_get_many", "arguments": { "orgName": "acme", "repoName": "world", "wrefs": ["Location/cave", "Location/forest", "Player/alice"], "version": 1 } } ``` **Shared across CLI, SDK, and MCP:** - All three surfaces return `{ requested, items, missing }`: - `requested` is a number — the count of input wrefs the call processed. The CLI unions positional + `--file` + stdin inputs and dedupes before the round-trip; the SDK and MCP forward the `wrefs` array as-is, so duplicates count toward the 500-wref cap and produce duplicate `items`/`missing` entries. - `items[]` carries the resolved entities — same fields as a single-thing read. - `missing[]` is `string[]` — wrefs that don't exist or that the caller can't read, returned instead of throwing. - Missing entries are version-qualified when a top-level `version` / `--version` was supplied **and** the wref didn't already carry a version modifier (`@vN` or `@HEAD`). Per-wref pins always survive intact (no double-pinning). - All three enforce a 500-wref cap per call. **CLI-only:** - `--version` implies `--include-retracted`, so retract versions can be retrieved by their pinned id (mirrors `wh thing view --version`). Pass `--include-retracted` explicitly when you want retract versions without a fallback `--version`. - `--format jsonl` emits one row 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.) Use it for shell pipelines where you want to filter or fan out the result. Use `--json` instead when you want the full `{requested, items, missing}` envelope as a single object. Each row is `{requested, found, wref, ...}`. `requested` is the input wref preserved verbatim (so canonical/cross-repo inputs are still identifiable on the consumer side), `wref` is the local form for hits or the version-qualified form for misses, and `found` is the boolean. - `--live` is rejected (batch reads are one-shot — use `wh thing view --live` for per-thing polling). ## Search Search things by text content. Three modes are available: - **text** (default) — full-text search against thing names, shape names, and data fields. Supports pagination. - **vector** — semantic similarity search using embeddings. No pagination; does not support `--about` filter. - **hybrid** — runs text and vector searches in parallel, merges results with reciprocal rank fusion. No pagination. ```bash # Full-text search (default mode) wh thing search "safe location" --shape Observation # Semantic similarity search wh thing search "places that are dangerous" --mode vector # Hybrid search — combines text and vector results wh thing search "policy" --mode hybrid --limit 10 # Filter by about target (text mode only) wh thing search "safe" --about Location/cave # Resolve through collections (text mode only) wh thing search "safe" --about Location/cave --resolve-collections # Paginate through text results # NOTE: when using --about or --resolve-collections, pages may be sparse — # a page may return fewer items than --limit (or even zero) while nextCursor # is still present. Paginate until nextCursor is absent to collect all results. wh thing search "policy" --limit 50 --cursor # Fetch all pages automatically (text mode only) wh thing search "policy" --all ``` ```json { "name": "warmhub_thing_search", "arguments": { "query": "safe location", "shape": "Observation", "mode": "hybrid", "limit": 10 } } ``` Search is available via the CLI, SDK (`client.thing.search()`), and MCP (`warmhub_thing_search`) but is not currently exposed as an HTTP endpoint. ## Reactive Mode Most CLI queries support `--live` for real-time updates: ```bash wh thing query --shape Location --live wh thing history Location/cave --live wh thing about Location/cave --live ``` This opens a WebSocket connection and re-renders whenever the underlying data changes. --- # HEAD Queries > Get a fast snapshot of the current active state of a repository. A **HEAD query** returns all active items in a repository — things, assertions, shapes, and collections — at their current version. It's the fastest way to orient yourself in a repository. ## CLI The CLI names these snapshot reads `list` for consistency across domains; the command returns the repository HEAD projection. ```bash # All active items wh thing list # Filter by shape wh thing list --shape Location # Filter by kind (thing, assertion, shape, collection) wh thing list --kind assertion # Limit results wh thing list --limit 50 # Glob filter on wrefs (* = one segment, ** = zero or more) wh thing list --match "Location/*" # JSON output wh thing list --json ``` ### Counting Use `--count` to get just the number of matching items instead of the full result list: ```bash # Count all active items wh thing list --count # Count by shape wh thing list --shape Location --count # Count assertions wh assertion list --count wh assertion list --shape Observation --count ``` `--count` cannot be combined with `--limit`, `--cursor`, `--all`, or `--live`. For assertions specifically: ```bash # Equivalent to: wh thing list --kind assertion wh assertion list # With shape filter wh assertion list --shape Observation ``` ## MCP ```json { "name": "warmhub_thing_head", "arguments": { "shape": "Location", "kind": "thing", "match": "Location/dungeon/*", "limit": 100 } } ``` Count via MCP: ```json { "name": "warmhub_thing_head", "arguments": { "shape": "Location", "count": true } } ``` All parameters are optional. Omit them to get everything. ## SDK ```ts import type { Page, ThingItem } from "@warmhub/sdk-ts"; // Optionally filter by shape, kind, or a wref glob const page: Page = await client.thing.head("my-org", "my-repo", { shape: "Location", kind: "thing", match: "Location/dungeon/*", limit: 100, }); for (const item of page.items) console.log(item.wref); // Page through results with the returned cursor if (page.nextCursor) { // pass page.nextCursor back as `cursor` on the next call } ``` All filters are optional. A single call returns one `Page` — follow `nextCursor`, or use `client.thing.headAll` / `headIter`, to read past the first page. The example assumes a configured `client`; see the [SDK Quickstart](/get-started/quickstart/#connect-via-sdk) to create one, and [Read Semantics](/sdk/read-semantics/) for filters, glob match, and pagination. ## HTTP API The HTTP examples below use the public `warmhub-data/congress-trading` repo so you can copy-paste them directly; the CLI and MCP blocks above use generic `Location` names that you'd swap for your repo's own shapes. ``` GET /api/repos/warmhub-data/congress-trading/head GET /api/repos/warmhub-data/congress-trading/head?shape=StockTrade&kind=thing&limit=25 GET /api/repos/warmhub-data/congress-trading/head?match=StockTrade%2F2024%2F20025368%2F*&limit=25 ``` Anonymous callers reading public repos are capped at `limit=25` per page; authenticated callers can request up to `limit=500`. See [Anonymous Pagination Caps](/http-api/queries/#anonymous-pagination-caps). The HTTP API does not currently mount a count-only route. Use CLI/MCP count surfaces for count-only reads, or page through `/head` when using HTTP. ## Response The response contains an `items` array. Each item includes: | Field | Description | |-------|-------------| | `name` | Item name | | `wref` | Full wref (`Shape/name` for things, assertions, and collections; the shape name itself for shape rows) | | `shapeName` | Shape name (omitted for shape rows) | | `kind` | Entity kind (shape, thing, assertion, collection) | | `version` | Current version number | | `data` | Current data payload | | `aboutWref` | About target wref (assertions only) | ## Reactive Mode Watch HEAD in real-time: ```bash wh thing list --live ``` This opens a WebSocket connection and re-renders the display whenever data changes. On TTY terminals, the output refreshes in place. Combine with filters: ```bash wh thing list --live --shape Location --kind thing ``` ## When to Use HEAD HEAD queries are best for: - **Orientation** — understanding what's in a repo before targeted queries - **Monitoring** — watching for changes with `--live` - **Broad snapshots** — getting everything of a particular shape or kind For targeted lookups, prefer [filtering queries](/queries/filtering/) which accept more specific criteria. --- # Queries > Read data from WarmHub — HEAD snapshots, targeted lookups, search, and history. WarmHub reads come in a few shapes — a HEAD snapshot, a targeted get, a filtered query, search, and history. This page runs the most common one; the table routes you to the rest. ## Run a query Read the active things of a shape from any repo: ```bash wh thing query --repo warmhub-data/us.congress --shape Legislator ``` Add `--json` for structured output, `--count` for just the total, or `--about ` to find the assertions that target a thing. The full filter grammar, search modes, collection resolution, version history, and active-vs-retracted semantics live in [Filtering & Query Modes](/queries/filtering/). ## Query types | Query | Use case | CLI | MCP Tool | |-------|----------|-----|----------| | **HEAD** | Snapshot of all active entities | `wh thing list` | `warmhub_thing_head` | | **Count** | Count matching items without fetching data | `wh thing list --count` | `warmhub_thing_head` with `count: true` | | **Get** | Fetch a specific thing by wref | `wh thing view` | `warmhub_thing_get` | | **Query** | Filter by shape, kind, about | `wh thing query` | `warmhub_thing_query` | | **About** | Assertions whose about target resolves to a target identity | `wh thing about ` or `wh assertion list --about ` | `warmhub_thing_about` | | **History** | Version history of a thing | `wh thing history` | `warmhub_thing_history` | | **Resolve** | Resolve wref to identity | `wh thing resolve` | `warmhub_wref_resolve` | | **Search** | Find things by text content | `wh thing search` | `warmhub_thing_search` | | **Get Many** | Batch lookup by wrefs | `wh thing view [wrefs...]` | `warmhub_thing_get_many` | | **Refs** | Backlinks and cross-references | `wh thing refs` | `warmhub_thing_refs` | Use `wh thing about ` when starting from a thing and asking what assertions directly target it. `wh assertion list --about ` is the equivalent assertion-domain form when you are already browsing assertions. ## Access methods The same queries are available across three surfaces — pick what fits your workflow: - **CLI**: `wh [args] [--flags]` — human-readable output, or `--json` for structured data - **MCP**: `warmhub_*` tools — typed JSON-RPC tool calls - **HTTP API**: `GET /api/repos/:org/:repo/` — RESTful queries with query parameters ## Where to next | Need | Page | |------|------| | A point-in-time snapshot of all active entities | [Head](/queries/head/) | | Filter by shape / kind / about, search modes, version history, batch lookup, collection resolution, active-vs-retracted | [Filtering & Query Modes](/queries/filtering/) | --- # AllStreamOperationsFailedError Error raised when the stream append path completed deterministically but every submitted operation failed. ## Extends - `Error` ## Constructors ### Constructor > **new AllStreamOperationsFailedError**(`result`): `AllStreamOperationsFailedError` #### Parameters ##### result [`OperationSubmitResult`](/sdk-reference/type-aliases/operationsubmitresult/) #### Returns `AllStreamOperationsFailedError` #### Overrides `Error.constructor` ## Properties ### code > `readonly` **code**: `"STREAM_ALL_OPERATIONS_FAILED"` = `'STREAM_ALL_OPERATIONS_FAILED'` *** ### result > `readonly` **result**: [`OperationSubmitResult`](/sdk-reference/type-aliases/operationsubmitresult/) *** ### operations > `readonly` **operations**: `object`[] #### opIndex? > `optional` **opIndex?**: `number` #### name > **name**: `string` #### operation > **operation**: `"add"` \| `"revise"` \| `"retract"` \| `"noop"` #### dataHash > **dataHash**: `string` #### version > **version**: `number` #### status? > `optional` **status?**: `"noop"` \| `"applied"` \| `"error"` #### error? > `optional` **error?**: `object` ##### error.code > **code**: `string` ##### error.message > **message**: `string` ##### error.details? > `optional` **details?**: \{ `reason`: `"expected_version_mismatch"`; `expectedVersion`: `number`; `currentVersion`: `number`; \} \| \{ `reason`: `"lease_held"`; `leaseExpiresAt`: `string`; \} #### submittedName? > `optional` **submittedName?**: `string` #### resolvedName? > `optional` **resolvedName?**: `string` #### retryable? > `optional` **retryable?**: `boolean` #### warnings? > `optional` **warnings?**: `object` Non-blocking warnings collected during shape validation — see GH-1418. Aliased to the generated wire shape so this type cannot drift from the backend's `streamMutationResultEntrySchema.warnings`. ##### warnings.undeclaredFields > **undeclaredFields**: `string`[] ##### warnings.undeclaredFieldsTruncated? > `optional` **undeclaredFieldsTruncated?**: `true` ##### warnings.totalUndeclared? > `optional` **totalUndeclared?**: `number` *** ### statusCounts > `readonly` **statusCounts**: `object` #### applied > **applied**: `number` #### noop > **noop**: `number` #### error > **error**: `number` *** ### cause? > `readonly` `optional` **cause?**: `{ code: string; message: string; }` #### Overrides `Error.cause` --- # CliCallVerificationError ## Extends - `Error` ## Constructors ### Constructor > **new CliCallVerificationError**(`reason`, `message`): `CliCallVerificationError` #### Parameters ##### reason [`CliCallVerificationFailureReason`](/sdk-reference/type-aliases/clicallverificationfailurereason/) ##### message `string` #### Returns `CliCallVerificationError` #### Overrides `Error.constructor` ## Properties ### reason > `readonly` **reason**: [`CliCallVerificationFailureReason`](/sdk-reference/type-aliases/clicallverificationfailurereason/) --- # OperationBuilder Fluent builder for composing, validating, and submitting WarmHub commit operations. Use this when a caller needs to build a batch incrementally, run local preflight checks, optionally validate data against known shapes, and submit through the SDK stream-append path with continuation and retry options. The builder is not a promise — there is no `build()` method, and `await cb` alone will not submit anything. Finalize the batch by calling `await cb.commit({ client, orgName, repoName, ... })`, which validates, submits, and seals the builder in one step. Reusing a sealed builder throws. ## Example ```ts const cb = new OperationBuilder(); cb .add({ name: "Sensor/temp-1", data: { location: "A" } }) .revise({ name: "Sensor/temp-1", data: { location: "B" } }); const result = await cb.commit({ client, orgName: "acme", repoName: "world" }); ``` ## See https://docs.warmhub.ai/sdk/write-methods/ ## Constructors ### Constructor > **new OperationBuilder**(`options?`): `OperationBuilder` Create an empty operation builder. Pass known shape definitions when you want local data validation before submitting operations. #### Parameters ##### options? [`OperationBuilderOptions`](/sdk-reference/interfaces/operationbuilderoptions/) #### Returns `OperationBuilder` ## Accessors ### operations #### Get Signature > **get** **operations**(): readonly [`OperationBuilderOp`](/sdk-reference/type-aliases/operationbuilderop/)[] Return the queued operations. The returned array is read-only; use `add`, `revise`, and `retract` to modify the builder. ##### Returns readonly [`OperationBuilderOp`](/sdk-reference/type-aliases/operationbuilderop/)[] *** ### size #### Get Signature > **get** **size**(): `number` Return the number of queued operations. ##### Returns `number` ## Methods ### add() > **add**(`op`): `this` Add a shape, thing, assertion, or collection operation. For things and assertions, names use `Shape/localName`. Supplying `about` makes the operation an assertion. Supplying collection `type` and `members` makes it a collection. Otherwise the builder infers kind from the name. Set `kind: 'thing'` explicitly for hierarchical thing names that would otherwise look like assertion paths. #### Parameters ##### op `Omit | AddOp` #### Returns `this` *** ### revise() > **revise**(`op`): `this` Add a revise operation. The target can be supplied as `name` or `wref`. When shape definitions were provided to the constructor, revise data is validated locally before the operation is queued. #### Parameters ##### op `Omit | ReviseOp` #### Returns `this` *** ### retract() > **retract**(`target`): `this` Add a retract operation. Pass a wref string shorthand or an object with a target name, optional kind safety hint, and optional reason. #### Parameters ##### target `string` \| \{ `name`: `string`; `kind?`: `"shape"` \| `"thing"` \| `"assertion"` \| `"collection"`; `reason?`: `string`; `leaseId?`: `string`; \} #### Returns `this` *** ### has() > **has**(`name`): `boolean` Return whether this builder has queued an add operation for a name. #### Parameters ##### name `string` #### Returns `boolean` *** ### validate() > **validate**(): [`ValidationResult`](/sdk-reference/interfaces/validationresult/) Run client-side validation without contacting the server. Validation checks for empty commits, duplicate adds, missing revise targets, revise-after-retract patterns, local shape-data errors when shapes were provided, and the same preflight diagnostics used before commit submission. Returns a [ValidationResult](/sdk-reference/interfaces/validationresult/) object (`{ valid, errors, warnings }`) — not a boolean. Read `result.valid` to gate submission, and iterate `result.errors` for blocking diagnostics or `result.warnings` for informational ones. #### Returns [`ValidationResult`](/sdk-reference/interfaces/validationresult/) #### Example ```ts const cb = new OperationBuilder(); cb.add({ name: "Location/cave", data: { x: 0, y: 0 } }); const result = cb.validate(); if (!result.valid) { for (const err of result.errors) { console.error(`op ${err.operationIndex}: ${err.code} — ${err.message}`); } throw new Error("validation failed"); } for (const warn of result.warnings) console.warn(warn.message); ``` *** ### commit() > **commit**(`params`): `Promise`\<[`OperationSubmitResult`](/sdk-reference/type-aliases/operationsubmitresult/)\> Validate, submit, and seal the builder. A successful call submits operations through the same stream path as `client.commit.apply` and makes the builder single-use. Validation failures throw before any server request is made. Ambiguous multi-chunk failures surface as `PartialStreamSubmissionError`; deterministic all-failed batches surface as `AllStreamOperationsFailedError` with per-op failure data. #### Parameters ##### params ###### client `{ stream: { append(input: StreamAppendInput): Promise; }; }` WarmHub client or compatible stream client used for submission. ###### orgName `string` ###### repoName `string` ###### committer? `string` Optional wref identifying the actor on whose behalf the write is made. ###### message? `string` Optional commit message. ###### chunkSize? `number` Maximum operations per stream append. ###### streamId? `string` Advanced continuation hook for caller-managed streams. ###### retry? `false` \| [`RetryPolicyOptions`](/sdk-reference/type-aliases/retrypolicyoptions/) Retry policy for transient first-chunk failures, or `false` to disable automatic retry. #### Returns `Promise`\<[`OperationSubmitResult`](/sdk-reference/type-aliases/operationsubmitresult/)\> --- # PartialStreamSubmissionError Error raised when a streamed commit submission fails after an ambiguous or partial append. The `completedOperations` field contains operations the SDK knows completed. Before retrying manually, inspect repository state because the failed append may have committed additional operations that were not reflected locally. ## See https://docs.warmhub.ai/sdk/transient-retry/#partial-submissions ## Extends - `Error` ## Constructors ### Constructor > **new PartialStreamSubmissionError**(`input`): `PartialStreamSubmissionError` #### Parameters ##### input ###### cause `unknown` ###### completedOperations `object`[] #### Returns `PartialStreamSubmissionError` #### Overrides `Error.constructor` ## Properties ### code > `readonly` **code**: `"PARTIAL_STREAM_SUBMISSION"` = `'PARTIAL_STREAM_SUBMISSION'` Stable error code. Always `'PARTIAL_STREAM_SUBMISSION'`. *** ### completedOperations > `readonly` **completedOperations**: `object`[] Operations the SDK confirmed completed before the ambiguous or failing append. The remaining operations may or may not have landed; inspect repository state before retrying manually. #### opIndex? > `optional` **opIndex?**: `number` #### name > **name**: `string` #### operation > **operation**: `"add"` \| `"revise"` \| `"retract"` \| `"noop"` #### dataHash > **dataHash**: `string` #### version > **version**: `number` #### status? > `optional` **status?**: `"noop"` \| `"applied"` \| `"error"` #### error? > `optional` **error?**: `object` ##### error.code > **code**: `string` ##### error.message > **message**: `string` ##### error.details? > `optional` **details?**: \{ `reason`: `"expected_version_mismatch"`; `expectedVersion`: `number`; `currentVersion`: `number`; \} \| \{ `reason`: `"lease_held"`; `leaseExpiresAt`: `string`; \} #### submittedName? > `optional` **submittedName?**: `string` #### resolvedName? > `optional` **resolvedName?**: `string` #### retryable? > `optional` **retryable?**: `boolean` #### warnings? > `optional` **warnings?**: `object` Non-blocking warnings collected during shape validation — see GH-1418. Aliased to the generated wire shape so this type cannot drift from the backend's `streamMutationResultEntrySchema.warnings`. ##### warnings.undeclaredFields > **undeclaredFields**: `string`[] ##### warnings.undeclaredFieldsTruncated? > `optional` **undeclaredFieldsTruncated?**: `true` ##### warnings.totalUndeclared? > `optional` **totalUndeclared?**: `number` *** ### cause > `readonly` **cause**: `unknown` Underlying error that triggered the partial submission, preserved for diagnostics. Inspect via `isWarmHubError(err.cause)` / `err.cause.kind`. #### Overrides `Error.cause` --- # WarmHubClient ## Constructors ### Constructor > **new WarmHubClient**(`apiUrl?`, `options?`): `WarmHubClient` Create a WarmHub client. Pass either an options object or the legacy `(apiUrl, options)` form. The options object form is preferred for new code. #### Parameters ##### apiUrl? `string` ##### options? [`WarmHubClientOptions`](/sdk-reference/interfaces/warmhubclientoptions/) #### Returns `WarmHubClient` ### Constructor > **new WarmHubClient**(`options?`): `WarmHubClient` #### Parameters ##### options? [`WarmHubClientOptions`](/sdk-reference/interfaces/warmhubclientoptions/) #### Returns `WarmHubClient` ## Properties ### apiUrl > `readonly` **apiUrl**: `string` The resolved API base URL the client issues requests against. Defaults to [DEFAULT\_API\_URL](/sdk-reference/variables/default_api_url/) when no `apiUrl` is passed to the constructor. *** ### auth > `readonly` **auth**: `object` Authentication helpers for browser sign-in flows, session checks, and token diagnostics. #### getClientId > **getClientId**: () => `Promise`\<`string`\> Return the configured browser authentication client ID. Use this in browser sign-in flows that need to initialize the configured auth provider before redirecting or opening a login UI. ##### Returns `Promise`\<`string`\> #### sync > **sync**: () => `Promise`\<`{ success: boolean; }`\> Sync the authenticated identity with WarmHub. Call after a browser or server session is established so WarmHub can provision or refresh the corresponding user and personal organization records. ##### Returns `Promise`\<`{ success: boolean; }`\> #### currentUser > **currentUser**: () => `Promise`\<`CurrentUserInfo`\> Return the current authenticated WarmHub user. Throws when the request is unauthenticated or the token cannot be resolved. ##### Returns `Promise`\<`CurrentUserInfo`\> #### whoami > **whoami**: () => `Promise`\<`WhoamiInfo`\> Return authentication status, identity details, and token scope diagnostics for the current request. ##### Returns `Promise`\<`WhoamiInfo`\> *** ### diagnostics > `readonly` **diagnostics**: `object` Connectivity and compatibility helpers for checking the configured WarmHub backend. #### ping > **ping**: () => `Promise`\<`PingResult`\> Perform a health-check request against the configured backend URL. This uses the HTTP health endpoint rather than tRPC, so it is useful for distinguishing connection failures from procedure-level errors. ##### Returns `Promise`\<`PingResult`\> #### capabilities > **capabilities**: () => `Promise`\<`{ apiVersion: string; minSupportedSdk: string; features: Record; }`\> Return backend API version, minimum supported SDK version, and feature flags. ##### Returns `Promise`\<`{ apiVersion: string; minSupportedSdk: string; features: Record; }`\> #### assertCompatible > **assertCompatible**: () => `Promise`\<`void`\> Verify the installed SDK is at or above the backend's advertised minimum supported version, throwing a `WarmHubError` with a clear "upgrade to >=X" message when it is not. Call this once at startup (e.g. immediately after constructing the client) to fail fast on SDK version skew, rather than discovering a removed route deep in the commit pipeline as an opaque error. The result is cached on the client instance — repeated calls reuse the first network round-trip and re-throw the same error if too old. ##### Returns `Promise`\<`void`\> ##### See https://github.com/warmhub/warmhub-app/issues/3081 *** ### component > `readonly` **component**: `object` Installed component inspection for packages that add shapes, subscriptions, credentials, and seed data to a repository. The top-level methods cover per-repo installation queries, bundled system installs, and the registry-backed install pipeline used by `wh component install `. #### list > **list**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`{ items: Array<{ ref?: string; componentName: string; version?: string; state?: string; source?: string; sourceRef?: string; resolvedSha?: string; manifestHash?: string; installedAt?: string; checkedAt?: string; active: boolean; wref: string; }>; nextCursor?: string; }`\> List components installed in a repository. Pass pagination options when a repository may have many installed components. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? `ComponentListOptions` ##### Returns `Promise`\<`{ items: Array<{ ref?: string; componentName: string; version?: string; state?: string; source?: string; sourceRef?: string; resolvedSha?: string; manifestHash?: string; installedAt?: string; checkedAt?: string; active: boolean; wref: string; }>; nextCursor?: string; }`\> #### search > **search**: (`query`, `opts?`) => `Promise`\<`GlobalSearchResult`\> Search public registered components across all orgs (GH-4383). Public-only at launch. Distinct from listing components installed in a repo (`component.list`). ##### Parameters ###### query `string` ###### opts? [`GlobalSearchOptions`](/sdk-reference/type-aliases/globalsearchoptions/) ##### Returns `Promise`\<`GlobalSearchResult`\> #### get > **get**: (`orgName`, `repoName`, `componentRef`) => `Promise`\<`{ ref?: string; componentName: string; version?: string; state?: string; source?: string; sourceRef?: string; resolvedSha?: string; manifestHash?: string; installedAt?: string; checkedAt?: string; active: boolean; wref: string; install: { wref: string; pinnedWref?: string; name: string; kind: 'shape' | 'thing' | 'assertion' | 'collection'; shape?: string; shapeName?: string; validatedShape?: string; version: number; data?: unknown; active: boolean; synthesized?: boolean; aboutWref?: string; committerWref?: string; createdByWref?: string; metadata?: { durableId: string; thingCreatedAt: number; versionCreatedAt: number; }; }; ownedShapes: Array<{ name: string; kind: 'shape'; active: boolean; version: { version: number; operation: 'add' | 'revise' | 'retract'; data: unknown; dataHash: string; } | null; componentRef?: string; }>; ownedThings: Array<{ wref: string; name: string; kind: 'shape' | 'thing' | 'assertion' | 'collection'; shapeName?: string; componentRef?: string; version: number; createdAt: number; data?: unknown; active: boolean; aboutWref?: string; metadata?: { durableId: string; thingCreatedAt: number; versionCreatedAt: number; }; }>; }`\> Fetch one installed component by its `Org/Name` ref. ##### Parameters ###### orgName `string` ###### repoName `string` ###### componentRef `string` ##### Returns `Promise`\<`{ ref?: string; componentName: string; version?: string; state?: string; source?: string; sourceRef?: string; resolvedSha?: string; manifestHash?: string; installedAt?: string; checkedAt?: string; active: boolean; wref: string; install: { wref: string; pinnedWref?: string; name: string; kind: 'shape' | 'thing' | 'assertion' | 'collection'; shape?: string; shapeName?: string; validatedShape?: string; version: number; data?: unknown; active: boolean; synthesized?: boolean; aboutWref?: string; committerWref?: string; createdByWref?: string; metadata?: { durableId: string; thingCreatedAt: number; versionCreatedAt: number; }; }; ownedShapes: Array<{ name: string; kind: 'shape'; active: boolean; version: { version: number; operation: 'add' | 'revise' | 'retract'; data: unknown; dataHash: string; } | null; componentRef?: string; }>; ownedThings: Array<{ wref: string; name: string; kind: 'shape' | 'thing' | 'assertion' | 'collection'; shapeName?: string; componentRef?: string; version: number; createdAt: number; data?: unknown; active: boolean; aboutWref?: string; metadata?: { durableId: string; thingCreatedAt: number; versionCreatedAt: number; }; }>; }`\> #### registry > **registry**: `object` Registered-component identity and install-pipeline operations. This sub-surface drives the backend-mediated install flow that powers `wh component install `. It manages registered component identities (`register`, `unregister`, `list`, `view`, `update`) and the install pipeline (`resolve` returns the latest published manifest plus an install id, `setupCall` dispatches the optional setup callback). Use these when building a custom installer; most callers should run the CLI instead. ##### registry.register > **register**: (`orgName`, `componentName`, `input`) => `Promise`\<`{ ownerOrgName: string; componentName: string; ref: string; isPrivate: boolean; mintedTokens: boolean; sourceUrl?: string; sourceDefaultRef?: string; setupUrl?: string; uninstallUrl?: string; allowedCallbackDomains: string[]; credentialSetId?: string; credentialSetName?: string; description?: string; createdAt: number; updatedAt: number; }`\> ###### Parameters ###### orgName `string` ###### componentName `string` ###### input `unknown` ###### Returns `Promise`\<`{ ownerOrgName: string; componentName: string; ref: string; isPrivate: boolean; mintedTokens: boolean; sourceUrl?: string; sourceDefaultRef?: string; setupUrl?: string; uninstallUrl?: string; allowedCallbackDomains: string[]; credentialSetId?: string; credentialSetName?: string; description?: string; createdAt: number; updatedAt: number; }`\> ##### registry.unregister > **unregister**: (`orgName`, `componentName`) => `Promise`\<\{ `unregistered`: `true`; \}\> ###### Parameters ###### orgName `string` ###### componentName `string` ###### Returns `Promise`\<\{ `unregistered`: `true`; \}\> ##### registry.list > **list**: (`orgName`) => `Promise`\<`unknown`\> ###### Parameters ###### orgName `string` ###### Returns `Promise`\<`unknown`\> ##### registry.view > **view**: (`orgName`, `componentName`) => `Promise`\<`{ ownerOrgName: string; componentName: string; ref: string; isPrivate: boolean; mintedTokens: boolean; sourceUrl?: string; sourceDefaultRef?: string; setupUrl?: string; uninstallUrl?: string; allowedCallbackDomains: string[]; credentialSetId?: string; credentialSetName?: string; description?: string; createdAt: number; updatedAt: number; }`\> ###### Parameters ###### orgName `string` ###### componentName `string` ###### Returns `Promise`\<`{ ownerOrgName: string; componentName: string; ref: string; isPrivate: boolean; mintedTokens: boolean; sourceUrl?: string; sourceDefaultRef?: string; setupUrl?: string; uninstallUrl?: string; allowedCallbackDomains: string[]; credentialSetId?: string; credentialSetName?: string; description?: string; createdAt: number; updatedAt: number; }`\> ##### registry.update > **update**: (`orgName`, `componentName`, `input`) => `Promise`\<`{ ownerOrgName: string; componentName: string; ref: string; isPrivate: boolean; mintedTokens: boolean; sourceUrl?: string; sourceDefaultRef?: string; setupUrl?: string; uninstallUrl?: string; allowedCallbackDomains: string[]; credentialSetId?: string; credentialSetName?: string; description?: string; createdAt: number; updatedAt: number; }`\> ###### Parameters ###### orgName `string` ###### componentName `string` ###### input `{ isPrivate?: boolean; mintedTokens?: boolean; sourceUrl?: string; sourceDefaultRef?: string; setupUrl?: string; uninstallUrl?: string; allowedCallbackDomains?: string[]; credentialSetName?: string; description?: string; manifest?: Record; }` ###### Returns `Promise`\<`{ ownerOrgName: string; componentName: string; ref: string; isPrivate: boolean; mintedTokens: boolean; sourceUrl?: string; sourceDefaultRef?: string; setupUrl?: string; uninstallUrl?: string; allowedCallbackDomains: string[]; credentialSetId?: string; credentialSetName?: string; description?: string; createdAt: number; updatedAt: number; }`\> ##### registry.resolve > **resolve**: (`orgName`, `componentName`, `installRepo`) => `Promise`\<`{ manifest: Record; manifestHash: string; hasSetup: boolean; installId: string; }`\> ###### Parameters ###### orgName `string` ###### componentName `string` ###### installRepo `string` ###### Returns `Promise`\<`{ manifest: Record; manifestHash: string; hasSetup: boolean; installId: string; }`\> ##### registry.setupCall > **setupCall**: (`orgName`, `componentName`, `input`) => `Promise`\<`{ ok: boolean; status: number; body?: string; warnings: string[]; }`\> ###### Parameters ###### orgName `string` ###### componentName `string` ###### input `{ installId: string; installRepo: string; expectedManifestHash: string; }` ###### Returns `Promise`\<`{ ok: boolean; status: number; body?: string; warnings: string[]; }`\> #### cli > **cli**: `object` Operator-invoked CLI methods (GH-3193). `cli.call(orgName, componentName, method, { installRepo, args })` dispatches a component-declared method via the backend. The backend loads the install's manifest snapshot, verifies the method exists, authorizes the operator, signs the request with the install-repo credential set (HMAC over `${timestamp}.${body}` by default), and proxies the component service's JSON response back as the envelope's `body`. WarmHub-level failures (component not installed, method not in snapshot, `ComponentConfig.cliBaseUrl` missing, credentials missing, operator lacks `requiresPermission`) come back as a thrown `WarmHubError`. Upstream non-2xx responses do **not** throw — they arrive inside the envelope as `{ ok: false, status, body }` so the CLI can pretty-print the component's own error payload. ##### cli.call > **call**: (`orgName`, `componentName`, `method`, `input`) => `Promise`\<`{ ok: boolean; status: number; body?: string; warnings: string[]; }`\> ###### Parameters ###### orgName `string` ###### componentName `string` ###### method `string` ###### input `{ installRepo: string; args?: Record; }` ###### Returns `Promise`\<`{ ok: boolean; status: number; body?: string; warnings: string[]; }`\> *** ### commit > `readonly` **commit**: `object` High-level write surface for submitting WarmHub operations through the commit pipeline. #### apply > **apply**: (`orgName`, `repoName`, `message`, `operations`, `opts?`) => `Promise`\<[`OperationSubmitResult`](/sdk-reference/type-aliases/operationsubmitresult/)\> Submit one or more operations through WarmHub's commit pipeline. This is the primary write path for SDK callers. It streams operations to the backend, preserves server-side per-operation results, supports chunking for large submissions, and can attribute writes to a committer wref or installed component. Simple first-chunk transport failures are retried by default. Ambiguous failures surface as `PartialStreamSubmissionError`, including the case where an ambiguous attempt is followed by an all-failed retry; inspect `error.cause` for the underlying `AllStreamOperationsFailedError` or backend error. Deterministic all-failed submissions without prior ambiguity throw `AllStreamOperationsFailedError` directly so callers can inspect per-operation failure rows. Validation, auth, conflict, rate-limit, and other definite backend failures surface as `WarmHubError`. See [Transient Retry](/sdk/transient-retry/) for the full retry and partial-submission rules. Writing to an archived organization or repository fails with an `ARCHIVED` error before any operations are applied. ##### Parameters ###### orgName `string` ###### repoName `string` ###### message `string` \| `undefined` Optional commit message stored with the submitted operations. ###### operations [`Operation`](/sdk-reference/type-aliases/operation/)[] Add, revise, or retract operations to submit in order. ###### opts? ###### committer? `string` Optional wref identifying the actor on whose behalf the write is made. Must be a full wref string like `"Agent/bot-1"` (a thing that already exists), or a cross-repo wref like `"wh:other-org/other-repo/Agent/bot-1"`. Bare names such as `"eval-runner"` are rejected by the backend with a "Thing wref required" error — there is no implicit shape. ###### componentRef? `string` Attribute writes to an installed component, identified by its `Org/Name` ref, when the caller is allowed to claim it. ###### chunkSize? `number` Maximum operations per stream append. Values are clamped by the SDK. ###### skipExisting? `boolean` Return `noop` for add operations whose target already exists. ###### streamId? `string` Advanced continuation hook for caller-managed streams. ###### retry? `false` \| [`RetryPolicyOptions`](/sdk-reference/type-aliases/retrypolicyoptions/) Retry policy for transient first-chunk failures, or `false` to disable automatic retry. ##### Returns `Promise`\<[`OperationSubmitResult`](/sdk-reference/type-aliases/operationsubmitresult/)\> #### See https://docs.warmhub.ai/sdk/write-methods/ *** ### org > `readonly` **org**: `object` Organization management surface for namespaces, membership, roles, and scoped member permissions. #### get > **get**: (`orgName`) => `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> Get an organization by name. ##### Parameters ###### orgName `string` ##### Returns `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> #### getCallerRole > **getCallerRole**: (`orgName`) => `Promise`\<[`OrgRole`](/sdk-reference/type-aliases/orgrole/) \| `null`\> Return the caller's role in an organization, or `null` when the caller is not a member. Useful for UI gating before showing organization-level controls. ##### Parameters ###### orgName `string` ##### Returns `Promise`\<[`OrgRole`](/sdk-reference/type-aliases/orgrole/) \| `null`\> #### list > **list**: (`opts?`) => `Promise`\<[`Page`](/sdk-reference/type-aliases/page/)\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\>\> List organizations visible to the caller. Archived organizations are hidden unless `includeArchived` is set. ##### Parameters ###### opts? `OrgListOptions` ##### Returns `Promise`\<[`Page`](/sdk-reference/type-aliases/page/)\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\>\> #### create > **create**: (`name`, `displayName?`, `description?`) => `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> Create a new organization. The description is trimmed and empty strings are ignored. Organization names must avoid reserved public slugs such as `docs`, `api`, `login`, and `warmhub`. ##### Parameters ###### name `string` ###### displayName? `string` Optional display label. Defaults to the organization name when omitted. ###### description? `string` ##### Returns `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> #### setDescription > **setDescription**: (`orgName`, `description?`) => `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> Set or clear an organization description. Descriptions are trimmed; empty strings clear the stored value. ##### Parameters ###### orgName `string` ###### description? `string` ##### Returns `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> #### setDisplayName > **setDisplayName**: (`orgName`, `displayName`) => `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> Update an organization's display name. Display names are trimmed; empty or whitespace-only values are rejected. ##### Parameters ###### orgName `string` ###### displayName `string` ##### Returns `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> #### rename > **rename**: (`orgName`, `newName`) => `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> Rename an organization in place. The new slug must satisfy the same naming and reserved-name rules as organization creation. ##### Parameters ###### orgName `string` ###### newName `string` ##### Returns `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> #### update > **update**: (`input`) => `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> Atomic combined update of an organization's display name and/or slug. Both writes run in a single backend transaction so a slug conflict cannot leave a partial display-name change behind. Pass at least one of `displayName` or `newName`. ##### Parameters ###### input ###### orgName `string` ###### displayName? `string` ###### newName? `string` ##### Returns `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> #### addMember > **addMember**: (`orgName`, `email`, `role`) => `Promise`\<`{ email: string; firstName?: string; lastName?: string; role: 'owner' | 'admin' | 'editor' | 'viewer'; status: 'active' | 'pending'; invitedBy?: string; createdAt: number; }`\> Add a member to an organization or create a pending invite. The role defaults to `editor`. If the email address does not belong to an existing WarmHub user, WarmHub creates a pending invite and attempts to send the invite email asynchronously. Only owners can assign the `owner` role. ##### Parameters ###### orgName `string` ###### email `string` ###### role? `"owner"` \| `"admin"` \| `"editor"` \| `"viewer"` Organization role to assign. Defaults to `editor`. ##### Returns `Promise`\<`{ email: string; firstName?: string; lastName?: string; role: 'owner' | 'admin' | 'editor' | 'viewer'; status: 'active' | 'pending'; invitedBy?: string; createdAt: number; }`\> #### removeMember > **removeMember**: (`orgName`, `email`) => `Promise`\<`void`\> Remove an active member or revoke a pending invite by email address. ##### Parameters ###### orgName `string` ###### email `string` ##### Returns `Promise`\<`void`\> #### changeMemberRole > **changeMemberRole**: (`orgName`, `email`, `role`) => `Promise`\<`{ email: string; firstName?: string; lastName?: string; role: 'owner' | 'admin' | 'editor' | 'viewer'; status: 'active' | 'pending'; invitedBy?: string; createdAt: number; }`\> Change a member's organization role. Only owners can promote another member to owner or demote an existing owner. WarmHub rejects attempts to remove the final owner. ##### Parameters ###### orgName `string` ###### email `string` ###### role `"owner"` \| `"admin"` \| `"editor"` \| `"viewer"` ##### Returns `Promise`\<`{ email: string; firstName?: string; lastName?: string; role: 'owner' | 'admin' | 'editor' | 'viewer'; status: 'active' | 'pending'; invitedBy?: string; createdAt: number; }`\> #### setMemberScopes > **setMemberScopes**: (`orgName`, `email`, `scopes`) => `Promise`\<`void`\> Replace a member's scoped permission entries. Each entry targets either the organization (`acme`) or one repository (`acme/world`) and carries the full desired permission set for that resource. Matching entries replace the role-derived permission set for that resource; include every permission the member should retain. Member scope entries share the same wire shape as personal access token scopes, but `allowedMatches` is enforced for PATs only. Member scopes cannot restrict access by thing-name glob. ##### Parameters ###### orgName `string` ###### email `string` ###### scopes `WireScopeEntry`[] Scoped permission entries with `resource` and `permissions` fields. ##### Returns `Promise`\<`void`\> #### clearMemberScopes > **clearMemberScopes**: (`orgName`, `email`) => `Promise`\<`void`\> Remove all scoped permission entries from a member. After clearing, the member's effective access comes from their organization role only. ##### Parameters ###### orgName `string` ###### email `string` ##### Returns `Promise`\<`void`\> #### listMembers > **listMembers**: (`orgName`, `opts?`) => `Promise`\<`OrgMemberList`\> List organization members and pending invites. The response includes the caller's current organization role so frontend settings pages can gate owner/admin-only controls without making a second request. ##### Parameters ###### orgName `string` ###### opts? `OrgListMembersOptions` ##### Returns `Promise`\<`OrgMemberList`\> #### archive > **archive**: (`orgName`) => `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> Archive an organization, blocking new repositories and membership changes. ##### Parameters ###### orgName `string` ##### Returns `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> #### unarchive > **unarchive**: (`orgName`) => `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> Unarchive an organization. ##### Parameters ###### orgName `string` ##### Returns `Promise`\<`{ name: string; displayName: string; description?: string; tier: 'free' | 'pro' | 'enterprise'; archivedAt?: number; createdAt: number; repoCount?: number; errorCount?: number; lastActivityAt?: number; }`\> *** ### repo > `readonly` **repo**: `object` Repository management surface for lifecycle operations, metadata, statistics, and content documents. #### get > **get**: (`orgName`, `repoName`) => `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> Get a repository by organization and repository name. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> #### getStats > **getStats**: (`orgName`, `repoName`) => `Promise`\<`{ total: number; byKind: { shape: number; thing: number; assertion: number; }; byShape: Record; }`\> Return authoritative active-item totals for a single repository. The returned `total` is the sum of active shapes, things, and assertions. Use this when billing, quota checks, health reports, or per-shape breakdowns need single-repo stats. The per-shape breakdown counts active things and assertions by shape. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<`{ total: number; byKind: { shape: number; thing: number; assertion: number; }; byShape: Record; }`\> #### getStatsBatch > **getStatsBatch**: (`orgName`, `repoNames`) => `Promise`\<`RepoStatsBatchResult`\> Return active-item totals for up to 100 repositories in one request. Use this instead of issuing one `getStats` request per repository when building organization dashboards. Batch entries include the exact `total` for visible repositories; call `getStats` for an individual repository when you need the per-shape map. ##### Parameters ###### orgName `string` ###### repoNames `string`[] ##### Returns `Promise`\<`RepoStatsBatchResult`\> #### getConfigureStats > **getConfigureStats**: (`orgName`, `repoName`) => `Promise`\<`{ subscriptionCount: number; }`\> Return configuration-surface counts for a repository. Currently this reports the number of subscriptions attached to the repository, which is useful before delete or visibility-change flows. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<`{ subscriptionCount: number; }`\> #### getShapeInstanceCounts > **getShapeInstanceCounts**: (`orgName`, `repoName`) => `Promise`\<`Record`\> Return per-shape thing and assertion counts for a repository. The server computes the totals directly, so callers do not need to page through repository contents to build shape summary UI. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<`Record`\> #### list > **list**: (`orgName`, `opts?`) => `Promise`\<`{ items: Array<{ orgName: string; name: string; displayName: string; description?: string; visibility: 'public' | 'private'; archivedAt?: number; createdAt: number; }>; nextCursor?: string; }`\> List repositories in an organization. Archived repositories are hidden by default. Search and sort options are applied before pagination. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### opts? `RepoListOptions` ##### Returns `Promise`\<`{ items: Array<{ orgName: string; name: string; displayName: string; description?: string; visibility: 'public' | 'private'; archivedAt?: number; createdAt: number; }>; nextCursor?: string; }`\> #### search > **search**: (`query`, `opts?`) => `Promise`\<`GlobalSearchResult`\> Search public repos across all orgs (GH-4383). Public-only at launch; BM25 over name/description/shape vocabulary/README. ##### Parameters ###### query `string` ###### opts? [`GlobalSearchOptions`](/sdk-reference/type-aliases/globalsearchoptions/) ##### Returns `Promise`\<`GlobalSearchResult`\> #### create > **create**: (`orgName`, `repoName`, `description?`, `visibility?`, `displayName?`) => `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> Create a repository inside an organization. Repositories are private by default. Descriptions are trimmed and capped by the backend. ##### Parameters ###### orgName `string` ###### repoName `string` ###### description? `string` ###### visibility? `'public' | 'private'` `public` or `private`; defaults to `private`. ###### displayName? `string` ##### Returns `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> #### setDescription > **setDescription**: (`orgName`, `repoName`, `description?`) => `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> Set or clear a repository description. Descriptions are trimmed; empty strings clear the stored value. ##### Parameters ###### orgName `string` ###### repoName `string` ###### description? `string` ##### Returns `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> #### setDisplayName > **setDisplayName**: (`orgName`, `repoName`, `displayName`) => `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> Set a repository display name. `displayName` is required and non-empty (trimmed); slug fallback is a creation-time behavior only and there is no clear-to-slug flow. ##### Parameters ###### orgName `string` ###### repoName `string` ###### displayName `string` ##### Returns `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> #### setVisibility > **setVisibility**: (`orgName`, `repoName`, `visibility`) => `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> Set a repository's visibility to `public` or `private`. ##### Parameters ###### orgName `string` ###### repoName `string` ###### visibility `'public' | 'private'` ##### Returns `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> #### rename > **rename**: (`orgName`, `repoName`, `newName`) => `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> Rename a repository within its organization. The new name must be unused in the organization and follow the same path-segment rules as repository creation. ##### Parameters ###### orgName `string` ###### repoName `string` ###### newName `string` ##### Returns `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> #### update > **update**: (`input`) => `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> Atomic combined update of a repository's display name and/or slug. Both writes run in a single backend transaction so a slug conflict cannot leave a partial display-name change behind. Pass at least one of `displayName` or `newName`. `displayName` is rejected when empty — slug fallback is a creation-time behavior only. ##### Parameters ###### input ###### orgName `string` ###### repoName `string` ###### displayName? `string` ###### newName? `string` ##### Returns `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> #### archive > **archive**: (`orgName`, `repoName`) => `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> Archive a repository, blocking new writes. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> #### unarchive > **unarchive**: (`orgName`, `repoName`) => `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> Unarchive a repository. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<[`RepoInfo`](/sdk-reference/type-aliases/repoinfo/)\> #### delete > **delete**: (`orgName`, `repoName`) => `Promise`\<\{ `graceExpiresAt`: `Date`; \}\> Soft-delete a repository. The repository is hidden immediately and scheduled for permanent purge after a 30-day grace window. WarmHub blocks deletion when another repository still has inbound cross-repo references, active subscriptions, or active credential grants that depend on the repository. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<\{ `graceExpiresAt`: `Date`; \}\> #### hardDelete > **hardDelete**: (`orgName`, `repoName`) => `Promise`\<`void`\> Immediately and irreversibly purge a repository. This owner-only operation has no grace window and uses the same inbound-reference guard as `delete`. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<`void`\> #### listPage > **listPage**: (`orgName`, `opts?`) => `Promise`\<`RepoListPageResult`\> List repositories with dashboard-oriented per-repository metadata. Each item includes exact active counts, an activity-oriented `lastWriteAt`, and a `hasErrors` flag for terminal action failures. Search and sort are applied before pagination, so cursors remain stable across the filtered and ordered list. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### opts? `RepoListPageOptions` ##### Returns `Promise`\<`RepoListPageResult`\> #### listForCaller > **listForCaller**: (`opts?`) => `Promise`\<`{ orgName: string; name: string; displayName: string; createdAt: number; lastWriteAt?: number; }`[]\> List the caller's repositories across every org they belong to, ordered for recency by default (most recently written first), capped at `limit`. Unlike `listPage`, this is user-level and resolves the caller's orgs server-side, so you don't fan out one request per org to build an account-wide view. Membership is the access filter and per-token `allowedMatches` narrowing is not applied, so this requires an interactive session — PAT and component-token callers are rejected. Use the org-scoped `listPage` from token-authenticated contexts. ##### Parameters ###### opts? `RepoListForCallerOptions` ##### Returns `Promise`\<`{ orgName: string; name: string; displayName: string; createdAt: number; lastWriteAt?: number; }`[]\> #### getReadme > **getReadme**: (`orgName`, `repoName`) => `Promise`\<[`ThingDetail`](/sdk-reference/type-aliases/thingdetail/) \| `null`\> Fetch a repository's `Content/Readme` markdown record. The SDK contract allows `null`; current backend behavior returns a synthesized empty stub for repositories that have not committed README content yet. Callers should still null-check defensively. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<[`ThingDetail`](/sdk-reference/type-aliases/thingdetail/) \| `null`\> #### getAgents > **getAgents**: (`orgName`, `repoName`) => `Promise`\<[`ThingDetail`](/sdk-reference/type-aliases/thingdetail/) \| `null`\> Fetch a repository's `Content/Agents` markdown record. The SDK contract allows `null`; current backend behavior mirrors `getReadme` and returns a synthesized empty stub when no AGENTS.md content has been committed yet. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<[`ThingDetail`](/sdk-reference/type-aliases/thingdetail/) \| `null`\> #### setReadme > **setReadme**: (`orgName`, `repoName`, `content`) => `Promise`\<`{ committer?: string; createdByEmail?: string; message?: string; operationCount: number; operations: Array<{ opIndex?: number; name: string; operation: 'add' | 'revise' | 'retract' | 'noop'; version: number; dataHash: string; status?: 'applied' | 'noop'; warnings?: { undeclaredFields: Array; undeclaredFieldsTruncated?: true; totalUndeclared?: number; }; }>; }`\> Commit a new `Content/Readme` value. The backend adds or revises the content record through the normal commit pipeline. ##### Parameters ###### orgName `string` ###### repoName `string` ###### content `string` ##### Returns `Promise`\<`{ committer?: string; createdByEmail?: string; message?: string; operationCount: number; operations: Array<{ opIndex?: number; name: string; operation: 'add' | 'revise' | 'retract' | 'noop'; version: number; dataHash: string; status?: 'applied' | 'noop'; warnings?: { undeclaredFields: Array; undeclaredFieldsTruncated?: true; totalUndeclared?: number; }; }>; }`\> #### setAgents > **setAgents**: (`orgName`, `repoName`, `content`) => `Promise`\<`{ committer?: string; createdByEmail?: string; message?: string; operationCount: number; operations: Array<{ opIndex?: number; name: string; operation: 'add' | 'revise' | 'retract' | 'noop'; version: number; dataHash: string; status?: 'applied' | 'noop'; warnings?: { undeclaredFields: Array; undeclaredFieldsTruncated?: true; totalUndeclared?: number; }; }>; }`\> Commit a new `Content/Agents` value through the normal commit pipeline. ##### Parameters ###### orgName `string` ###### repoName `string` ###### content `string` ##### Returns `Promise`\<`{ committer?: string; createdByEmail?: string; message?: string; operationCount: number; operations: Array<{ opIndex?: number; name: string; operation: 'add' | 'revise' | 'retract' | 'noop'; version: number; dataHash: string; status?: 'applied' | 'noop'; warnings?: { undeclaredFields: Array; undeclaredFieldsTruncated?: true; totalUndeclared?: number; }; }>; }`\> #### getLlmsTxt > **getLlmsTxt**: (`orgName`, `repoName`) => `Promise`\<`SynthesizedRepoContent`\> Fetch the synthesized `Content/LlmsTxt` sitemap for a repository. The returned markdown follows the llms.txt convention. Authenticated callers also receive structured reference metadata partitioned by readable outbound and inbound references; cross-org references the caller cannot read are omitted. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<`SynthesizedRepoContent`\> #### index > **index**: `object` Field-index diagnostics. ##### index.describe > **describe**: (`orgName`, `repoName`) => `Promise`\<[`IndexedFieldsReport`](/sdk-reference/type-aliases/indexedfieldsreport/)\> Describe all indexed fields for a repo, grouped by state. Returns ready, building, failed, and other buckets (read-only diagnostics). Use `wh repo describe --indexed-fields --repo `. ###### Parameters ###### orgName `string` ###### repoName `string` ###### Returns `Promise`\<[`IndexedFieldsReport`](/sdk-reference/type-aliases/indexedfieldsreport/)\> #### See https://docs.warmhub.ai/sdk/repo-stats/ *** ### shape > `readonly` **shape**: `object` Shape management surface for schema definitions that validate things and assertions. #### list > **list**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`{ items: Array<{ name: string; kind: 'shape'; active: boolean; version: { version: number; operation: 'add' | 'revise' | 'retract'; data: unknown; dataHash: string; } | null; componentRef?: string; }>; }`\> List shape definitions in a repository. Options can include retracted shapes, filter by component ownership, or hide component-owned shapes. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? `ShapeListOptions` ##### Returns `Promise`\<`{ items: Array<{ name: string; kind: 'shape'; active: boolean; version: { version: number; operation: 'add' | 'revise' | 'retract'; data: unknown; dataHash: string; } | null; componentRef?: string; }>; }`\> #### get > **get**: (`orgName`, `repoName`, `shapeName`, `opts?`) => `Promise`\<`{ name: string; kind: 'shape'; active: boolean; version: { version: number; operation: 'add' | 'revise' | 'retract'; data: unknown; dataHash: string; } | null; componentRef?: string; }`\> Get one shape definition by name. Returns the full shape thing record, with `name`, `kind: "shape"`, `active`, and a nested `version: { version, operation, data, dataHash } | null`. This is **not** the same shape as the shape returned by `create` and `revise`: the change result is flat (`name`, `operation`, `version: number`, `dataHash`) and does not carry `data`. To read shape fields, call `get` and read `result.version?.data` — the change result alone is not enough. ##### Parameters ###### orgName `string` ###### repoName `string` ###### shapeName `string` ###### opts? `ShapeGetOptions` ##### Returns `Promise`\<`{ name: string; kind: 'shape'; active: boolean; version: { version: number; operation: 'add' | 'revise' | 'retract'; data: unknown; dataHash: string; } | null; componentRef?: string; }`\> #### create > **create**: (`orgName`, `repoName`, `shapeName`, `fields`, `opts?`) => `Promise`\<`{ name: string; operation: 'add' | 'revise' | 'retract' | 'noop'; version: number; dataHash: string; }`\> Create a shape definition. Shape data should describe the fields used to validate things and assertions with that shape. Returns `{ name, operation, version, dataHash }`. The result confirms the write and exposes the new version number, but it does **not** include the shape data — call `client.shape.get` for the full shape record (with `kind`, `active`, and nested `version.data`). ##### Parameters ###### orgName `string` ###### repoName `string` ###### shapeName `string` ###### fields `Record`\<`string`, `unknown`\> ###### opts? `ShapeCreateOptions` ##### Returns `Promise`\<`{ name: string; operation: 'add' | 'revise' | 'retract' | 'noop'; version: number; dataHash: string; }`\> #### revise > **revise**: (`orgName`, `repoName`, `shapeName`, `newFields`, `opts?`) => `Promise`\<`{ name: string; operation: 'add' | 'revise' | 'retract' | 'noop'; version: number; dataHash: string; }`\> Revise a shape definition, creating a new shape version. Returns `{ name, operation, version, dataHash }`, the same flat shape as `create`. To read the revised fields back, call `client.shape.get` for the full shape record. ##### Parameters ###### orgName `string` ###### repoName `string` ###### shapeName `string` ###### newFields `Record`\<`string`, `unknown`\> ###### opts? `ShapeReviseOptions` ##### Returns `Promise`\<`{ name: string; operation: 'add' | 'revise' | 'retract' | 'noop'; version: number; dataHash: string; }`\> #### remove > **remove**: (`orgName`, `repoName`, `shapeName`) => `Promise`\<`ShapeRemove`\> Retract a shape definition. ##### Parameters ###### orgName `string` ###### repoName `string` ###### shapeName `string` ##### Returns `Promise`\<`ShapeRemove`\> #### rename > **rename**: (`orgName`, `repoName`, `oldName`, `newName`) => `Promise`\<[`RenameResult`](/sdk-reference/type-aliases/renameresult/)\> Rename a shape within a repository. The rename is applied in place: the existing shape history is preserved and no new version is created. ##### Parameters ###### orgName `string` ###### repoName `string` ###### oldName `string` ###### newName `string` ##### Returns `Promise`\<[`RenameResult`](/sdk-reference/type-aliases/renameresult/)\> #### history > **history**: (`orgName`, `repoName`, `name`, `opts`) => `Promise`\<`HistoryResult`\> Return add, revise, retract, and rename history for a shape. Use pagination options for long-lived shapes with many revisions. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### name `string` ###### opts? `ShapeHistoryOptions` = `{}` ##### Returns `Promise`\<`HistoryResult`\> *** ### subscription > `readonly` **subscription**: `object` Webhook subscription management surface scoped to a repository. #### create > **create**: (`input`) => `Promise`\<`SubscriptionInfo`\> Create a webhook subscription. Webhook subscriptions require a delivery URL and can forward events from another repository, allow trace reentry, bind fallback delivery, and opt into success notifications. Component identity is set at creation time and follows the same authority rules as commit writes. Webhook subscriptions must specify a shape filter via `shapeName` or `filterJson.shape`, except [shape-lifecycle subscriptions](/subscriptions/creating/#shape-lifecycle-subscriptions) which omit both and rely on a `{ kind: 'shape', ... }` filter. ##### Parameters ###### input [`SubscriptionCompatCreateInput`](/sdk-reference/type-aliases/subscriptioncompatcreateinput/) ##### Returns `Promise`\<`SubscriptionInfo`\> #### get > **get**: (`orgName`, `repoName`, `name`) => `Promise`\<`SubscriptionInfo`\> Get one subscription by name. ##### Parameters ###### orgName `string` ###### repoName `string` ###### name `string` ##### Returns `Promise`\<`SubscriptionInfo`\> #### reveal > **reveal**: (`orgName`, `repoName`, `name`) => `Promise`\<`{ webhookUrl?: string; fallbackWebhookUrl?: string; }`\> Reveal the raw webhook URL(s) for a subscription. Reads return only `webhookOrigin`/`fallbackWebhookOrigin` (scheme://host); the raw URL path is a bearer secret. This break-glass call returns the raw URL(s) and is audit-logged server-side. Requires `repo:configure`. Because it returns the secret, a name-scoped principal (e.g. a component setup token) may reveal only the subscriptions it is scoped to — a stricter contract than the redacted `get`/`list`, which are unscoped. ##### Parameters ###### orgName `string` ###### repoName `string` ###### name `string` ##### Returns `Promise`\<`{ webhookUrl?: string; fallbackWebhookUrl?: string; }`\> #### list > **list**: (`orgName`, `repoName`) => `Promise`\<`SubscriptionList`\> List subscriptions attached to a repository. ##### Parameters ###### orgName `string` ###### repoName `string` ##### Returns `Promise`\<`SubscriptionList`\> #### update > **update**: (`input`) => `Promise`\<`SubscriptionInfo`\> Update an existing webhook subscription. Use `null` for nullable fields such as fallback webhook URL when you need to clear an existing value. Legacy cron subscriptions cannot be updated — delete them instead. ##### Parameters ###### input [`SubscriptionCompatUpdateInput`](/sdk-reference/type-aliases/subscriptioncompatupdateinput/) ##### Returns `Promise`\<`SubscriptionInfo`\> #### pause > **pause**: (`orgName`, `repoName`, `name`) => `Promise`\<\{ `name`: `string`; `active`: `boolean`; \}\> Pause a subscription. ##### Parameters ###### orgName `string` ###### repoName `string` ###### name `string` ##### Returns `Promise`\<\{ `name`: `string`; `active`: `boolean`; \}\> #### resume > **resume**: (`orgName`, `repoName`, `name`) => `Promise`\<\{ `name`: `string`; `active`: `boolean`; \}\> Resume a paused subscription. ##### Parameters ###### orgName `string` ###### repoName `string` ###### name `string` ##### Returns `Promise`\<\{ `name`: `string`; `active`: `boolean`; \}\> #### remove > **remove**: (`orgName`, `repoName`, `name`) => `Promise`\<\{ `ok`: `true`; \}\> Delete a subscription. ##### Parameters ###### orgName `string` ###### repoName `string` ###### name `string` ##### Returns `Promise`\<\{ `ok`: `true`; \}\> #### bindCredentials > **bindCredentials**: (`orgName`, `repoName`, `subscriptionName`, `credentialSetName`) => `Promise`\<`SubscriptionBindCredentialsResult`\> Bind a credential set to a subscription for outbound webhook authentication. ##### Parameters ###### orgName `string` ###### repoName `string` ###### subscriptionName `string` ###### credentialSetName `string` ##### Returns `Promise`\<`SubscriptionBindCredentialsResult`\> #### unbindCredentials > **unbindCredentials**: (`orgName`, `repoName`, `subscriptionName`) => `Promise`\<`SubscriptionUnbindCredentialsResult`\> Remove the credential set currently bound to a subscription. ##### Parameters ###### orgName `string` ###### repoName `string` ###### subscriptionName `string` ##### Returns `Promise`\<`SubscriptionUnbindCredentialsResult`\> #### See https://docs.warmhub.ai/sdk/component-identity/#subscriptions *** ### action > `readonly` **action**: `object` Low-level action lease, delivery, run, and notification primitives for subscription consumers. #### acquireLease > **acquireLease**: (`orgName`, `repoName`, `subscriptionName`, `holderId`, `holderType`, `opts?`) => `Promise`\<`ActionLeaseAcquire`\> Acquire an exclusive processing lease for a subscription consumer. ##### Parameters ###### orgName `string` ###### repoName `string` ###### subscriptionName `string` ###### holderId `string` Stable identifier for the process claiming the lease. ###### holderType `"cli"` \| `"sdk"` Kind of consumer claiming the lease. ###### opts? ###### graceMs? `number` ###### ttlMs? `number` ##### Returns `Promise`\<`ActionLeaseAcquire`\> #### heartbeatLease > **heartbeatLease**: (`orgName`, `repoName`, `subscriptionName`, `holderId`, `ttlMs?`) => `Promise`\<`ActionLeaseOp`\> Extend the TTL for an existing processing lease. ##### Parameters ###### orgName `string` ###### repoName `string` ###### subscriptionName `string` ###### holderId `string` ###### ttlMs? `number` ##### Returns `Promise`\<`ActionLeaseOp`\> #### releaseLease > **releaseLease**: (`orgName`, `repoName`, `subscriptionName`, `holderId`) => `Promise`\<`ActionLeaseOp`\> Release an existing processing lease. ##### Parameters ###### orgName `string` ###### repoName `string` ###### subscriptionName `string` ###### holderId `string` ##### Returns `Promise`\<`ActionLeaseOp`\> #### claimDelivery > **claimDelivery**: (`orgName`, `repoName`, `runId`, `holderId`) => `Promise`\<`ActionLeaseOp`\> Claim one action delivery run for processing. ##### Parameters ###### orgName `string` ###### repoName `string` ###### runId `string` ###### holderId `string` ##### Returns `Promise`\<`ActionLeaseOp`\> #### completeDelivery > **completeDelivery**: (`orgName`, `repoName`, `runId`, `holderId`) => `Promise`\<`ActionLeaseOp`\> Mark one claimed action delivery run as complete. ##### Parameters ###### orgName `string` ###### repoName `string` ###### runId `string` ###### holderId `string` ##### Returns `Promise`\<`ActionLeaseOp`\> #### liveFeed > **liveFeed**: (`orgName`, `repoName`, `subscriptionName`, `opts?`) => `Promise`\<`{ items: Array<{ subscriptionName: string; runId?: string; status: string; matchedOperationIndexes: Array; matchedOperations: Array<{ index: number; operation?: unknown; }>; createdAt: number; updatedAt?: number; runStatus?: string; attemptCount?: number; maxAttempts?: number; lastErrorCode?: string; lastErrorMessage?: string; lastResponseSnippet?: string; traceId?: string; causationId?: string; hopCount?: number; originRunId?: string; originRepoId?: string; actionContainer?: string; }>; nextCursor?: string; }`\> Query the live delivery feed for a subscription. Use this for polling or live-log views that need recent delivery status entries. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### subscriptionName `string` ###### opts? `ActionLiveFeedOptions` ##### Returns `Promise`\<`{ items: Array<{ subscriptionName: string; runId?: string; status: string; matchedOperationIndexes: Array; matchedOperations: Array<{ index: number; operation?: unknown; }>; createdAt: number; updatedAt?: number; runStatus?: string; attemptCount?: number; maxAttempts?: number; lastErrorCode?: string; lastErrorMessage?: string; lastResponseSnippet?: string; traceId?: string; causationId?: string; hopCount?: number; originRunId?: string; originRepoId?: string; actionContainer?: string; }>; nextCursor?: string; }`\> #### listRuns > **listRuns**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`{ runId: string; subscriptionName?: string; status: string; matchedOperationIndexes: Array; attemptCount: number; maxAttempts: number; lastErrorCode?: string; lastErrorMessage?: string; traceId?: string; causationId?: string; hopCount?: number; originRunId?: string; originRepoId?: string; createdAt: number; updatedAt: number; }`[]\> List subscription action runs for a repository. The result can be filtered by subscription name, status, and start time. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? `ActionListRunsOptions` ##### Returns `Promise`\<`{ runId: string; subscriptionName?: string; status: string; matchedOperationIndexes: Array; attemptCount: number; maxAttempts: number; lastErrorCode?: string; lastErrorMessage?: string; traceId?: string; causationId?: string; hopCount?: number; originRunId?: string; originRepoId?: string; createdAt: number; updatedAt: number; }`[]\> #### runStats > **runStats**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`{ total: number; byStatus: { pending: number; running: number; processing: number; retry_wait: number; succeeded: number; failed_terminal: number; dead_letter: number; }; }`\> Aggregate run counts (total + per-status) for a repo or one subscription. Computed server-side over an optional `since` window — not bounded by the `listRuns` row cap, so it reflects the full window regardless of volume. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? `ActionRunStatsOptions` ##### Returns `Promise`\<`{ total: number; byStatus: { pending: number; running: number; processing: number; retry_wait: number; succeeded: number; failed_terminal: number; dead_letter: number; }; }`\> #### getRunAttempts > **getRunAttempts**: (`orgName`, `repoName`, `runId`) => `Promise`\<`{ attempt: number; status: string; startedAt: number; finishedAt?: number; httpStatus?: number; errorCode?: string; errorMessage?: string; responseSnippet?: string; }`[]\> List delivery attempts for one action run. ##### Parameters ###### orgName `string` ###### repoName `string` ###### runId `string` ##### Returns `Promise`\<`{ attempt: number; status: string; startedAt: number; finishedAt?: number; httpStatus?: number; errorCode?: string; errorMessage?: string; responseSnippet?: string; }`[]\> #### listNotifications > **listNotifications**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`{ subscriptionName?: string; attempt: number; channel: string; status: string; eventType?: string; errorCode?: string; errorMessage?: string; createdAt: number; }`[]\> List repo-scoped action notifications. Use `since` or `limit` to bound notification-center style reads. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? `ActionListNotificationsOptions` ##### Returns `Promise`\<`{ subscriptionName?: string; attempt: number; channel: string; status: string; eventType?: string; errorCode?: string; errorMessage?: string; createdAt: number; }`[]\> *** ### thing > `readonly` **thing**: `object` Read surface for things, assertions, histories, references, search, and in-place thing renames. #### head > **head**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`HeadResult`\> Return the current HEAD snapshot for repository contents. Filter by shape, kind, assertion target, or glob `match` pattern, and choose the data mode appropriate for the payload size. Component filters can narrow results to component-owned records or hide component infrastructure records. Tokenless reads of public repositories have stricter page-size and page-count limits than authenticated reads. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? `ThingHeadOptions` ##### Returns `Promise`\<`HeadResult`\> #### headIter > **headIter**: (`orgName`, `repoName`, `opts?`) => `AsyncIterableIterator`\<`ThingItem`\> Iterate every current HEAD row matching the supplied filters. Prefer this over hand-written cursor loops when scanning all matching records. Pass `opts.cursor` to resume from a saved cursor; the iterator advances the cursor automatically after the first request. Pass `limit` to control page size. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? `ThingHeadOptions` ##### Returns `AsyncIterableIterator`\<`ThingItem`\> #### headAll > **headAll**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`ThingItem`[]\> Materialize every current HEAD row matching the supplied filters. Use `max` to guard memory usage; throws a `WarmHubError` with kind `VALIDATION_ERROR` once more than `max` items have actually been observed across pages. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? `ThingHeadOptions` & `object` ##### Returns `Promise`\<`ThingItem`[]\> #### get > **get**: (`orgName`, `repoName`, `wref`, `version?`, `opts?`) => `Promise`\<[`ThingDetail`](/sdk-reference/type-aliases/thingdetail/)\> Get one thing, assertion, shape, or collection by wref. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wref `string` ###### version? `number` Optional exact version to pin when the wref is not already version-qualified. ###### opts? `ThingGetOptions` ##### Returns `Promise`\<[`ThingDetail`](/sdk-reference/type-aliases/thingdetail/)\> #### getWithLease > **getWithLease**: (`orgName`, `repoName`, `wref`, `opts?`) => `Promise`\<[`ThingGetWithLease`](/sdk-reference/type-aliases/thinggetwithlease/)\> Acquire a short, bounded, exclusive lease on a thing AND read it in one atomic round trip (#3625). Returns everything WarmHub.thing.get returns plus a `lease` block; the holder echoes `lease.id` back as `leaseId` on the subsequent `revise`/`retract` (auto-releasing the lease) or calls WarmHub.thing.releaseLease to return it early. Requires `things:write` — never anonymous. Fail-fast: if another holder already holds an active lease, throws a `WarmHubError` with `kind === 'LEASE_UNAVAILABLE'` and `error.details?.reason === 'lease_held'` (read `leaseExpiresAt` to back off until expiry). `ttlMs` out of the backend's bounds (default 5s / min 1s / max 30s) is rejected, never clamped. ##### Parameters ###### orgName `string` ###### repoName `string` ###### wref `string` ###### opts? ###### ttlMs? `number` ##### Returns `Promise`\<[`ThingGetWithLease`](/sdk-reference/type-aliases/thinggetwithlease/)\> #### releaseLease > **releaseLease**: (`orgName`, `repoName`, `wref`, `leaseId`) => `Promise`\<`void`\> Release a lease early (#3625), closing the acquire↔release loop without waiting out the TTL. Idempotent and owner-gated: releasing an absent, already-released, expired, or non-matching lease is a benign no-op (no error). A successful `revise`/`retract` carrying the `leaseId` already auto-releases the lease, so this is only needed when the holder decides not to mutate. Requires `things:write`. ##### Parameters ###### orgName `string` ###### repoName `string` ###### wref `string` ###### leaseId `string` ##### Returns `Promise`\<`void`\> #### graph > **graph**: (`orgName`, `repoName`, `wref`, `opts?`) => `Promise`\<`ThingGraphResult`\> Get one record and its embedded assertion, about, and wref graph. Depth and limit options bound traversal size. References the caller cannot read remain string wrefs in the returned graph. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wref `string` ###### opts? `ThingGraphOptions` ##### Returns `Promise`\<`ThingGraphResult`\> #### getMany > **getMany**: (`orgName`, `repoName`, `wrefs`, `version?`, `opts?`) => `Promise`\<`ThingGetManyResult`\> Batch-fetch wrefs, auto-chunking above the backend's 500-wref transport cap. The result preserves duplicate requested wrefs and reports inaccessible or missing refs in `missing` rather than throwing per item. A top-level version pins every unqualified wref; per-wref version pins remain intact. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wrefs `string`[] ###### version? `number` Optional exact version to apply to unqualified wrefs. ###### opts? ###### includeRetracted? `boolean` Include retracted records in `items` instead of reporting them in `missing`. ###### chunkSize? `number` Maximum wrefs per backend request. Defaults to 500 and is clamped to the backend cap. ###### chunkConcurrency? `number` Maximum concurrent chunk requests. Defaults to 1 and is clamped to 8. ##### Returns `Promise`\<`ThingGetManyResult`\> #### headVersions > **headVersions**: (`orgName`, `repoName`, `wrefs`, `opts?`) => `Promise`\<`ThingHeadVersionsResult`\> Batched lightweight per-thing change probe — returns `{ wref, durableId, version, active, versionCreatedAt }` per wref with NO payload. Use it to check whether locally-cached copies are still fresh without pulling `data`: a value is stale if `durableId` differs (the wref now points to a different thing), `version`/`active` differ (the same thing changed/was retracted), or the wref appears in `missing`. Auto-chunks above the backend's 500-wref transport cap; preserves duplicate requested wrefs and reports inaccessible/unknown refs in `missing` rather than throwing per item. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wrefs `string`[] ###### opts? ###### chunkSize? `number` Maximum wrefs per backend request. Defaults to 500 and is clamped to the backend cap. ###### chunkConcurrency? `number` Maximum concurrent chunk requests. Defaults to 1 and is clamped to 8. ##### Returns `Promise`\<`ThingHeadVersionsResult`\> #### history > **history**: (`orgName`, `repoName`, `opts`) => `Promise`\<`HistoryResult`\> Return version history and timeline metadata for repository records. Provide at least one selector: a concrete wref, a shape filter, or an assertion target. Shape- and target-filtered histories support pagination and optional collection resolution. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### opts `ThingHistoryOptions` ##### Returns `Promise`\<`HistoryResult`\> #### rename > **rename**: (`orgName`, `repoName`, `shapeName`, `oldName`, `newName`) => `Promise`\<[`RenameResult`](/sdk-reference/type-aliases/renameresult/)\> Rename a thing within its shape namespace. The rename is applied in place: the thing's existing history is preserved and no new version is created. ##### Parameters ###### orgName `string` ###### repoName `string` ###### shapeName `string` ###### oldName `string` ###### newName `string` ##### Returns `Promise`\<[`RenameResult`](/sdk-reference/type-aliases/renameresult/)\> #### resolve > **resolve**: (`orgName`, `repoName`, `wref`) => `Promise`\<[`ThingDetail`](/sdk-reference/type-aliases/thingdetail/)\> Resolve a wref to its current projected record. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wref `string` ##### Returns `Promise`\<[`ThingDetail`](/sdk-reference/type-aliases/thingdetail/)\> #### about > **about**: (`orgName`, `repoName`, `wref`, `opts?`) => `Promise`\<`AboutResult`\> Return assertions about a thing or collection target. Filter by assertion shape or glob `match` pattern, optionally resolve collection targets, and page through large assertion sets with `limit` and `cursor`. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. Returns `{ target?, assertions, nextCursor? }`. The array is named `assertions`, **not** `items`. This breaks the repo-wide `items` convention used by `HeadResult`, `FilterResult`, `SearchResult`, `RefsResult`, and `LogResult`; destructure explicitly to avoid the trap: ```ts const { target, assertions } = await client.thing.about(org, repo, "Location/cave"); for (const a of assertions) console.log(a.wref); ``` Any returned subjective-logic opinion tuple `(b, d, u, α)` is a binomial opinion — well-formed only when the underlying assertion expresses a binary proposition. See [Opinions as Separate Assertions](/data-modeling/patterns/#opinions-as-separate-assertions). ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wref `string` ###### opts? `AboutOptions` ##### Returns `Promise`\<`AboutResult`\> #### aboutIter > **aboutIter**: (`orgName`, `repoName`, `wref`, `opts?`) => `AsyncIterableIterator`\<`Assertion`\> Iterate every assertion about a thing or collection target. Prefer this over hand-written cursor loops when scanning all matching assertions. The iterator reads the `assertions` envelope field and advances the cursor automatically; pass `opts.cursor` to resume from a saved cursor and `limit` to control page size. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wref `string` ###### opts? `AboutOptions` ##### Returns `AsyncIterableIterator`\<`Assertion`\> #### aboutAll > **aboutAll**: (`orgName`, `repoName`, `wref`, `opts?`) => `Promise`\<`Assertion`[]\> Materialize every assertion about a thing or collection target. Use `max` to guard memory usage; throws a `WarmHubError` with kind `VALIDATION_ERROR` once more than `max` assertions have actually been observed across pages. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wref `string` ###### opts? `AboutOptions` & `object` ##### Returns `Promise`\<`Assertion`[]\> #### query > **query**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`HeadResult`\> Query repository records by shape, kind, assertion target, text filters, or glob `match` pattern. Use this for structured reads where the caller controls filters. For ranked text or vector search, use `thing.search`. For count-only reads, use `thing.count`. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? [`FilterOptions`](/sdk-reference/type-aliases/filteroptions/) ##### Returns `Promise`\<`HeadResult`\> #### queryIter > **queryIter**: (`orgName`, `repoName`, `opts?`) => `AsyncIterableIterator`\<`ThingItem`\> Iterate every repository record matching the supplied filters. Prefer this over hand-written cursor loops when scanning all matching records. Pass `opts.cursor` to resume from a saved cursor; the iterator advances the cursor automatically after the first request. Pass `limit` to control page size. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? [`FilterOptions`](/sdk-reference/type-aliases/filteroptions/) ##### Returns `AsyncIterableIterator`\<`ThingItem`\> #### queryAll > **queryAll**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`ThingItem`[]\> Materialize every repository record matching the supplied filters. Use `max` to guard memory usage; throws a `WarmHubError` with kind `VALIDATION_ERROR` once more than `max` items have actually been observed across pages. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? [`FilterOptions`](/sdk-reference/type-aliases/filteroptions/) & `object` ##### Returns `Promise`\<`ThingItem`[]\> #### search > **search**: (`orgName`, `repoName`, `query`, `opts?`) => `Promise`\<`HeadResult`\> Search repository records with text, vector, or hybrid mode. When searching with an assertion target or collection resolution, pages may be sparse; keep paginating until `nextCursor` is absent. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### query `string` ###### opts? [`SearchOptions`](/sdk-reference/type-aliases/searchoptions/) ##### Returns `Promise`\<`HeadResult`\> #### count > **count**: (`orgName`, `repoName`, `opts?`) => `Promise`\<`CountResult`\> Count matching repository records without returning record data. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts? [`FilterOptions`](/sdk-reference/type-aliases/filteroptions/) ##### Returns `Promise`\<`CountResult`\> #### refs > **refs**: (`orgName`, `repoName`, `wref`, `opts?`) => `Promise`\<`RefsResult`\> Query wref-typed field references for a record. Inbound mode finds records whose wref fields point at the supplied wref. Outbound mode finds records that the supplied record points to. Inbound queries can be narrowed to a field path. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wref `string` ###### opts? [`RefsOptions`](/sdk-reference/type-aliases/refsoptions/) ##### Returns `Promise`\<`RefsResult`\> #### refsIter > **refsIter**: (`orgName`, `repoName`, `wref`, `opts?`) => `AsyncIterableIterator`\<\{ `wref`: `string`; `kind?`: `"shape"` \| `"thing"` \| `"assertion"` \| `"collection"`; `shapeName?`: `string`; `version?`: `number`; `fieldPath?`: `string`; \}\> Iterate every wref-typed field reference for a record. Prefer this over hand-written cursor loops when scanning all matching references. Pass `opts.cursor` to resume from a saved cursor; the iterator advances the cursor automatically after the first request. Pass `limit` to control page size. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wref `string` ###### opts? [`RefsOptions`](/sdk-reference/type-aliases/refsoptions/) ##### Returns `AsyncIterableIterator`\<\{ `wref`: `string`; `kind?`: `"shape"` \| `"thing"` \| `"assertion"` \| `"collection"`; `shapeName?`: `string`; `version?`: `number`; `fieldPath?`: `string`; \}\> #### refsAll > **refsAll**: (`orgName`, `repoName`, `wref`, `opts?`) => `Promise`\<`object`[]\> Materialize every wref-typed field reference for a record. Use `max` to guard memory usage; throws a `WarmHubError` with kind `VALIDATION_ERROR` once more than `max` refs have actually been observed across pages. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` \| `undefined` ###### repoName `string` \| `undefined` ###### wref `string` ###### opts? [`RefsOptions`](/sdk-reference/type-aliases/refsoptions/) & `object` ##### Returns `Promise`\<`object`[]\> *** ### live > `readonly` **live**: `object` Live repository update surface backed by server-sent events. #### thingHead > **thingHead**: (`orgName`, `repoName`, `opts`, `onUpdate`) => `Promise`\<[`LiveHandle`](/sdk-reference/type-aliases/livehandle/)\> Stream refreshed `thing.head` results whenever the repository changes. The SDK re-runs the underlying `thing.head` query after each invalidation and passes the latest snapshot to `onUpdate`. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts `LiveThingHeadOptions` \| `undefined` ###### onUpdate (`result`) => `void` \| `Promise`\<`void`\> ##### Returns `Promise`\<[`LiveHandle`](/sdk-reference/type-aliases/livehandle/)\> #### thingHistory > **thingHistory**: (`orgName`, `repoName`, `opts`, `onUpdate`) => `Promise`\<[`LiveHandle`](/sdk-reference/type-aliases/livehandle/)\> Stream refreshed history results for a single wref whenever the repository changes. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts `LiveThingHistoryOptions` ###### onUpdate (`result`) => `void` \| `Promise`\<`void`\> ##### Returns `Promise`\<[`LiveHandle`](/sdk-reference/type-aliases/livehandle/)\> #### subscriptionLog > **subscriptionLog**: (`orgName`, `repoName`, `subscriptionName`, `opts`, `onUpdate`) => `Promise`\<[`LiveHandle`](/sdk-reference/type-aliases/livehandle/)\> Stream refreshed action live-feed entries for a subscription. Cursor contract: cursors are short-lived resume tokens for the same query and result scope. If filters, visibility, or backing streams change, the backend may reject a saved cursor with `VALIDATION_ERROR` / `Invalid cursor`; restart the query without `cursor` instead of retrying the stale token. ##### Parameters ###### orgName `string` ###### repoName `string` ###### subscriptionName `string` ###### opts `LiveSubscriptionLogOptions` \| `undefined` ###### onUpdate (`result`) => `void` \| `Promise`\<`void`\> ##### Returns `Promise`\<[`LiveHandle`](/sdk-reference/type-aliases/livehandle/)\> #### subscribe > **subscribe**: (`orgName`, `repoName`, `opts`, `onEvent`) => `Promise`\<[`LiveHandle`](/sdk-reference/type-aliases/livehandle/)\> Subscribe to raw repository invalidation events. Unlike the higher-level live helpers, this method does not re-query. It forwards invalidation metadata such as affected shapes, affected things, affected assertion targets, and whether the event corresponds to a new commit. ##### Parameters ###### orgName `string` ###### repoName `string` ###### opts \{ `signal?`: `AbortSignal`; \} \| `undefined` ###### Type Literal \{ `signal?`: `AbortSignal`; \} ###### signal? `AbortSignal` Optional abort signal used to close the SSE stream. *** `undefined` ###### onEvent (`event`) => `void` \| `Promise`\<`void`\> Callback invoked for each repository invalidation event. ##### Returns `Promise`\<[`LiveHandle`](/sdk-reference/type-aliases/livehandle/)\> *** ### token > `readonly` **token**: `object` Personal access token management for the authenticated user. #### create > **create**: (`input`) => `Promise`\<`{ token: string; name: string; scopes?: Array<{ resource?: string; permissions: Array; allowedMatches?: Array; }>; warnings?: Array; expiresAt: number; createdAt: number; }`\> Create a personal access token for the authenticated user. Omit `scopes` to mint a token with the same authority as the calling principal. PATs cannot create or revoke other PATs — token-management permissions are excluded from the grantable set. Server enforces a maximum lifetime; pass an `expiresAt` unix-millis value to clamp earlier. See [Personal Access Tokens](/auth/personal-access-tokens/) for scope grammar (resource format, permission strings, `allowedMatches`), rotation, and CI usage. ##### Parameters ###### input `{ name: string; scopes?: Array<{ resource?: string; permissions: Array; allowedMatches?: Array; }>; structured?: boolean; description?: string; expiresAt?: number; committerIdentityWref?: string; }` ##### Returns `Promise`\<`{ token: string; name: string; scopes?: Array<{ resource?: string; permissions: Array; allowedMatches?: Array; }>; warnings?: Array; expiresAt: number; createdAt: number; }`\> #### list > **list**: (`opts?`) => `Promise`\<`TokenInfo`[]\> List personal access tokens for the authenticated user. By default only active tokens are returned. Pass `{ includeInactive: true }` to also include expired and revoked tokens. ##### Parameters ###### opts? ###### includeInactive? `boolean` ##### Returns `Promise`\<`TokenInfo`[]\> #### get > **get**: (`name`) => `Promise`\<`TokenInfo` \| `null`\> Get one personal access token by name. ##### Parameters ###### name `string` ##### Returns `Promise`\<`TokenInfo` \| `null`\> #### revoke > **revoke**: (`name`) => `Promise`\<`{ ok: boolean; }`\> Revoke a personal access token by name. ##### Parameters ###### name `string` ##### Returns `Promise`\<`{ ok: boolean; }`\> *** ### stream > `readonly` **stream**: `object` Low-level stream append surface for callers that already have backend stream operations. #### append > **append**: (`input`) => `Promise`\<[`StreamAppendResult`](/sdk-reference/type-aliases/streamappendresult/)\> Append one non-empty chunk of stream operations to a repository. Most callers should prefer `commit.apply` or `OperationBuilder`. Use this low-level surface only when you already have stream operations, a stream ID, and any allocated `$N` / `#N` token state you need to continue. ##### Parameters ###### input [`StreamAppendInput`](/sdk-reference/type-aliases/streamappendinput/) ##### Returns `Promise`\<[`StreamAppendResult`](/sdk-reference/type-aliases/streamappendresult/)\> *** ### credential > `readonly` **credential**: `object` Credential set management for subscription webhook authentication and component integrations. Sets are scoped at creation time. Repo-scoped sets are visible only to the owning repo; org-scoped sets can be granted to multiple repositories in the same organization. Methods that operate on a specific set accept `repoName: string | undefined` — pass the owning repo name for repo-scoped sets or `undefined` for org-scoped sets. #### createSet > **createSet**: (`orgName`, `repoName`, `name`, `opts?`) => `Promise`\<`CredentialInfo`\> Create a credential set. Credential sets are repo-scoped by default. Org-scoped sets can be granted to multiple repositories in the same organization. ##### Parameters ###### orgName `string` ###### repoName `string` \| `undefined` ###### name `string` ###### opts? ###### scope? `"repo"` \| `"org"` ###### description? `string` ##### Returns `Promise`\<`CredentialInfo`\> #### listSets > **listSets**: (`orgName`, `repoName?`) => `Promise`\<`CredentialInfo`[]\> List credential sets visible from a repository. ##### Parameters ###### orgName `string` ###### repoName? `string` ##### Returns `Promise`\<`CredentialInfo`[]\> #### getSet > **getSet**: (`orgName`, `repoName`, `name`) => `Promise`\<`CredentialInfo`\> Get credential set metadata without secret values. ##### Parameters ###### orgName `string` ###### repoName `string` \| `undefined` ###### name `string` ##### Returns `Promise`\<`CredentialInfo`\> #### setKey > **setKey**: (`orgName`, `repoName`, `setName`, `keyName`, `value`) => `Promise`\<`CredentialKeyMutationResult`\> Set or replace one secret key in a credential set. ##### Parameters ###### orgName `string` ###### repoName `string` \| `undefined` ###### setName `string` ###### keyName `string` ###### value `string` ##### Returns `Promise`\<`CredentialKeyMutationResult`\> #### setKeys > **setKeys**: (`orgName`, `repoName`, `setName`, `secrets`) => `Promise`\<`CredentialKeyMutationResult`\> Set or replace multiple secret keys in one request. ##### Parameters ###### orgName `string` ###### repoName `string` \| `undefined` ###### setName `string` ###### secrets `Record`\<`string`, `string`\> ##### Returns `Promise`\<`CredentialKeyMutationResult`\> #### unsetKey > **unsetKey**: (`orgName`, `repoName`, `setName`, `keyName`) => `Promise`\<`CredentialKeyMutationResult`\> Remove one secret key from a credential set. ##### Parameters ###### orgName `string` ###### repoName `string` \| `undefined` ###### setName `string` ###### keyName `string` ##### Returns `Promise`\<`CredentialKeyMutationResult`\> #### deleteSet > **deleteSet**: (`orgName`, `repoName`, `setName`) => `Promise`\<`CredentialDeleteResult`\> Delete a credential set and its stored secrets. ##### Parameters ###### orgName `string` ###### repoName `string` \| `undefined` ###### setName `string` ##### Returns `Promise`\<`CredentialDeleteResult`\> #### listAuditLog > **listAuditLog**: (`orgName`, `repoName`, `setName`, `opts?`) => `Promise`\<`CredentialAuditEntry`[]\> List audit entries for a credential set. ##### Parameters ###### orgName `string` ###### repoName `string` \| `undefined` ###### setName `string` ###### opts? ###### limit? `number` ##### Returns `Promise`\<`CredentialAuditEntry`[]\> #### revokeSet > **revokeSet**: (`orgName`, `repoName`, `setName`, `opts?`) => `Promise`\<`CredentialRevokeResult`\> Revoke a credential set so it can no longer be exported or bound. ##### Parameters ###### orgName `string` ###### repoName `string` \| `undefined` ###### setName `string` ###### opts? ###### reason? `string` ##### Returns `Promise`\<`CredentialRevokeResult`\> ## Methods ### withAccessToken() > **withAccessToken**(`accessToken`): `WarmHubClient` Return a new client that shares this client's backend URL and fetch implementation but uses a different access-token provider. #### Parameters ##### accessToken `AccessTokenProvider` #### Returns `WarmHubClient` --- # WarmHubError Normalized error shape thrown by the WarmHub SDK. SDK helpers convert transport, tRPC, validation, and backend failures into this class so callers can branch on stable `code` / `kind` values and read optional response metadata such as `status`, `hint`, and `retryAfter`. See [ErrorKind](/sdk-reference/type-aliases/errorkind/) for the catalog of stable kinds with cause, retryability, corrective action, and origin per kind, and [isRetryable](/sdk-reference/functions/isretryable/) for the canonical retry test. ## Example ```ts try { await client.repo.get('acme', 'world') } catch (err) { if (isWarmHubError(err) && err.kind === 'NOT_FOUND') { // handle missing repo } else if (isRetryable(err)) { // safe to retry (NETWORK, CANCELLED, BACKEND, RATE_LIMITED) } else { throw err } } ``` ## Extends - `Error` ## Constructors ### Constructor > **new WarmHubError**(`code`, `message`, `status?`, `hint?`, `retryAfter?`, `errorCode?`, `details?`): `WarmHubError` #### Parameters ##### code `string` ##### message `string` ##### status? `number` ##### hint? `string` ##### retryAfter? `number` ##### errorCode? `string` ##### details? [`WarmHubErrorDetails`](/sdk-reference/type-aliases/warmhuberrordetails/) #### Returns `WarmHubError` #### Overrides `Error.constructor` ## Properties ### code > `readonly` **code**: `string` Stable SDK error code or pass-through backend domain code. Branch on this (or the alias [kind](/sdk-reference/classes/warmhuberror/#kind)) to handle expected failure modes. *** ### status? > `readonly` `optional` **status?**: `number` HTTP status from the failing response when one was available. *** ### hint? > `readonly` `optional` **hint?**: `string` Optional human-readable hint surfaced by the backend. *** ### retryAfter? > `readonly` `optional` **retryAfter?**: `number` Seconds the caller should wait before retrying. Present on `RATE_LIMITED` responses and other backend signals that carry a Retry-After header. *** ### errorCode? > `readonly` `optional` **errorCode?**: `string` Canonical domain error code from the response body. Set iff the backend wire carried a structured `error.code` string. Use this when the question is "did the backend specifically say this?". For best-effort labelling that also covers SDK-local transport codes (`NETWORK`, `CANCELLED`, the generic `BACKEND` fallback), branch on [code](/sdk-reference/classes/warmhuberror/#code) or [kind](/sdk-reference/classes/warmhuberror/#kind). *** ### ~~backendCode?~~ > `readonly` `optional` **backendCode?**: `string` :::caution[Deprecated] Use [errorCode](/sdk-reference/classes/warmhuberror/#errorcode). Retained during the GH-3533 migration window for callers that already branch on the old field. ::: *** ### details? > `readonly` `optional` **details?**: [`WarmHubErrorDetails`](/sdk-reference/type-aliases/warmhuberrordetails/) Structured backend error details, when the response carried them. Branch on `details.reason`: an `expected_version_mismatch` carries `expectedVersion`/`currentVersion` so an optimistic-concurrency caller can re-read HEAD and retry (#3624); a `lease_held` carries `leaseExpiresAt` so a caller can back off until the lease expires (#3625). Present only when the backend wire carried `data.warmhub.details`. See [WarmHubErrorDetails](/sdk-reference/type-aliases/warmhuberrordetails/). ## Accessors ### kind #### Get Signature > **get** **kind**(): [`ErrorKind`](/sdk-reference/type-aliases/errorkind/) ##### Returns [`ErrorKind`](/sdk-reference/type-aliases/errorkind/) --- # connectionErrorMessage > **connectionErrorMessage**(`url`): `string` Canonical user-facing message for a connection-level failure. Embeds the target URL so the user can see which endpoint was unreachable. ## Parameters ### url `string` ## Returns `string` --- # contentFieldLimitError > **contentFieldLimitError**(`path`, `value`): `string` \| `null` Returns a validation error message if the given string exceeds [MAX\_CONTENT\_FIELD\_BYTES](/sdk-reference/variables/max_content_field_bytes/) when UTF-8 encoded, or `null` if it is within the limit. ## Parameters ### path `string` The field path, used to identify the offending field in the message. ### value `string` The candidate field value to measure. ## Returns `string` \| `null` --- # isConnectionError > **isConnectionError**(`error`): `boolean` True if `error` is a connection-level fetch failure — raised before the server produced a response. Detects both `TypeError('fetch failed')` (Node undici, browsers) and Bun's native `Error` with a `code` like `ConnectionRefused` / `ECONNREFUSED`. ## Parameters ### error `unknown` ## Returns `boolean` --- # isRetryable > **isRetryable**(`error`): `boolean` Return whether an error kind is generally safe to retry. Returns `true` for these [ErrorKind](/sdk-reference/type-aliases/errorkind/) values: - `NETWORK` — transport-level failure during a request (DNS, refused, timeout, mid-flight fetch error). Safe to retry GETs and other read surfaces; for side-effectful POSTs the request may already have landed on the server, so retry only when the surface is idempotent or use an idempotency key / state reconciliation before replaying. - `CANCELLED` — the caller aborted; retry only if the caller still wants the result. - `BACKEND` — generic server-side failure with no specific domain code; retry with backoff and surface the message if it persists. - `RATE_LIMITED` — caller exceeded the budget; retry after the [WarmHubError.retryAfter](/sdk-reference/classes/warmhuberror/#retryafter) interval. All other kinds — including backend pass-through domain codes such as `SHAPE_MISMATCH` or `WREF_UNRESOLVABLE` — return `false`. This is a conservative default: for most pass-through codes the caller must fix the input or escalate, but the backend reuses some codes for transient conditions (notably `INTERNAL_ERROR` from the webhook-validation path, whose `message` literally says `please retry`). See the pass-through paragraph on [ErrorKind](/sdk-reference/type-aliases/errorkind/) — read `WarmHubError.message` before giving up on a `false` return from this helper. ## Parameters ### error `unknown` ## Returns `boolean` --- # isWarmHubError > **isWarmHubError**(`error`): `error is WarmHubError` Return whether a thrown value is already a `WarmHubError`. ## Parameters ### error `unknown` ## Returns `error is WarmHubError` --- # normalizeWref > **normalizeWref**(`wref`): `string` Remove an `@vN`, `@HEAD`, or `@ALL` version suffix from a wref. ## Parameters ### wref `string` ## Returns `string` ## See https://docs.warmhub.ai/data-modeling/wrefs/#version-modifiers --- # toWarmHubError > **toWarmHubError**(`error`): [`WarmHubError`](/sdk-reference/classes/warmhuberror/) Normalize an unknown thrown value into `WarmHubError`. ## Parameters ### error `unknown` ## Returns [`WarmHubError`](/sdk-reference/classes/warmhuberror/) --- # validateAgainstShape > **validateAgainstShape**(`data`, `shapeFields`): [`ShapeValidatorResult`](/sdk-reference/type-aliases/shapevalidatorresult/) Validate a data payload against shape field definitions. Shape field types: - "number", "string", "boolean": primitive types - "wref": string (reference to another thing) - Typed field objects: { type: primitive, description?: string } - Nested objects: { fieldName: type, ... } - Arrays: [elementType] - Optional fields: field name ending with "?" (e.g. "terminal_reason?": "string") OR type string ending with "?" (e.g. "terminal_reason": "string?") OR typed object type ending with "?" (e.g. { type: "string?" }) For v1 this is permissive: validates top-level field existence and basic types. Returns a [ShapeValidatorResult](/sdk-reference/type-aliases/shapevalidatorresult/) discriminated union — read `result.valid` to branch, then `result.errors` (only present when invalid) for the list of messages. `result.warnings` may be present in either branch and carries undeclared top-level field names. ## Parameters ### data `Record`\<`string`, `unknown`\> ### shapeFields `Record`\<`string`, `unknown`\> ## Returns [`ShapeValidatorResult`](/sdk-reference/type-aliases/shapevalidatorresult/) ## Example ```ts import { validateAgainstShape } from "@warmhub/sdk-ts"; const shapeFields = { x: "number", y: "number", label: "string?" }; const ok = validateAgainstShape({ x: 1, y: 2 }, shapeFields); // ok.valid === true const bad = validateAgainstShape({ x: "not-a-number" }, shapeFields); if (!bad.valid) { for (const message of bad.errors) console.error(message); // ['Field "x" must be a number', 'Missing required field: "y"'] } ``` --- # verifyCliCall > **verifyCliCall**(`request`, `secrets`, `opts?`): `Promise`\<[`VerifiedCliCall`](/sdk-reference/interfaces/verifiedclicall/)\> Verify and parse an inbound component CLI call. ## Parameters ### request `Request` The Fetch `Request` as received by the worker. ### secrets [`CliCallSecrets`](/sdk-reference/type-aliases/clicallsecrets/) Credential set values (see `CliCallSecrets`). ### opts? Optional overrides. `nowUnixSeconds` defaults to the current wall clock; `toleranceSec` defaults to 300 s. #### nowUnixSeconds? `number` #### toleranceSec? `number` ## Returns `Promise`\<[`VerifiedCliCall`](/sdk-reference/interfaces/verifiedclicall/)\> --- # AddOp Builder add operation for shapes, things, assertions, and collections. ## Properties ### operation > `readonly` **operation**: `"add"` Operation discriminator. Always `add`. *** ### name > `readonly` **name**: `string` Target name. Use `Shape/localName` for things and assertions; use the plain shape name for shapes. *** ### kind? > `readonly` `optional` **kind?**: `string` Optional kind override: `shape`, `thing`, `assertion`, or `collection`. *** ### about? > `readonly` `optional` **about?**: `string` \| `CollectionAbout` Assertion target wref or inline collection descriptor. Presence of `about` makes the add an assertion unless `kind` overrides it. *** ### type? > `readonly` `optional` **type?**: `string` Collection type. Used only for collection adds. *** ### members? > `readonly` `optional` **members?**: `string`[] Collection member wrefs. Used only for collection adds. *** ### shapeWref? > `readonly` `optional` **shapeWref?**: `string` Optional shape wref used by local validation. *** ### aboutWref? > `readonly` `optional` **aboutWref?**: `string` Deprecated spelling for `about`; retained for compatibility. *** ### data? > `readonly` `optional` **data?**: `unknown` Shape-validated data payload for shape, thing, or assertion adds. *** ### skipExisting? > `readonly` `optional` **skipExisting?**: `boolean` When true, an existing target returns `noop` instead of failing. --- # AddOperation Add operation accepted by `client.commit.apply`. Use this shape to create new shapes, things, assertions, or collections in a single commit. ## See https://docs.warmhub.ai/writes/operations/#add-operations ## Properties ### operation? > `optional` **operation?**: `"add"` Operation discriminator. Defaults to `add` when omitted. *** ### kind? > `optional` **kind?**: `"shape"` \| `"thing"` \| `"assertion"` \| `"collection"` Optional kind override: `shape`, `thing`, `assertion`, or `collection`. When omitted, kind is inferred: `about` present → assertion; collection `type` plus `members` → collection; otherwise by `name` segmentation (1–2 segments → thing, 3+ segments → assertion). Shape adds always require explicit `kind: 'shape'`. *** ### name? > `optional` **name?**: `string` Target name. Use `Shape/localName` for things and assertions; use the plain shape name for shapes. *** ### about? > `optional` **about?**: `string` \| `CollectionAbout` Assertion target wref or inline collection descriptor. Presence of `about` makes the add an assertion unless `kind` overrides it. *** ### data? > `optional` **data?**: `unknown` Shape-validated data payload for shape, thing, or assertion adds. *** ### type? > `optional` **type?**: `CollectionTag` Collection type. Used only for collection adds. *** ### members? > `optional` **members?**: `string`[] Collection member wrefs. Used only for collection adds. *** ### skipExisting? > `optional` **skipExisting?**: `boolean` When true, an existing target returns `noop` instead of failing with `ALREADY_EXISTS`. Use for caller-side idempotency on retried writes. --- # OperationBuilderOptions Options for constructing an `OperationBuilder`. ## Properties ### shapes? > `optional` **shapes?**: `Record`\<`string`, [`ShapeFields`](/sdk-reference/type-aliases/shapefields/)\> Known shape definitions used for local data validation. --- # RetractOp Builder retract operation. ## Properties ### operation > `readonly` **operation**: `"retract"` Operation discriminator. Always `retract`. *** ### name > `readonly` **name**: `string` Target wref or local name to retract. *** ### kind? > `readonly` `optional` **kind?**: `"shape"` \| `"thing"` \| `"assertion"` \| `"collection"` Optional kind safety hint: `thing`, `assertion`, `shape`, or `collection`. *** ### reason? > `readonly` `optional` **reason?**: `string` Optional human-readable retraction reason. *** ### leaseId? > `readonly` `optional` **leaseId?**: `string` Optional read-lease token (#3625) returned by `thing.getWithLease`. When it matches the target's active lease the lease auto-releases on commit; a mismatch (or absence against a live lease) is rejected with `LEASE_UNAVAILABLE`. --- # RetractOperation Retract operation accepted by `client.commit.apply`. Marks the target as retracted in a new version; prior versions remain queryable by history. ## See https://docs.warmhub.ai/writes/operations/#retract-operations ## Properties ### operation > **operation**: `"retract"` Operation discriminator. Required. *** ### name > **name**: `string` Target wref or local name to retract. *** ### kind? > `optional` **kind?**: `"shape"` \| `"thing"` \| `"assertion"` \| `"collection"` Optional kind safety hint: `thing`, `assertion`, `shape`, or `collection`. *** ### reason? > `optional` **reason?**: `string` Optional human-readable retraction reason. *** ### leaseId? > `optional` **leaseId?**: `string` Optional read-lease token (#3625) returned by `thing.getWithLease`. A matching token auto-releases the lease on commit; a mismatch (or absence against a live lease) is rejected with `LEASE_UNAVAILABLE`. --- # ReviseOp Builder revise operation. ## Properties ### operation > `readonly` **operation**: `"revise"` Operation discriminator. Always `revise`. *** ### wref? > `readonly` `optional` **wref?**: `string` Target wref to revise. *** ### name? > `readonly` `optional` **name?**: `string` Target name to revise. Equivalent to `wref` for local paths. *** ### kind? > `readonly` `optional` **kind?**: `string` Optional kind safety hint: `shape`, `thing`, `assertion`, or `collection`. *** ### data? > `readonly` `optional` **data?**: `unknown` New shape-validated data payload. *** ### expectedVersion? > `readonly` `optional` **expectedVersion?**: `number` Optional optimistic-concurrency precondition. When supplied, the revise is applied only if the target is still at this version number. A stale value produces a `CONFLICT` error with `details.reason = 'expected_version_mismatch'`. Absent field preserves today's behavior exactly (opt-in). *** ### leaseId? > `readonly` `optional` **leaseId?**: `string` Optional read-lease token (#3625) returned by `thing.getWithLease`. When it matches the target's active lease the lease auto-releases on commit; a mismatch (or absence against a live lease) is rejected with `LEASE_UNAVAILABLE`. --- # ReviseOperation Revise operation accepted by `client.commit.apply`. Replaces the shape-validated data on an existing shape, thing, or assertion and creates a new version. `data` is a full replacement, not a patch — include every shape field, not just the ones that changed. Revise cannot deactivate a target: the `active` field is declared `never` at the type level, so passing `active: false` is a compile error. Use a [RetractOperation](/sdk-reference/interfaces/retractoperation/) to mark an entity inactive. ## See https://docs.warmhub.ai/writes/operations/#revise-operations ## Properties ### operation? > `optional` **operation?**: `"revise"` Operation discriminator. Set `revise` explicitly — an omitted discriminator normalizes as `add`, not `revise`. (Supplying `expectedVersion` without `operation: 'revise'` throws rather than silently dropping the precondition.) *** ### kind? > `optional` **kind?**: `"shape"` \| `"thing"` \| `"assertion"` \| `"collection"` Optional kind safety hint: `shape`, `thing`, `assertion`, or `collection`. *** ### name? > `optional` **name?**: `string` Target name to revise. Equivalent to `wref` for local paths. *** ### wref? > `optional` **wref?**: `string` Target wref to revise. Cross-repo references use `wh:org/repo/Shape/name`. *** ### data? > `optional` **data?**: `unknown` New shape-validated data payload. *** ### expectedVersion? > `optional` **expectedVersion?**: `number` Optional optimistic-concurrency precondition. When supplied, the revise is applied only if the target is still at this version number. A stale value produces a `CONFLICT` error with `details.reason = 'expected_version_mismatch'`. Absent field preserves today's behavior exactly (opt-in). *** ### active? > `optional` **active?**: `undefined` Type-level guard: revise cannot toggle activity. The field is declared `never` so passing `active: true` or `active: false` is a TypeScript error. To mark an entity inactive, use a [RetractOperation](/sdk-reference/interfaces/retractoperation/) instead. *** ### leaseId? > `optional` **leaseId?**: `string` Optional read-lease token (#3625) returned by `thing.getWithLease`. A matching token auto-releases the lease on commit; a mismatch (or absence against a live lease) is rejected with `LEASE_UNAVAILABLE`. --- # ValidationDiagnostic Client-side validation diagnostic returned by `OperationBuilder.validate`. ## Properties ### code > **code**: `string` Machine-readable diagnostic code. *** ### operationIndex > **operationIndex**: `number` Zero-based operation index, or `-1` for whole-builder diagnostics. *** ### message > **message**: `string` Human-readable diagnostic message. --- # ValidationResult Result returned by `OperationBuilder.validate`. ## Properties ### valid > **valid**: `boolean` Whether validation produced no errors. *** ### errors > **errors**: [`ValidationDiagnostic`](/sdk-reference/interfaces/validationdiagnostic/)[] Blocking diagnostics that prevent commit submission. *** ### warnings > **warnings**: [`ValidationDiagnostic`](/sdk-reference/interfaces/validationdiagnostic/)[] Non-blocking diagnostics for suspicious but possibly valid operations. --- # VerifiedCliCall ## Properties ### method > **method**: `'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'` HTTP method the request arrived on. *** ### installRepo > **installRepo**: `string` `/` slug of the install, from the install-repo header. Only cryptographically bound under the signing scheme (the HMAC covers the install repo). Under token schemes (bearer / api-key / basic) it is caller-asserted — verifying the request does NOT prove the caller is entitled to this install. When the install repo drives authorization, bind it (resolve the per-install secret keyed by this value and verify with it) or require the signing scheme. See docs/dev/component-rules.md → "What each scheme binds". *** ### args > **args**: `Record`\<`string`, `string` \| `number` \| `boolean` \| `undefined`\> Method args — query params for GET, parsed JSON body for POST/PUT/PATCH. --- # WarmHubClientOptions Options for constructing a `WarmHubClient`. ## Properties ### apiUrl? > `optional` **apiUrl?**: `string` Override the WarmHub API URL. Defaults to the public WarmHub API. *** ### fetch? > `optional` **fetch?**: \{(`input`, `init?`): `Promise`\<`Response`\>; (`input`, `init?`): `Promise`\<`Response`\>; \} Custom `fetch` implementation for non-standard runtimes or tests. #### Call Signature > (`input`, `init?`): `Promise`\<`Response`\> [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) ##### Parameters ###### input `URL` \| `RequestInfo` ###### init? `RequestInit` ##### Returns `Promise`\<`Response`\> #### Call Signature > (`input`, `init?`): `Promise`\<`Response`\> [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) ##### Parameters ###### input `string` \| `Request` \| `URL` ###### init? `RequestInit` ##### Returns `Promise`\<`Response`\> *** ### accessToken? > `optional` **accessToken?**: `AccessTokenProvider` Static token or sync/async token provider used for authenticated requests. *** ### auth? > `optional` **auth?**: `object` Authentication provider object. Prefer `auth.getToken` when integrating with an existing session system. #### getToken > **getToken**: () => `Promise`\<`string` \| `undefined`\> ##### Returns `Promise`\<`string` \| `undefined`\> *** ### functionLogs? > `optional` **functionLogs?**: `FunctionLogMode` Control replay of backend function log lines. Defaults to `off`. --- # @warmhub/sdk-ts ## Classes - [CliCallVerificationError](/sdk-reference/classes/clicallverificationerror/) - [WarmHubError](/sdk-reference/classes/warmhuberror/) - [WarmHubClient](/sdk-reference/classes/warmhubclient/) - [OperationBuilder](/sdk-reference/classes/operationbuilder/) - [PartialStreamSubmissionError](/sdk-reference/classes/partialstreamsubmissionerror/) - [AllStreamOperationsFailedError](/sdk-reference/classes/allstreamoperationsfailederror/) ## Interfaces - [VerifiedCliCall](/sdk-reference/interfaces/verifiedclicall/) - [WarmHubClientOptions](/sdk-reference/interfaces/warmhubclientoptions/) - [AddOp](/sdk-reference/interfaces/addop/) - [ReviseOp](/sdk-reference/interfaces/reviseop/) - [RetractOp](/sdk-reference/interfaces/retractop/) - [ValidationDiagnostic](/sdk-reference/interfaces/validationdiagnostic/) - [ValidationResult](/sdk-reference/interfaces/validationresult/) - [OperationBuilderOptions](/sdk-reference/interfaces/operationbuilderoptions/) - [AddOperation](/sdk-reference/interfaces/addoperation/) - [ReviseOperation](/sdk-reference/interfaces/reviseoperation/) - [RetractOperation](/sdk-reference/interfaces/retractoperation/) ## Type Aliases - [UndeclaredFieldsWarning](/sdk-reference/type-aliases/undeclaredfieldswarning/) - [ShapeValidatorResult](/sdk-reference/type-aliases/shapevalidatorresult/) - [CliCallVerificationFailureReason](/sdk-reference/type-aliases/clicallverificationfailurereason/) - [CliCallSecrets](/sdk-reference/type-aliases/clicallsecrets/) - [StreamAppendInput](/sdk-reference/type-aliases/streamappendinput/) - [StreamAppendResult](/sdk-reference/type-aliases/streamappendresult/) - [ErrorKind](/sdk-reference/type-aliases/errorkind/) - [OperationSubmitResult](/sdk-reference/type-aliases/operationsubmitresult/) - [SubscriptionCompatCreateInput](/sdk-reference/type-aliases/subscriptioncompatcreateinput/) - [SubscriptionCompatUpdateInput](/sdk-reference/type-aliases/subscriptioncompatupdateinput/) - [RenameResult](/sdk-reference/type-aliases/renameresult/) - [IndexedFieldEntry](/sdk-reference/type-aliases/indexedfieldentry/) - [IndexedFieldsReport](/sdk-reference/type-aliases/indexedfieldsreport/) - [OrgRole](/sdk-reference/type-aliases/orgrole/) - [RepoInfo](/sdk-reference/type-aliases/repoinfo/) - [GlobalSearchResultItem](/sdk-reference/type-aliases/globalsearchresultitem/) - [GlobalSearchOptions](/sdk-reference/type-aliases/globalsearchoptions/) - [Page](/sdk-reference/type-aliases/page/) - [ThingMetadata](/sdk-reference/type-aliases/thingmetadata/) - [ThingGetWithLease](/sdk-reference/type-aliases/thinggetwithlease/) - [ThingDetail](/sdk-reference/type-aliases/thingdetail/) - [LiveHandle](/sdk-reference/type-aliases/livehandle/) - [LiveRepoEvent](/sdk-reference/type-aliases/liverepoevent/) - [WherePredicate](/sdk-reference/type-aliases/wherepredicate/) - [FilterOptions](/sdk-reference/type-aliases/filteroptions/) - [SearchOptions](/sdk-reference/type-aliases/searchoptions/) - [CountOptions](/sdk-reference/type-aliases/countoptions/) - [RefsOptions](/sdk-reference/type-aliases/refsoptions/) - [WarmHubErrorDetails](/sdk-reference/type-aliases/warmhuberrordetails/) - [OperationBuilderOp](/sdk-reference/type-aliases/operationbuilderop/) - [Operation](/sdk-reference/type-aliases/operation/) - [ShapeFields](/sdk-reference/type-aliases/shapefields/) - [StreamContinuationState](/sdk-reference/type-aliases/streamcontinuationstate/) - [RetryPolicyOptions](/sdk-reference/type-aliases/retrypolicyoptions/) ## Variables - [CLI\_INSTALL\_REPO\_HEADER](/sdk-reference/variables/cli_install_repo_header/) - [CLI\_SIGNATURE\_HEADER](/sdk-reference/variables/cli_signature_header/) - [CLI\_TIMESTAMP\_HEADER](/sdk-reference/variables/cli_timestamp_header/) - [MAX\_CONTENT\_FIELD\_BYTES](/sdk-reference/variables/max_content_field_bytes/) - [CONTENT\_FIELD\_LIMIT\_ERROR](/sdk-reference/variables/content_field_limit_error/) - [SDK\_VERSION](/sdk-reference/variables/sdk_version/) - [DEFAULT\_API\_URL](/sdk-reference/variables/default_api_url/) ## Functions - [contentFieldLimitError](/sdk-reference/functions/contentfieldlimiterror/) - [validateAgainstShape](/sdk-reference/functions/validateagainstshape/) - [verifyCliCall](/sdk-reference/functions/verifyclicall/) - [normalizeWref](/sdk-reference/functions/normalizewref/) - [toWarmHubError](/sdk-reference/functions/towarmhuberror/) - [isWarmHubError](/sdk-reference/functions/iswarmhuberror/) - [isRetryable](/sdk-reference/functions/isretryable/) - [isConnectionError](/sdk-reference/functions/isconnectionerror/) - [connectionErrorMessage](/sdk-reference/functions/connectionerrormessage/) --- # CliCallSecrets > **CliCallSecrets** = `Partial`\<\{ `CLI_SIGNING_SECRET`: `string`; `CLI_BEARER_TOKEN`: `string`; `CLI_API_KEY`: `string`; `CLI_API_KEY_HEADER`: `string`; `CLI_BASIC_USERNAME`: `string`; `CLI_BASIC_PASSWORD`: `string`; \}\> & `Record`\<`string`, `string` \| `undefined`\> Secret bundle accepted by `verifyCliCall`. Pass your worker's full env (e.g. `c.env`) or just the CLI_* subset — the verifier looks only at the keys it knows. --- # CliCallVerificationFailureReason > **CliCallVerificationFailureReason** = `"missing-signature"` \| `"missing-timestamp"` \| `"invalid-timestamp"` \| `"missing-install-repo"` \| `"unsupported-method"` \| `"invalid-signature"` \| `"invalid-format"` \| `"expired"` \| `"invalid-body"` \| `"no-scheme-configured"` \| `"invalid-bearer"` \| `"invalid-api-key"` \| `"invalid-basic"` --- # CountOptions > **CountOptions** = [`FilterOptions`](/sdk-reference/type-aliases/filteroptions/) Options for `client.thing.count`. Same filter set as [FilterOptions](/sdk-reference/type-aliases/filteroptions/); pagination fields (`limit`, `cursor`) are ignored. --- # ErrorKind > **ErrorKind** = `CoreErrorKind` \| `string` & `object` Stable SDK error kinds plus backend domain codes that pass through as strings. Branch on this (or [WarmHubError.kind](/sdk-reference/classes/warmhuberror/#kind)) to recover from expected failures. [isRetryable](/sdk-reference/functions/isretryable/) returns the retryability column verbatim. ## Remarks Stable kinds (members of `CoreErrorKind`): - `BACKEND` — cause: server-side failure that did not carry a more specific domain code (tRPC error data has no `data.warmhub.code`, REST status mapped to the catch-all default — primarily 5xx that lack a body `error.code` — or an SDK transport throw such as a non-OK SSE response); retryable: true; action: retry with backoff and surface the message if it persists; if the underlying error is a deterministic local-adapter failure (e.g. a misconfigured custom `fetch`), fix the adapter rather than retrying; origin: SDK tRPC fallback, `httpStatusToWarmHubCode` default for unmapped HTTP statuses, SSE transport, unknown thrown values inside tRPC paths (custom-`fetch` adapter throws are normalized to `BACKEND` only on tRPC surfaces — on REST and `ping` surfaces the raw error escapes uncaught). REST responses with a parseable body surface as their pass-through `error.code` (typically `INTERNAL_ERROR`), and REST 4xx responses without a body code fall through to the HTTP-status mapping (`NOT_FOUND`, `UNAUTHENTICATED`, `FORBIDDEN`, `CONFLICT`, `VALIDATION_ERROR`, `RATE_LIMITED`), not `BACKEND`. - `NOT_FOUND` — cause: a referenced entity (repo, org, thing, shape, subscription, credential set, etc.) is missing or the caller cannot see it; retryable: false; action: verify the identifier the call site used (wref, slug, name) and escalate to a caller with read access if the entity should exist; origin: SDK reads across all surfaces, commit pipeline, REST query endpoints - `FORBIDDEN` — cause: caller is authenticated but lacks the required permission; retryable: false; action: request the missing permission or use a token with broader scope; origin: SDK + REST authorization middleware - `UNAUTHENTICATED` — cause: missing, expired, or invalid bearer token on a tRPC or REST call; retryable: false; action: mint a fresh PAT and update whichever client option the application wires it into (`auth.getToken` callback or `accessToken` — the SDK does not read `WH_TOKEN` or any env var directly); `wh auth login` only refreshes the CLI's on-disk credentials and does not propagate to a constructed SDK client; for export tickets, remint via the appropriate backend endpoint; origin: SDK + REST auth middleware. Live-ticket SSE auth failures surface as `BACKEND`, not `UNAUTHENTICATED`, because `openSse` maps every non-OK SSE response to `BACKEND`. - `VALIDATION_ERROR` — cause: input failed schema, shape, or content-length validation; retryable: false; action: fix the offending field per the error message; origin: commit pipeline, SDK writes, client-side content limits - `CONFLICT` — cause: write collides with current state (concurrent edit, name already in use); retryable: false; action: refetch the current state and reconcile before retrying the write; origin: commit pipeline, repo/org creation. Archived org / repo writes surface as the pass-through code `ARCHIVED`, not `CONFLICT`. - `RATE_LIMITED` — cause: caller exceeded the per-token or per-org request budget; retryable: true; action: wait [WarmHubError.retryAfter](/sdk-reference/classes/warmhuberror/#retryafter) seconds before retrying; origin: REST + tRPC throttling - `CANCELLED` — cause: caller aborted the request via `AbortSignal`; retryable: true; action: retry only if the caller still wants the result; origin: SDK `AbortError` mapping - `NETWORK` — cause: connection failed before the server produced a response (DNS, refused, timeout, fetch error); retryable: true; action: check connectivity to the configured `apiUrl` and retry with backoff; origin: SDK `fetchWithAuth` connection-error mapping Backend domain codes (e.g. `SHAPE_MISMATCH`, `WREF_UNRESOLVABLE`, `UNRESOLVED_TOKEN`, `ARCHIVED`, `INTERNAL_ERROR`) pass through unchanged on `code` / `kind`. Surface the `message` and `hint` verbatim and use the message to decide whether to retry — most pass-through codes are terminal, but the backend reuses some codes for transient failures (notably `INTERNAL_ERROR`, which the webhook-validation path emits with a `please retry` message when DNS or HEAD probes flake). See the [HTTP error catalog](/http-api/overview/#response-format) for HTTP-status mapping and the most common backend codes (the catalog is not exhaustive — rarer codes such as `UNRESOLVED_TOKEN` and `WREF_UNRESOLVABLE` are emitted by the backend but not yet tabulated). --- # FilterOptions > **FilterOptions** = `object` ## Properties ### shape? > `optional` **shape?**: `string` Filter to records of this shape name (for example `Player`). *** ### kind? > `optional` **kind?**: `"shape"` \| `"thing"` \| `"assertion"` \| `"collection"` Filter to records of this kind: `shape`, `thing`, `assertion`, or `collection`. *** ### about? > `optional` **about?**: `string` Filter assertions whose `about` target wref matches the supplied value. *** ### match? > `optional` **match?**: `string` Glob pattern matched against record names (for example `Player/*`). *** ### includeRetracted? > `optional` **includeRetracted?**: `boolean` Include retracted versions in the result set. *** ### limit? > `optional` **limit?**: `number` Maximum records returned per page. *** ### cursor? > `optional` **cursor?**: `string` Pagination cursor returned by the prior call. *** ### componentRef? > `optional` **componentRef?**: `string` Filter to records attributed to this installed component's `Org/Name` ref. *** ### excludeComponents? > `optional` **excludeComponents?**: `boolean` Exclude records attributed to any component. *** ### excludeInfraShapes? > `optional` **excludeInfraShapes?**: `boolean` Exclude internal infrastructure shapes from the result set. *** ### resolveCollections? > `optional` **resolveCollections?**: `boolean` Expand collection targets to their members in the result. *** ### where? > `optional` **where?**: [`WherePredicate`](/sdk-reference/type-aliases/wherepredicate/)[] Field-value WHERE predicates (ANDed). Each predicate must target a pinned/ready field for typed-index routing; unavailable or unindexable fields return a machine-readable field-index error. Up to 8 predicates per call. --- # GlobalSearchOptions > **GlobalSearchOptions** = `object` ## Properties ### limit? > `optional` **limit?**: `number` *** ### cursor? > `optional` **cursor?**: `string` --- # GlobalSearchResultItem > **GlobalSearchResultItem** = `object` A cross-org search hit (repo or component). GH-4383. ## Properties ### kind > **kind**: `"repo"` \| `"component"` *** ### orgName > **orgName**: `string` *** ### name > **name**: `string` *** ### description > **description**: `string` \| `null` --- # IndexedFieldEntry > **IndexedFieldEntry** = `object` A single entry in the indexed-fields report. ## Properties ### shapeName > **shapeName**: `string` *** ### fieldPath > **fieldPath**: `string` *** ### state > **state**: `string` Index states: building | ready | failed *** ### promotedAt > **promotedAt**: `string` \| `null` ISO-8601 date string or null — dates are serialized as strings over tRPC JSON wire. *** ### stateChangedAt > **stateChangedAt**: `string` ISO-8601 date string — dates are serialized as strings over tRPC JSON wire. *** ### failureReason > **failureReason**: `string` \| `null` *** ### backfillDone > **backfillDone**: `number` *** ### backfillTotal > **backfillTotal**: `number` \| `null` --- # IndexedFieldsReport > **IndexedFieldsReport** = `object` Report of all indexed fields for a repo, grouped by state. ## Properties ### ready > **ready**: [`IndexedFieldEntry`](/sdk-reference/type-aliases/indexedfieldentry/)[] *** ### building > **building**: [`IndexedFieldEntry`](/sdk-reference/type-aliases/indexedfieldentry/)[] *** ### failed > **failed**: [`IndexedFieldEntry`](/sdk-reference/type-aliases/indexedfieldentry/)[] *** ### other > **other**: [`IndexedFieldEntry`](/sdk-reference/type-aliases/indexedfieldentry/)[] --- # LiveHandle > **LiveHandle** = `object` Handle returned by live subscription helpers. ## Properties ### close > **close**: () => `void` #### Returns `void` *** ### closed > **closed**: `Promise`\<`LiveWatchResult`\> --- # LiveRepoEvent > **LiveRepoEvent** = `object` Raw SSE invalidation event from a watched repo. ## See https://docs.warmhub.ai/subscriptions/overview/#how-subscriptions-work ## Properties ### topic > **topic**: `"commit.applied"` \| `"action.updated"` *** ### affectedShapes > **affectedShapes**: `string`[] *** ### affectedThings > **affectedThings**: `string`[] *** ### affectedTargets > **affectedTargets**: `string`[] *** ### hasNewCommit > **hasNewCommit**: `boolean` --- # Operation > **Operation** = [`AddOperation`](/sdk-reference/interfaces/addoperation/) \| [`ReviseOperation`](/sdk-reference/interfaces/reviseoperation/) \| [`RetractOperation`](/sdk-reference/interfaces/retractoperation/) Commit operation accepted by `client.commit.apply`. Discriminated union over [AddOperation](/sdk-reference/interfaces/addoperation/), [ReviseOperation](/sdk-reference/interfaces/reviseoperation/), and [RetractOperation](/sdk-reference/interfaces/retractoperation/), keyed on `operation`. Inline array literals passed directly to `client.commit.apply` are contextually typed by the parameter, so `operation: "add"` stays narrowed and the call typechecks. Binding the array to a variable **without** a type annotation widens `operation` to `string`, so the variable no longer assigns to the `Operation[]` parameter. Either annotate the variable as `Operation[]` (contextually typed by the annotation) or use `satisfies Operation[]` to preserve the inferred literal types: ```ts // 1. Annotate the variable. const ops: Operation[] = [ { operation: "add", kind: "thing", name: "Sensor/temp-1", data: { x: 1 } }, { operation: "revise", name: "Sensor/temp-1", data: { x: 2 } }, ]; // 2. Or use `satisfies` to keep the inferred literal types. const ops2 = [ { operation: "add", kind: "thing", name: "Sensor/temp-1", data: { x: 1 } }, { operation: "revise", name: "Sensor/temp-1", data: { x: 2 } }, ] satisfies Operation[]; await client.commit.apply("acme", "world", "seed", ops); ``` ## See https://docs.warmhub.ai/sdk/write-methods/#typing-operation-arrays --- # OperationBuilderOp > **OperationBuilderOp** = [`AddOp`](/sdk-reference/interfaces/addop/) \| [`ReviseOp`](/sdk-reference/interfaces/reviseop/) \| [`RetractOp`](/sdk-reference/interfaces/retractop/) Operation queued by `OperationBuilder`. --- # OperationSubmitResult > **OperationSubmitResult** Result returned by successful commit submissions. --- # OrgRole > **OrgRole** = `"owner"` \| `"admin"` \| `"editor"` \| `"viewer"` Caller's role within an organization. Returned by `client.org.getCallerRole`, carried as `callerRole` on `client.org.listMembers` results, and accepted by `client.org.changeMemberRole` to set a member's role (only owners may promote to or demote from `'owner'`; the last owner cannot be demoted). Mirrors `orgRoleSchema` in `@warmhub/backend`; the static check below blocks any drift between this literal and the generated copy. ## See https://docs.warmhub.ai/auth/personal-access-tokens/#available-permissions --- # Page > **Page**\<`T`\> = `object` Cursor page envelope used by paginated SDK methods. `Page` is generic over the item type — always supply the type argument when annotating a variable that holds a page (`Page`, `Page`, etc.). Using the bare name `Page` fails with `TS2314: Generic type 'Page' requires 1 type argument(s)`. ## Example ```ts import type { Page, ThingItem } from "@warmhub/sdk-ts"; const page: Page = await client.thing.head("acme", "world", { limit: 50 }); for (const item of page.items) console.log(item.wref); if (page.nextCursor) { // pass page.nextCursor back as `cursor` on the next call } ``` ## Type Parameters ### T `T` ## Properties ### items > **items**: `T`[] *** ### nextCursor? > `optional` **nextCursor?**: `string` --- # RefsOptions > **RefsOptions** = `object` Options for `client.thing.refs` — wref-field reverse and forward lookups. ## See https://docs.warmhub.ai/sdk/read-semantics/#reference-queries ## Properties ### direction? > `optional` **direction?**: `"inbound"` \| `"outbound"` `inbound` finds records whose wref fields point at the target; `outbound` finds records the target points to. Defaults to `inbound`. *** ### fieldPath? > `optional` **fieldPath?**: `string` Narrow an inbound search to a specific wref field path on the source shape. *** ### limit? > `optional` **limit?**: `number` Maximum references returned per page. *** ### cursor? > `optional` **cursor?**: `string` Pagination cursor returned by the prior call. --- # RenameResult > **RenameResult** Result returned by in-place rename mutations: `client.shape.rename` and `client.thing.rename`. A rename patches the identity field directly and preserves the existing version history; no new version is created. The `renamed: true` discriminator distinguishes the success shape from a rejected request (which throws `WarmHubError`). ## See https://docs.warmhub.ai/data-modeling/naming-as-navigation/ --- # RepoInfo > **RepoInfo** = `object` Repository metadata returned by `client.repo.get`, `create`, `setDescription`, `setDisplayName`, `setVisibility`, `rename`, `update`, `archive`, and `unarchive`. ## Properties ### orgName > **orgName**: `string` *** ### name > **name**: `string` *** ### displayName > **displayName**: `string` *** ### description? > `optional` **description?**: `string` *** ### visibility > **visibility**: `"public"` \| `"private"` *** ### archivedAt? > `optional` **archivedAt?**: `number` *** ### createdAt > **createdAt**: `number` --- # RetryPolicyOptions > **RetryPolicyOptions** = `object` Caller-facing knobs for the SDK's auto-idempotency retry on transient first-chunk failures. Defaults are tuned for interactive and agent latency budgets: 3 attempts with exponential backoff capped at 8s, so a one-off flake recovers without user-perceptible latency. Override individual fields as needed. ## See https://docs.warmhub.ai/sdk/transient-retry/#tuning-retry ## Properties ### maxAttempts? > `optional` **maxAttempts?**: `number` *** ### baseDelayMs? > `optional` **baseDelayMs?**: `number` *** ### maxDelayMs? > `optional` **maxDelayMs?**: `number` --- # SearchOptions > **SearchOptions** = `Omit`\<[`FilterOptions`](/sdk-reference/type-aliases/filteroptions/), `"where"`\> & `object` Options for `client.thing.search`. Search does not support field-value `where` predicates in v1; use `thing.query` for typed structured filtering. When `about` is set or `resolveCollections` is true, result pages may be sparse — keep paginating until `nextCursor` is absent. ## Type Declaration ### about? > `optional` **about?**: `string` Restrict the search to assertions about this wref target. ### mode? > `optional` **mode?**: `"text"` \| `"vector"` \| `"hybrid"` Search mode: `text` (BM25), `vector` (semantic), or `hybrid` (combined). Defaults to `text`. ### resolveCollections? > `optional` **resolveCollections?**: `boolean` Expand collection members in the result; pages may be sparse when set. ### excludeInfraShapes? > `optional` **excludeInfraShapes?**: `boolean` Exclude internal infrastructure shapes from results. ## See https://docs.warmhub.ai/sdk/read-semantics/#search-modes --- # ShapeFields > **ShapeFields** = `Record`\<`string`, `unknown`\> Shape field definitions — the value side of a shape's data.fields --- # ShapeValidatorResult > **ShapeValidatorResult** = \{ `valid`: `true`; `warnings?`: [`UndeclaredFieldsWarning`](/sdk-reference/type-aliases/undeclaredfieldswarning/); \} \| \{ `valid`: `false`; `errors`: `string`[]; `warnings?`: [`UndeclaredFieldsWarning`](/sdk-reference/type-aliases/undeclaredfieldswarning/); \} --- # StreamAppendInput > **StreamAppendInput** Low-level stream append input accepted by `client.stream.append`. --- # StreamAppendResult > **StreamAppendResult** Low-level stream append result returned by `client.stream.append`. --- # StreamContinuationState > **StreamContinuationState** = `object` Continuation state for caller-managed streamed commit submissions. ## Properties ### streamId > **streamId**: `string` *** ### allocatedTokenRanges > **allocatedTokenRanges**: [`StreamAppendResult`](/sdk-reference/type-aliases/streamappendresult/)\[`"allocatedTokenRanges"`\] --- # SubscriptionCompatCreateInput > **SubscriptionCompatCreateInput** Input shape for `client.subscription.create`. Webhook subscriptions require `filterJson` and `webhookUrl`, and must identify a shape via `shapeName` or `filterJson.shape` (or a shape-lifecycle `{ kind: 'shape' }` filter). The cron kind was retired from public surfaces; legacy cron rows remain readable via SubscriptionInfo. ## Type Declaration ### kind > **kind**: `"webhook"` ### filterJson > **filterJson**: `Record`\<`string`, `unknown`\> ### sourceRepoRef? > `optional` **sourceRepoRef?**: `string` ## See https://docs.warmhub.ai/subscriptions/creating/ --- # SubscriptionCompatUpdateInput > **SubscriptionCompatUpdateInput** = `object` Input shape for `client.subscription.update`. `orgName`, `repoName`, and `name` identify the subscription; all other fields are optional and only the ones you provide get patched. ## See https://docs.warmhub.ai/subscriptions/managing/#update-subscription ## Properties ### orgName > **orgName**: `string` *** ### repoName > **repoName**: `string` *** ### name > **name**: `string` *** ### shapeName? > `optional` **shapeName?**: `string` *** ### filterJson? > `optional` **filterJson?**: `Record`\<`string`, `unknown`\> *** ### webhookUrl? > `optional` **webhookUrl?**: `string` *** ### fallbackWebhookUrl? > `optional` **fallbackWebhookUrl?**: `string` \| `null` *** ### allowTraceReentry? > `optional` **allowTraceReentry?**: `boolean` *** ### notifyOnSuccess? > `optional` **notifyOnSuccess?**: `boolean` *** ### workspacePolicy? > `optional` **workspacePolicy?**: `never` --- # ThingDetail > **ThingDetail** = `object` Full record shape for a single Thing returned by single-record SDK reads. Returned by `client.thing.get` and `client.thing.resolve`, the `items[]` element type for `client.thing.getMany`, the base of `client.thing.graph`, and the return of `client.repo.getReadme` and `getAgents` (nullable). Identity fields (`wref`, `name`, `kind`, `shape`/`shapeName`) are always populated. `data` carries the shape-validated payload when present; `active` is `false` for retracted reads. The `[key: string]: unknown` index signature is a forward-compat affordance — current callers should rely on the named fields. Note: list reads — `client.thing.head`, `query`, and `search` — return a thinner row shape (no `shape`, `validatedShape`, or `committerWref`); use `client.thing.get` to hydrate a full `ThingDetail` from a list result. ## See https://docs.warmhub.ai/data-modeling/things/ ## Indexable > \[`key`: `string`\]: `unknown` ## Properties ### wref > **wref**: `string` *** ### pinnedWref? > `optional` **pinnedWref?**: `string` *** ### name > **name**: `string` *** ### kind > **kind**: `string` *** ### shape? > `optional` **shape?**: `string` *** ### shapeName? > `optional` **shapeName?**: `string` *** ### validatedShape? > `optional` **validatedShape?**: `string` *** ### version > **version**: `number` *** ### active > **active**: `boolean` *** ### data? > `optional` **data?**: `unknown` *** ### synthesized? > `optional` **synthesized?**: `boolean` True on synthesized empty stubs from getReadme/getAgents when no row exists yet. *** ### aboutWref? > `optional` **aboutWref?**: `string` *** ### committerWref? > `optional` **committerWref?**: `string` *** ### createdByWref? > `optional` **createdByWref?**: `string` *** ### metadata? > `optional` **metadata?**: [`ThingMetadata`](/sdk-reference/type-aliases/thingmetadata/) --- # ThingGetWithLease > **ThingGetWithLease** = [`ThingDetail`](/sdk-reference/type-aliases/thingdetail/) & `object` Returned by `client.thing.getWithLease`. Everything `client.thing.get` returns, plus a `lease` block (ThingGetWithLease.lease) the holder echoes back on the subsequent `revise`/`retract` (`leaseId`) and on `client.thing.releaseLease`. The `version` and the `lease` come from a single backend snapshot, so the holder knows exactly which version it leased. ## Type Declaration ### lease > **lease**: `object` #### lease.id > **id**: `string` #### lease.expiresAt > **expiresAt**: `string` ## See https://docs.warmhub.ai/data-modeling/things/ --- # ThingMetadata > **ThingMetadata** = `object` Stable identity and creation timestamps embedded on every thing-like read result. `durableId` is a self-routing, self-verifying token (Crockford base32 over `repo_id + things.id + CRC-32C`) that remains identical before and after rename and across revise/retract. `thingCreatedAt` records the thing's birth date (stable across all mutations); `versionCreatedAt` is the timestamp of the current version (advances on each revise). History rows carry only `durableId` and `thingCreatedAt` — omit `versionCreatedAt`. Pass a bare `durableId` (optionally with `@vN`) wherever a wref is accepted on read surfaces; `orgName`/`repoName` may be omitted for self-routing reads. ## See https://docs.warmhub.ai/data-modeling/wrefs/#durable-ids ## Properties ### durableId > **durableId**: `string` *** ### thingCreatedAt > **thingCreatedAt**: `number` *** ### versionCreatedAt > **versionCreatedAt**: `number` --- # UndeclaredFieldsWarning > **UndeclaredFieldsWarning** = `object` ## Properties ### undeclaredFields > **undeclaredFields**: `string`[] *** ### undeclaredFieldsTruncated? > `optional` **undeclaredFieldsTruncated?**: `true` *** ### totalUndeclared? > `optional` **totalUndeclared?**: `number` --- # WarmHubErrorDetails > **WarmHubErrorDetails** = `NonNullable`\<`NonNullable`\<[`BackendStreamAppendResult`](/sdk-reference/type-aliases/streamappendresult/)\[`"results"`\]\[`number`\]\[`"error"`\]\>\[`"details"`\]\> Structured backend error details surfaced on `data.warmhub.details`. Derived from the generated wire type so it stays in lockstep with the backend's `WarmHubError.details` union. Branch on `details.reason` to recover the structured payload type-safely — e.g. read `currentVersion` after an `expected_version_mismatch` to retry against HEAD (#3624), or `leaseExpiresAt` after a `lease_held` to back off until the lease expires (#3625). --- # WherePredicate > **WherePredicate** A typed field-value WHERE predicate for `thing.query`, `thing.head`, and `thing.count`. --- # CLI_INSTALL_REPO_HEADER > `const` **CLI\_INSTALL\_REPO\_HEADER**: `"X-WarmHub-Install-Repo"` = `'X-WarmHub-Install-Repo'` Wire header names for component CLI dispatch — the single source of truth shared by the backend dispatcher (which sets them) and `verifyCliCall` (which reads them). The install repo always rides in [CLI\_INSTALL\_REPO\_HEADER](/sdk-reference/variables/cli_install_repo_header/); the signing scheme adds [CLI\_SIGNATURE\_HEADER](/sdk-reference/variables/cli_signature_header/) + [CLI\_TIMESTAMP\_HEADER](/sdk-reference/variables/cli_timestamp_header/). --- # CLI_SIGNATURE_HEADER > `const` **CLI\_SIGNATURE\_HEADER**: `"X-WarmHub-Signature"` = `'X-WarmHub-Signature'` --- # CLI_TIMESTAMP_HEADER > `const` **CLI\_TIMESTAMP\_HEADER**: `"X-WarmHub-Timestamp"` = `'X-WarmHub-Timestamp'` --- # CONTENT_FIELD_LIMIT_ERROR > `const` **CONTENT\_FIELD\_LIMIT\_ERROR**: `string` Human-readable explanation of the [MAX\_CONTENT\_FIELD\_BYTES](/sdk-reference/variables/max_content_field_bytes/) limit, suitable for appending to a field-specific validation error. States the byte cap and points callers to an external document store for large payloads. --- # DEFAULT_API_URL > `const` **DEFAULT\_API\_URL**: `"https://api.warmhub.ai"` = `'https://api.warmhub.ai'` Default WarmHub API URL used when a client is constructed without `apiUrl`. --- # MAX_CONTENT_FIELD_BYTES > `const` **MAX\_CONTENT\_FIELD\_BYTES**: `number` Maximum size, in bytes (UTF-8 encoded), allowed for a single content field value. WarmHub is not a document store: values larger than this are rejected at validation. Store large documents in S3, Box, Drive, or another document system and reference them from WarmHub instead. --- # SDK_VERSION > `const` **SDK\_VERSION**: `string` Version string for the installed SDK package. --- # Client Surfaces > Narrative map of the WarmHubClient surfaces, with links to the generated TypeDoc reference for method signatures and per-method details. The `WarmHubClient` organizes API calls into typed surfaces accessed as properties on the client instance. This page explains what each surface is for. The generated [WarmHubClient API reference](/sdk-reference/classes/warmhubclient/) is the reference for method signatures and per-method descriptions. ```ts import { WarmHubClient } from '@warmhub/sdk-ts' const client = new WarmHubClient({ auth: { getToken: async () => process.env.WH_TOKEN }, }) const orgs = await client.org.list() const head = await client.thing.head('acme', 'world') ``` All methods return promises. Most methods throw `WarmHubError` on failure. Methods that submit operations through the streaming write pipeline (`client.commit.apply()` and `OperationBuilder.commit()`) may instead throw [`PartialStreamSubmissionError`](/sdk/transient-retry/#partial-submissions) for ambiguous append outcomes or `AllStreamOperationsFailedError` when every submitted operation is rejected with per-op failure data. See [SDK Overview](/sdk/overview/) for how to create a token and full client setup. :::tip[Navigating the reference] The generated [WarmHubClient page](/sdk-reference/classes/warmhubclient/) renders all 15 surfaces and ~100 methods inline (~3,400 lines). Jump straight to a surface anchor (`#auth`, `#commit`, `#thing`, ...) rather than scrolling. Agents fetching the markdown can also use `/sdk-reference/classes/warmhubclient.md` and grep for the surface name; `/llms.txt` lists every method by surface. ::: ## Constructor Options Most callers construct the client with an `auth.getToken` provider or an `accessToken` value. `apiUrl` is only needed for non-default deployments, and `fetch` is mainly for custom runtimes or tests. See [WarmHubClientOptions](/sdk-reference/interfaces/warmhubclientoptions/) for the exact option type. ## client.auth Authentication helpers support browser sign-in flows, session sync, current-user lookup, and token diagnostics. Use this surface when an app needs to initialize browser auth or inspect the identity behind the current request. Reference: [WarmHubClient.auth](/sdk-reference/classes/warmhubclient/#auth). CLI counterpart: `wh auth`. ## client.access Access checks return booleans for repo- and org-scoped permissions. They are useful for frontend UI gating and service-side probes where the caller wants to ask "can this principal do this?" without performing the protected operation itself. Reference: [WarmHubClient.access](/sdk-reference/classes/warmhubclient/#access). No CLI counterpart — access checks are a frontend-gating utility. ## client.org Organization methods manage the top-level namespace for repositories, including creation, description changes, renames, archive state, membership, roles, and scoped member permissions. Member scope overrides replace the effective permission set for a matching resource, so include every permission the member should retain. `client.org.create()` requires an interactive browser session — a client authenticated with a personal access token can't create organizations. Personal organizations linked to a GitHub login cannot be renamed. Organization names are also checked against reserved public slugs such as `docs`, `api`, `login`, and `warmhub`. Reference: [WarmHubClient.org](/sdk-reference/classes/warmhubclient/#org). CLI counterpart: `wh org`. ## client.repo Repository methods cover lifecycle operations, metadata, visibility, soft delete, hard delete, content documents, and repo statistics. Use the [repo statistics guide](/sdk/repo-stats/) when choosing between dashboard list metadata, exact single-repo counts, and batch stats. The content helpers read and write the well-known `Content/Readme` and `Content/Agents` records described in [Content Shape](/data-modeling/content-shape/). The synthesized `Content/LlmsTxt` sitemap is read-only — `getLlmsTxt` returns the rendered markdown plus reference metadata, and there is no `setLlmsTxt`/`generateLlmsTxt` companion. Reference: [WarmHubClient.repo](/sdk-reference/classes/warmhubclient/#repo). CLI counterpart: `wh repo`. ## client.shape Shape methods manage schema definitions used to validate things and assertions. Shape create and revise calls are schema writes, while shape rename is applied in place: existing shape history is preserved and no new version is created. Reference: [WarmHubClient.shape](/sdk-reference/classes/warmhubclient/#shape). CLI counterpart: `wh shape`. ## client.thing Thing methods read repository records, histories, assertion targets, references, and search results. This surface also owns in-place thing renames; all other data mutations should go through `client.commit` or `OperationBuilder`. For read-modify-write cycles, `client.thing.getWithLease(org, repo, wref, { ttlMs? })` takes a short read lease on a thing so another caller's `revise` or `retract` of it is rejected with `LEASE_UNAVAILABLE` while you hold it: - **Requires write access** — unlike a plain read, a leased read is never anonymous. - **Fails fast if already leased** — if another caller holds an active lease, `getWithLease` itself throws `LEASE_UNAVAILABLE` (with `leaseExpiresAt` for backoff) rather than waiting. - **Returns** the leased `version` plus `lease.id` and `lease.expiresAt`; the lease also expires automatically at that deadline. - **Write under the lease** by passing `lease.id` as the `leaseId` on the subsequent `revise`/`retract`; it auto-releases on a successful or no-op write. - **Release early** with `client.thing.releaseLease(org, repo, wref, leaseId)` if you decide not to write. See [Write Methods](/sdk/write-methods/#read-leases) for the `leaseId` operation field. Thing read results include a `metadata` envelope with the thing's stable `durableId` and its creation timestamps. Read filters, glob `match` behavior, reference queries, search modes, the metadata envelope, and anonymous pagination limits are covered in [Read Semantics](/sdk/read-semantics/). Reference: [WarmHubClient.thing](/sdk-reference/classes/warmhubclient/#thing). CLI counterpart: `wh thing`. ## client.commit Commit methods are the high-level write path. `client.commit.apply(...)` submits add, revise, and retract operations as a single WarmHub commit, returns per-operation results, and supports chunking, component attribution, idempotent add behavior, and retry configuration. Use [Write Methods](/sdk/write-methods/) to choose between raw operation arrays and the builder API. Use [Transient Retry](/sdk/transient-retry/) for retry and partial-submission behavior. Reference: [WarmHubClient.commit](/sdk-reference/classes/warmhubclient/#commit). CLI counterpart: `wh commit`. ## client.stream The stream surface is the low-level append API. Most SDK users should prefer `client.commit.apply(...)` or `OperationBuilder`; use `client.stream.append(...)` only when you already have backend stream operations, a stream ID, and continuation token state. Reference: [WarmHubClient.stream](/sdk-reference/classes/warmhubclient/#stream). No CLI counterpart — use `wh commit` for normal writes. ## client.component Component methods inspect installed WarmHub components: packages that add shapes, subscriptions, credentials, and seed data to a repository. The nested `client.component.registry` sub-surface drives the backend-mediated install flow that powers registered installs, and `client.component.installSystem(...)` installs allowlisted bundled system components such as `com.warmhub.identity` into an existing repo. Most component lifecycle commands — `init`, local/GitHub `install`, `search`, `view`, `validate`, `update`, `doctor`, `teardown` — are managed through the `wh component` CLI. Reference: [WarmHubClient.component](/sdk-reference/classes/warmhubclient/#component). CLI counterpart: `wh component` (broader surface; see note above). ## client.subscription Subscription methods create and manage webhook subscriptions scoped to a repository. The create input covers delivery URLs, fallback delivery, success notifications, source-repo forwarding, trace reentry, and component attribution; update covers a narrower mutable subset (see the API reference for the exact fields). Credential binding attaches a named credential set to outbound webhook delivery. See [Credential binding](/subscriptions/credentials/#credential-binding) for the delivery-auth walkthrough, and [Component Identity](/sdk/component-identity/) for `componentId` rules shared with commit writes. Reference: [WarmHubClient.subscription](/sdk-reference/classes/warmhubclient/#subscription). CLI counterpart: `wh sub`. ## client.action Action methods are low-level primitives for subscription consumers: leases, live delivery feeds, run listings, attempt listings, and repo-scoped notifications. Webhook handlers and custom consumers use this surface to coordinate processing. The `client.actions` property is an alias for this surface. Reference: [WarmHubClient.action](/sdk-reference/classes/warmhubclient/#action). CLI counterpart: `wh sub log` covers `action.liveFeed`; lease and lifecycle primitives have no CLI surface. ## client.token Token methods create, list, inspect, and revoke personal access tokens for the authenticated user. For scope syntax, rotation, and CI usage, see [Personal Access Tokens](/auth/personal-access-tokens/). Reference: [WarmHubClient.token](/sdk-reference/classes/warmhubclient/#token). CLI counterpart: `wh token`. ## client.credential Credential methods manage named secret sets used by subscription webhooks and component integrations. Sets are scoped at creation: org-scoped sets can be granted across repos in an organization, repo-scoped sets stay with one repo. The `client.credentials` property is an alias for this surface. Reference: [WarmHubClient.credential](/sdk-reference/classes/warmhubclient/#credential). CLI counterpart: `wh credential`. ## client.live Live methods open server-sent event streams for repository invalidations. Higher-level helpers re-run their underlying queries after invalidation and pass refreshed results to the callback. The raw subscribe method forwards invalidation metadata without re-querying. Reference: [WarmHubClient.live](/sdk-reference/classes/warmhubclient/#live). No CLI counterpart — SSE streams are SDK-only. ## client.diagnostics Diagnostic methods check backend health and retrieve compatibility information such as API version, minimum supported SDK version, and feature flags. Reference: [WarmHubClient.diagnostics](/sdk-reference/classes/warmhubclient/#diagnostics). CLI counterpart: `wh doctor` covers similar health and capability probes. ## OperationBuilder `OperationBuilder` builds commit operation batches incrementally, runs local validation, optionally checks data against known shapes, and submits through the same stream path as `client.commit.apply(...)`. After a successful commit, the builder is sealed and cannot be reused. Reference: [OperationBuilder](/sdk-reference/classes/operationbuilder/), [AddOp](/sdk-reference/interfaces/addop/), [ReviseOp](/sdk-reference/interfaces/reviseop/), [RetractOp](/sdk-reference/interfaces/retractop/). CLI counterpart: `wh commit submit` (file-driven equivalent of the builder pattern). ## Linking to methods Surface anchors on the generated reference page (`#auth`, `#commit`, `#thing`, ...) are stable. Per-method anchors (`#list`, `#list-1`, `#list-2`, ...) are TypeDoc reflection-order dedup — they shift if `WarmHubClient` properties are reordered or new methods land between existing ones. Link to surface anchors and let readers scroll to the method, or fetch the `.md` and search by method name. ## Next Steps - [WarmHubClient API reference](/sdk-reference/classes/warmhubclient/) - generated reference for the main client class - [Write Methods](/sdk/write-methods/) - choose the write API that fits the call site - [Read Semantics](/sdk/read-semantics/) - filters, match patterns, search, refs, and pagination behavior - [Repo Statistics](/sdk/repo-stats/) - exact counts vs dashboard metadata --- # Component Identity > How componentId attribution works across SDK commit writes and subscription creation. Some SDK calls accept `componentId` to attribute records or subscriptions to an installed WarmHub [component](/components/overview/) — the installable-package concept. Component identity is used by component-owned shapes, subscriptions, credentials, and seed data. ## Commit Writes `client.commit.apply(...)` accepts `componentId` in its options object. This is the high-level write entrypoint that carries component identity end-to-end. ```ts await client.commit.apply('acme', 'world', 'install component seed', ops, { componentId: 'component_01J8...', }) ``` `OperationBuilder.commit(...)` does **not** accept `componentId` today. To attribute a batch to a component, route those call sites through `client.commit.apply(...)` with `componentId` in the options object — build the operations directly as a plain `Operation[]` rather than through the builder. The low-level `client.stream.append(input)` surface also accepts `componentId` on the input object — use it only when you already manage stream IDs and allocated tokens directly (for example, installer continuations). Most callers should prefer `commit.apply` or `OperationBuilder`. User tokens can set `componentId` for components installed by that user. Tokens with the [`org:configure`](/auth/personal-access-tokens/#scopes) scope can claim any installed component in the organization. Action tokens derive component identity from the running subscription. Explicit mismatches reject with `FORBIDDEN`. The CLI `wh commit submit` command doesn't currently expose `componentId`. The MCP `warmhub_commit_submit` tool accepts it directly. ## Subscriptions Subscription creation accepts `componentId` when the subscription should belong to an installed component. ```ts await client.subscription.create({ orgName: 'acme', repoName: 'world', name: 'location-hook', kind: 'webhook', shapeName: 'Location', filterJson: { shape: 'Location' }, webhookUrl: 'https://example.com/hook', componentId: 'component_01J8...', }) ``` Subscriptions require a `filterJson` object describing the operations to match, plus a shape binding from either `shapeName` or `filterJson.shape` (shape-lifecycle subscriptions instead supply a shape-only filter such as `{ "kind": "shape" }` and omit `shapeName`). See the [WarmHubClient API reference](/sdk-reference/classes/warmhubclient/#subscription) for the full input shape. The same authority rules apply: installer-owned components are available to that user, tokens with the [`org:configure`](/auth/personal-access-tokens/#scopes) scope can claim any installed component in the organization, and action-originated requests derive component identity from their action context. ## Related Filters Read surfaces such as shape and thing listings can filter by component ownership or hide component-owned infrastructure records. See [Read Semantics](/sdk/read-semantics/) and the [WarmHubClient API reference](/sdk-reference/classes/warmhubclient/) for exact option names. --- # SDK Overview > Install the TypeScript SDK, create a client, and browse the available SDK surfaces. The `@warmhub/sdk-ts` package is the typed TypeScript client for WarmHub. It provides promise-based methods for managing organizations and repositories, reading and writing repository data, and accessing the rest of the WarmHub surface — auth, access checks, commits, components, subscriptions, actions, tokens, credentials, diagnostics, and live feeds. WarmHub data is modeled as [things](/data-modeling/things/) (versioned named entities), [assertions](/data-modeling/assertions/) (claims about things), and [shapes](/data-modeling/shapes/) (schemas that define the structure of both). If those terms are new, skim [Core Concepts](/get-started/core-concepts/) before continuing. ## Installation See the [Quickstart](/get-started/quickstart/#connect-via-sdk) for full install instructions. In short: ```bash npm install @warmhub/sdk-ts ``` ## Client Setup Install the [WarmHub CLI](/get-started/quickstart/#connect-via-cli), then create a [personal access token](/auth/personal-access-tokens/) and export it as `WH_TOKEN`: ```bash wh auth login wh token create --name my-app export WH_TOKEN=eyJhbGciOi... ``` Then create a client: ```ts import { WarmHubClient } from '@warmhub/sdk-ts' const client = new WarmHubClient({ auth: { getToken: async () => process.env.WH_TOKEN }, }) ``` ### Client Options | Option | Type | Description | |--------|------|-------------| | `auth.getToken` | `() => Promise` | Token acquisition hook — required for authenticated endpoints | | `accessToken` | `string \| () => string \| undefined \| Promise` | Static token or sync/async provider (alternative to `auth.getToken`) | | `apiUrl` | `string` | Override the API URL (defaults to `https://api.warmhub.ai`) | | `fetch` | `typeof fetch` | Custom fetch implementation | | `functionLogs` | `'raw' \| 'off'` | Server function log forwarding (defaults to `'off'`) | For a comparison of when to use the SDK vs the CLI or MCP, see [How to Get Started](/get-started/what-is-warmhub/#how-to-get-started). ## API Reference The SDK groups calls into typed client surfaces such as `client.repo`, `client.thing`, and `client.commit`. The generated [WarmHubClient API reference](/sdk-reference/classes/warmhubclient/) is the reference for method signatures and per-method descriptions. Use [Client Surfaces](/sdk/client/) for a narrative map of the surfaces and the SDK concept pages for behavior shared across methods, such as [read semantics](/sdk/read-semantics/), [repo statistics](/sdk/repo-stats/), [commit retries](/sdk/transient-retry/), and [component identity](/sdk/component-identity/). ### Permission Checks The `client.access` surface has lightweight permission checks for frontend UI gating. They return `boolean` on success. Like all SDK methods, they throw `WarmHubError` on network, backend, or auth failures. ```ts const canWrite = await client.access.checkRepoPermission( 'acme', 'world', 'repo:write', ) ``` These checks use the same credentials as the rest of the client; auth and network errors surface the same way as other SDK methods. ## Error Handling Most SDK methods throw `WarmHubError` on failure. Streamed writes (`client.commit.apply()` and `OperationBuilder.commit()`) may instead throw `PartialStreamSubmissionError` for ambiguous append outcomes or `AllStreamOperationsFailedError` when every submitted operation is rejected with per-op failure data — see [Streaming Write Failures](/sdk/transient-retry/). Use `isWarmHubError()` and `isRetryable()` for error handling: ```ts import { isRetryable, isWarmHubError } from '@warmhub/sdk-ts' try { await client.repo.get('acme', 'world') } catch (err) { if (isWarmHubError(err) && err.kind === 'NOT_FOUND') { // handle missing repo } if (isRetryable(err)) { // safe to retry (NETWORK, CANCELLED, BACKEND, RATE_LIMITED) } throw err } ``` See the [`ErrorKind` reference](/sdk-reference/type-aliases/errorkind/) for the full per-kind cause, retryability, and corrective action. Common error kinds: `NOT_FOUND`, `VALIDATION_ERROR`, `CONFLICT`, `UNAUTHENTICATED`, `FORBIDDEN`, `RATE_LIMITED`, `CANCELLED`, `NETWORK`, `BACKEND`. Backend domain codes can also pass through unchanged in `err.code` and `err.kind`, including `UNRESOLVED_TOKEN`, `ARCHIVED`, `SHAPE_MISMATCH`, and `WREF_UNRESOLVABLE`. ## Next Steps - [SDK Quickstart](/get-started/quickstart/#connect-via-sdk) — install, create a client, and run your first query - [Client Surfaces](/sdk/client/) — narrative map of the main client surfaces - [WarmHubClient API reference](/sdk-reference/classes/warmhubclient/) — auto-generated TypeDoc for the main `@warmhub/sdk-ts` client class - [Write Methods](/sdk/write-methods/) — choose the write API that fits the call site - [Transient Retry](/sdk/transient-retry/) — retry and partial-submission behavior for SDK writes - [Read Semantics](/sdk/read-semantics/) — filters, glob match, search modes, batch reads, anonymous pagination - [Repo Statistics](/sdk/repo-stats/) — exact counts vs dashboard metadata vs per-shape breakdowns - [Component Identity](/sdk/component-identity/) — how `componentId` attribution works for component-installed records - [CLI Quickstart](/get-started/quickstart/#connect-via-cli) — terminal-first approach - [MCP Server](/agent-integration/mcp-server/) — agent integration via Model Context Protocol --- # Read Semantics > SDK read filters, glob match behavior, reference queries, search modes, batch reads, and anonymous pagination limits. SDK read methods share a few behavior patterns across `client.thing`, `client.shape`, and repository list calls. Use the generated [WarmHubClient API reference](/sdk-reference/classes/warmhubclient/) for exact signatures. ## Match Patterns The `match` option accepts a glob pattern that filters against full [wrefs](/data-modeling/wrefs/) such as `Location/cave`. - `*` matches a single path segment. - `**` matches zero or more path segments. ```ts await client.thing.head('acme', 'world', { match: 'Location/dungeon/*', }) await client.thing.about('acme', 'world', 'Location/cave', { match: 'Observation/**', }) ``` Reads with a `match` filter may lag briefly after a write while WarmHub updates read indexes. Subsequent reads after the index catches up will see the new state. ## Batch Reads `client.thing.getMany(...)` accepts any number of wrefs and auto-chunks requests above the backend's 500-wref transport cap. The result preserves duplicates: each duplicate counts toward the requested count and produces a duplicate result or missing entry. Dedupe upstream when you want one row per unique wref. The optional top-level version pins every requested wref that does not already include `@vN` or `@HEAD`. Per-wref pins remain intact. Missing and inaccessible refs are reported in `missing` rather than throwing per item. With `includeRetracted: true`, retracted records can appear in `items`; without it, retracted refs land in `missing`. Use `chunkSize` when you want smaller backend requests for memory or latency shaping; values above 500 are clamped to the transport cap. Use `chunkConcurrency` to run multiple chunks in parallel while keeping the merged result order deterministic. ## Read Result Metadata Thing read results include a `metadata` object alongside the record's data: | Field | What it tells you | |-------|-------------------| | `durableId` | A stable id for the thing that never changes across rename, revision, or retraction. See [Durable Ids](/data-modeling/wrefs/#durable-ids). | | `thingCreatedAt` | When the thing was first created, as a Unix timestamp in milliseconds. Stable across later revisions and renames. | | `versionCreatedAt` | When the current version was created, as a Unix timestamp in milliseconds. | Because `thingCreatedAt` is the original creation time, you can order things by when they were first created from a single read — without walking each thing's history. History rows carry `durableId` and `thingCreatedAt`. ## Reference Queries `client.thing.refs(...)` queries wref-typed fields. - Inbound refs find records whose wref fields point to the supplied wref. - Outbound refs find records the supplied record points to. - Inbound queries can be narrowed to a field path. Use `client.thing.about(...)` for assertions about a thing or collection target. Use `refs(...)` when you specifically need field-level wref links. ## Search Modes `client.thing.search(...)` supports text, vector, and hybrid modes. | Mode | Behavior | |------|----------| | `text` | Full-text search and the full read-filter set | | `vector` | Embedding-based semantic search | | `hybrid` | Combined text and vector search | See the [WarmHubClient API reference](/sdk-reference/classes/warmhubclient/) for the per-mode option set. When searching with an assertion target or collection resolution, pages may be sparse. Keep paginating until `nextCursor` is absent. ## Pagination Paginated methods follow a shared cursor pattern: the request accepts `limit` and `cursor`, and the response includes items plus an optional `nextCursor`. ```ts const page1 = await client.repo.listPage('acme', { limit: 10 }) if (page1.nextCursor) { const page2 = await client.repo.listPage('acme', { cursor: page1.nextCursor, limit: 10, }) } ``` Some methods return a generic page shape and others return method-specific envelopes. The cursor pattern is the same; methods that do not paginate return their full result set in one call. ## Iteration For "scan everything matching this filter" reads, prefer the SDK iterator helpers over manual cursor loops: ```ts for await (const item of client.thing.queryIter('acme', 'world', { shape: 'Location', limit: 500, })) { console.log(item.wref) } ``` `thing.refsIter`, `thing.queryIter`, `thing.headIter`, and `thing.aboutIter` handle the cursor internally and yield one item at a time. Their `*All` variants materialize the full scan into an array and accept `max` to prevent accidental unbounded reads: ```ts const refs = await client.thing.refsAll('acme', 'world', 'Location/cave', { direction: 'inbound', max: 10_000, }) ``` The underlying paginated methods remain available when you need explicit page envelopes, `nextCursor` storage, or custom page scheduling. ## Anonymous Pagination Caps Tokenless clients reading public repositories have narrower paging on shared read procedures: - `limit` is capped at 25 items per page. - Omitted `limit` defaults to 25. - The page size is bound to the cursor. Follow-up requests must omit `limit` or pass the same value used to mint the cursor. - Anonymous pagination stops after 2 pages for repository list pages, shape history, thing HEAD, thing about, thing query, and wref-form thing history. - Shape- or about-filtered thing history allows only page 0 anonymously. The SDK surfaces these boundaries as `WarmHubError` kinds such as `VALIDATION_ERROR` or `UNAUTHENTICATED`. REST query endpoints can collapse equivalent deny paths to opaque `404` responses to keep repository existence private. Authenticated callers see no anonymous pagination narrowing. --- # Repo Statistics > Choose between repo list metadata, repository stats, batch stats, configure stats, and shape instance counts. WarmHub exposes several repository statistics surfaces because dashboards, billing, settings pages, and shape summaries need different levels of detail. ## Dashboard List Metadata `client.repo.listPage(...)` returns repository list items with dashboard-oriented metadata: - `total` is the exact active count across shapes, things, and assertions. - `byKind` breaks that exact count into `shape`, `thing`, and `assertion`. - `lastWriteAt` is an activity timestamp, not a strict write-history cursor. - `hasErrors` is true when the repository has action runs in a terminal failure state. Use this for repository tiles and organization dashboards. ## Exact Single-Repo Stats `client.repo.getStats(...)` returns an exact active-item count for one repository. The `total` field sums active shapes, things, and assertions. The per-kind breakdown separates those categories, and the per-shape map counts active things by shape. Assertions are not included in the per-shape map. Use this for billing, quotas, or repository detail pages that need the per-shape map. ## Batch Stats `client.repo.getStatsBatch(...)` returns exact totals for up to 100 repositories in one request. Use it when callers need exact counts for a caller-provided set of repositories. The batch result does not include the per-shape map; call `getStats(...)` for any repository where you need that detail. ## Configure Stats `client.repo.getConfigureStats(...)` returns settings-page counts such as the number of subscriptions attached to a repository. Use it before delete or visibility-change flows when the UI needs to explain what repository configuration will be affected. ## Shape Instance Counts `client.repo.getShapeInstanceCounts(...)` returns thing and assertion counts grouped by shape. The server computes these totals directly, so callers do not need to page through repository contents to build a shape summary UI. --- # Transient Retry > Retry and partial-submission behavior for SDK commit writes and OperationBuilder submissions. `client.commit.apply(...)` and `OperationBuilder.commit(...)` automatically retry some ambiguous first-chunk failures: timeouts, dropped connections, and server 5xx responses on the very first stream append, where no operation has been acknowledged yet. In plain terms: simple single-op writes retry automatically; multi-op writes and writes using advanced stream options surface ambiguous failures as `PartialStreamSubmissionError`. If the backend deterministically rejects every submitted operation, the SDK throws `AllStreamOperationsFailedError` with the per-op failure ledger on `error.result` and `error.operations`. Auto-retry is conservative — it only fires when **every** condition holds: - The first chunk contains exactly one operation (multi-op chunks are rejected because the backend applies each op in its own transaction; a mid-chunk failure may have committed an arbitrary prefix). - No operation in the chunk references `$N` / `#N` allocation tokens (a fresh `streamId` on retry would resolve those tokens to a different identity than the first attempt may have landed). - No `streamId` or non-empty `allocatedTokens` was supplied (manual-resume mode bypasses retry). When auto-retry fires, the SDK mints a fresh `streamId` and re-issues the operation. **The retry does not prove the original attempt did not land** — definite rejections on the retry are wrapped as `PartialStreamSubmissionError` so the "inspect repo state before continuing" guarantee survives. Callers needing safe retry should make `add` operations idempotent with `skipExisting: true` — pass it via `client.commit.apply`'s options or set it on each `OperationBuilder.add(...)` op directly. For callers, a successful retry behaves like a single successful submission: the method resolves with the server's operation results. ## Tuning Retry Pass `retry: false` to disable automatic retry: ```ts await client.commit.apply(org, repo, message, operations, { retry: false }) ``` Pass a [`RetryPolicyOptions`](/sdk-reference/type-aliases/retrypolicyoptions/) value to override defaults: ```ts await client.commit.apply(org, repo, message, operations, { retry: { maxAttempts: 4, baseDelayMs: 100, maxDelayMs: 1000, }, }) ``` `OperationBuilder.commit(...)` accepts the same `retry` option. ## Partial Submissions The SDK throws [`PartialStreamSubmissionError`](/sdk-reference/classes/partialstreamsubmissionerror/) whenever an ambiguous or partial append outcome cannot be reduced to a clean success or definite failure. This covers both: - **Mid-stream failures.** A multi-chunk submission whose first chunks acknowledged but a later chunk failed ambiguously. `completedOperations` lists the operations the SDK knows landed. - **First-chunk ineligible-for-retry failures.** The first append fails ambiguously, but auto-retry is disabled (`retry: false`) or ineligible (multi-op first chunk, token-using ops, or manual-resume mode). `completedOperations` is empty, but the failed append may still have landed server-side. In every case, the failed append may have landed server-side, so inspect repository state before submitting additional operations. Definite request-level rejections propagate as `WarmHubError` with the original kind and code. Examples include `UNAUTHENTICATED`, `FORBIDDEN`, `VALIDATION_ERROR`, `RATE_LIMITED`, and `UNRESOLVED_TOKEN`. Definite operation-level rejections can also resolve as mixed partial results; when every operation is rejected, they throw `AllStreamOperationsFailedError` so callers can still inspect each failed op. ## Practical Rule Retry automatically when the SDK returns success or when a normal retryable `WarmHubError` is safe for your workload. When you catch `PartialStreamSubmissionError`, treat repository state as the source of truth before deciding what to submit next. When you catch `AllStreamOperationsFailedError`, use `error.operations` to fix the rejected inputs before retrying. --- # Write Methods > Choose between client.commit.apply and OperationBuilder for SDK writes, and understand operation inputs and results. WarmHub has one write path: every mutation lands through the same operation pipeline. The TypeScript SDK gives you two ways to submit operations: - `client.commit.apply(...)` for call sites that already have operation arrays. - `OperationBuilder` for call sites that benefit from incremental construction and local validation. Both submit add, revise, and retract operations through the same backend stream-append surface and return the same per-operation result shape. ## Choosing an API | Need | Prefer | |------|--------| | Submit a small operation array directly | `client.commit.apply(...)` | | Build operations across several branches or helper functions | `OperationBuilder` | | Run client-side preflight checks before any server call (and shape-data validation when constructed with `{ shapes }`) | `OperationBuilder` | | Preserve a raw operation payload from another system | `client.commit.apply(...)` | | Chain add, revise, and retract calls fluently | `OperationBuilder` | | Resume caller-managed stream chunks | Either, with advanced stream options | ```ts await client.commit.apply('acme', 'world', 'seed cave', [ { operation: 'add', name: 'Location/cave', data: { x: 0, y: 0 }, }, ]) ``` ```ts import { OperationBuilder } from '@warmhub/sdk-ts' const builder = new OperationBuilder() builder.add({ name: 'Location/cave', data: { x: 0, y: 0 } }) builder.add({ name: 'Location/forest', data: { x: 5, y: 3 } }) const check = builder.validate() if (!check.valid) { throw new Error(check.errors.map((e) => e.message).join('; ')) } await builder.commit({ client, orgName: 'acme', repoName: 'world', message: 'seed locations', }) ``` The builder has no `.build()` step and is not itself a promise — `await builder` does nothing. `builder.commit({...})` is the only finalizer; it validates, submits, and seals the builder so calling `builder.commit(...)` a second time throws. ## Typing Operation arrays `Operation` is a discriminated union over `AddOperation`, `ReviseOperation`, and `RetractOperation`, keyed on the `operation` field. When the array is passed inline to `client.commit.apply`, the parameter type narrows the literal for you and the call typechecks with no extra ceremony. When you bind the array to a variable first **without** a type annotation, TypeScript widens `operation: "add"` to `operation: string`, and the variable no longer assigns to the `Operation[]` parameter. Two equivalent fixes — pick whichever fits the call site: ```ts import type { Operation } from "@warmhub/sdk-ts"; // 1. Annotate the variable — contextually typed by the annotation. const operations: Operation[] = [ { operation: "add", kind: "thing", name: "Sensor/temp-1", data: { x: 1 } }, { operation: "revise", name: "Sensor/temp-1", data: { x: 2 } }, ]; // 2. Or `satisfies` — preserves the inferred literal types instead of // widening them to `Operation`. const operations2 = [ { operation: "add", kind: "thing", name: "Sensor/temp-1", data: { x: 1 } }, { operation: "revise", name: "Sensor/temp-1", data: { x: 2 } }, ] satisfies Operation[]; await client.commit.apply("acme", "world", "seed", operations); ``` `as const` works too, at the cost of marking the whole array readonly. ## Kind Inference When `kind` is omitted, both write surfaces — `client.commit.apply` and `OperationBuilder` — infer it with the same shared rule, applied in order: - `about` present -> assertion - `type` and `members` both present -> collection - one-segment name (e.g. `game-state`) -> thing - two-segment `Shape/name` -> thing - three or more segments -> assertion The rule is identical across surfaces; only where an invalid result is rejected differs. `OperationBuilder` rejects at `.add()`/`.revise()` time (a one-segment thing name fails the local-path preflight; an inferred assertion without `about` fails immediately rather than at `commit()`). `client.commit.apply` rejects while normalizing the operation or server-side, since the backend requires an explicit `kind` on every operation and never infers. Shape adds always require explicit `kind: 'shape'` — a bare shape name (e.g. `Player`) is otherwise inferred as a thing and rejected as a thing-path violation. Use `kind: 'thing'` for hierarchical thing names such as `GameState/round-1/state` if you need to keep them on the thing path despite the segment count. Supplying only one of `type`/`members` is rejected on both surfaces rather than silently dropping the collection fields. The same name-segmentation rule applies to kind-less `revise` operations. The `wh` CLI shorthand is a separate, explicit-kind surface: it always sends a `kind` (defaulting to `thing`, or `assertion` when `--about` is supplied), so SDK inference never applies to CLI-built operations. See the [write submit deep dive](/cli-reference/write-submit-deep-dive/) for the CLI's defaulting rules. [Wref](/data-modeling/wrefs/) shape constraints are enforced server-side. `OperationBuilder` validates field types and most local constraints, but it cannot prove that a referenced thing belongs to the required shape until the operation reaches the server. ## Version preconditions `revise` accepts an optional `expectedVersion` — the write applies only if the target (thing, shape, or assertion) is still at that version, otherwise it is rejected with a `CONFLICT` (`details.reason: "expected_version_mismatch"`). Use it for read-modify-write safety when you don't need to hold an exclusive lease. See [Conditional Operations](/writes/operations/#conditional-operations) for an overview of all three conditional write patterns across surfaces. ## Read leases `revise` and `retract` operations accept an optional `leaseId` to write under a read lease acquired with [`client.thing.getWithLease`](/sdk/client/#clientthing) — a leased read requires write access and is never an anonymous read. The field is per-operation, so the `client.commit.apply` signature is unchanged; `add` is never lease-gated (a new thing has no prior version to lease). ```ts const leased = await client.thing.getWithLease("acme", "world", "Player/alice", { ttlMs: 5000 }); await client.commit.apply("acme", "world", "update score", [ { operation: "revise", name: "Player/alice", data: { score: 2 }, leaseId: leased.lease.id }, ]); // The lease auto-releases on a successful or no-op write. To bail out without writing, // call client.thing.releaseLease("acme", "world", "Player/alice", leased.lease.id). ``` A successful (or no-op) write auto-releases the lease. If the lease has already expired, the write runs as an ordinary write — the same path you would take without a lease, following the usual version-conflict rules. But if another caller still holds the lease and your `leaseId` doesn't match it, the write is rejected with `LEASE_UNAVAILABLE`. ## Operation Results Submissions that apply at least one operation return operation results in input order. Mixed results include `partial: true`, `statusCounts`, and per-operation `error` objects for failed entries. If every operation fails, `client.commit.apply(...)` and `OperationBuilder.commit(...)` reject with `AllStreamOperationsFailedError`; inspect `error.result` or `error.operations` for the same per-operation failure data. Warnings are informational. A result can still be applied or no-op'd when the submitted data includes undeclared top-level shape fields. The warning lists field names and reports truncation when the list is capped. There is no `commitId` field. Version histories are the audit source; use `client.thing.history(...)` or `client.shape.history(...)` when you need to inspect what changed over time. ## Reference - [WarmHubClient.commit](/sdk-reference/classes/warmhubclient/#commit) - [OperationBuilder](/sdk-reference/classes/operationbuilder/) - [AddOp](/sdk-reference/interfaces/addop/) - [ReviseOp](/sdk-reference/interfaces/reviseop/) - [RetractOp](/sdk-reference/interfaces/retractop/) --- # Creating Subscriptions > Set up webhook subscriptions, with pointers to filters and credentials. Subscriptions are created with the [`wh sub create`](/cli-reference/commands/#sub--subscription-management) CLI command, the [`warmhub_subscription_create`](/agent-integration/mcp-tools-reference/#warmhub_subscription_create) MCP tool, or the SDK `client.subscription.create(...)` method. Subscription management REST endpoints are not currently mounted. Two pieces of a webhook subscription have their own pages: the [Filter JSON](/subscriptions/filter-json/) that selects which writes fire it, and the [credentials and signature verification](/subscriptions/credentials/) for securing deliveries. This page covers creating webhook subscriptions; [Advanced](#advanced) below covers cross-repo and self-chaining setups. ## Webhook Subscriptions A webhook subscription sends an HTTP POST to your URL when matching operations are written. **Required:** filter, webhook URL. Most webhooks also bind to a target shape via `--on ` (or `shapeName` in the SDK/MCP) — that scopes the subscription to things and assertions of that shape. The one exception is [shape lifecycle subscriptions](/subscriptions/filter-json/#shape-lifecycle-subscriptions): they omit `--on` and rely on a `{"kind":"shape"}` filter to subscribe to shape adds, revises, and retracts. See [Filter JSON](/subscriptions/filter-json/) for the full filter grammar. ### Webhook URL Requirements WarmHub validates webhook URLs at subscription create/update time and again at delivery time. URLs that don't meet these requirements are rejected with the message **"Webhook target is not reachable or not allowed"**. - **HTTPS required.** Production WarmHub deployments do not deliver to `http://` URLs. - **Port allowlist:** `80`, `443`, or `8443`. Other ports (for example `25`, `22`, or `6379`) are rejected. - **Must resolve to a public IP address.** WarmHub rejects loopback addresses like `127.0.0.1`, private-network addresses (RFC 1918 ranges such as `10.0.0.0/8` and `192.168.0.0/16`), link-local addresses, cloud-provider metadata IPs, and similar reserved ranges. - **Encoded IP bypasses are rejected.** WarmHub blocks non-canonical numeric host forms even when the scheme is otherwise allowed — examples include `https://2130706433/` (decimal-encoded `127.0.0.1`), hex or octal IP literals, IPv4-mapped IPv6 forms, and 6to4-mapped private IPs. - **No credentials in the URL.** `https://user:pass@host/` is rejected. Use [credential binding](/subscriptions/credentials/#credential-binding) to attach auth headers instead. - **Use a canonical hostname.** WarmHub accepts normal DNS hostnames and canonical IP literals only. The same rules apply to `fallbackWebhookUrl` — an optional secondary endpoint WarmHub calls after a terminal delivery failure on the primary webhook. Set it via the SDK `fallbackWebhookUrl` field (see [`client.subscription`](/sdk/client/#clientsubscription)), the CLI `--fallback-webhook-url` flag, or the [`warmhub_subscription_create` / `warmhub_subscription_update` MCP tools](/agent-integration/mcp-tools-reference/#warmhub_subscription_create). For local development, expose your receiver through a public HTTPS tunnel (for example [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](https://www.cloudflare.com/products/tunnel/)) and point the subscription at the tunnel URL. WarmHub does not deliver to `http://` URLs or to private/loopback IPs. #### Redirect behavior WarmHub re-validates **every redirect hop** against the same webhook URL rules above. A URL that passes create-time validation can still fail at delivery time if it redirects to: - a non-HTTPS target - a non-allowlisted port - a private, loopback, link-local, or otherwise reserved address - a non-canonical numeric host form WarmHub also enforces a redirect-follow limit. If the target loops or exceeds that cap, the attempt fails with `WEBHOOK_REDIRECT_LIMIT`. If a redirect crosses origins, WarmHub does **not** forward your auth, signature, idempotency, run-tracing, or W3C trace-propagation headers to the new origin. Only body-describing headers needed to preserve the request payload are retained. In practice, that means webhook endpoints behind a vanity redirector or cross-origin bounce URL should terminate on the final receiving origin directly rather than relying on WarmHub to carry credentials across origins. #### Two failure modes at validate time If validation fails, the outcome falls into one of two buckets: - **Your URL is rejected.** The request comes back with the message `Webhook target is not reachable or not allowed`. Edit the URL to match the rules above before retrying — the same message is used for every reason (scheme, port, host form, credentials, reserved IP, …) on purpose, so the surface can't be used to fingerprint internal infrastructure. - **WarmHub couldn't check it right now.** The request comes back with the message `Webhook URL validation temporarily unavailable, please retry`. This means a transient infrastructure issue (typically a DNS resolver hiccup) interrupted the check before validation could finish. Submit the same request again once the resolver issue clears. ### Via CLI ```bash wh sub create signal-hook \ --on Signal \ --kind webhook \ --filter '{"shape":"Signal"}' \ --webhook-url https://example.com/hook # Optional: allow same-trace reentry for a self-chaining webhook wh sub create signal-loop \ --on Signal \ --kind webhook \ --filter '{"shape":"Signal"}' \ --webhook-url https://example.com/hook \ --allow-trace-reentry ``` ### Via MCP ```json { "name": "warmhub_subscription_create", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "signal-hook", "kind": "webhook", "shapeName": "Signal", "filterJson": { "shape": "Signal" }, "webhookUrl": "https://example.com/hook" } } ``` ### Webhook Payload The POST body includes: | Field | Description | |-------|-------------| | `event` | `"warmhub.write"` or `"warmhub.retract"` | | `traceId` | Unique trace identifier for the event chain | | `runId` | Action run identifier | | `subscriptionId` | Subscription identifier | | `callback_url` | Callback endpoint to report asynchronous progress or terminal outcome for this run | | `repo` | `{ "orgName", "repoName" }` — the subscription's home repo | | `matchedOperationIndexes` | Indexes of operations that matched the filter | | `matchedOperations` | The matched operations with their details | Headers include `X-WarmHub-Idempotency-Key`, `X-WarmHub-Run-Id`, and `X-WarmHub-Attempt` for deduplication and observability. Deliveries also include World Wide Web Consortium (W3C) trace propagation headers such as `traceparent` and `tracestate` when they run inside an active backend trace. When the subscription binds a `WEBHOOK_SIGNING_SECRET`, deliveries are also signed — see [Verifying Signatures](/subscriptions/credentials/#verifying-signatures). Use `callback_url` when your handler accepts the request and finishes work asynchronously. Post `processing`, `success`, `failure`, or `retry_requested` back to that URL with normal `repo:write` authentication. ## Advanced ### Trace Reentry Write-triggered `webhook` subscriptions support an optional `allowTraceReentry` setting. - Default: `false` - CLI flag: `--allow-trace-reentry` - Meaning when `false`: a subscription runs at most once per trace per shape - Meaning when `true`: the subscription may run again within the same trace - Hard fuse: a global chain-depth safety limit still applies even when reentry is allowed You can set `allowTraceReentry` at create time through any surface: the CLI `--allow-trace-reentry` flag, the SDK `client.subscription.create(...)` method, or the MCP `warmhub_subscription_create` tool. It can also be patched later through MCP with `warmhub_subscription_update`. ### Cross-Repo Subscriptions A webhook subscription can watch a **source repo** that is different from the repo where the subscription lives. When a matching write lands in the source repo, the subscription fires and delivers to the webhook URL configured in its home repo. #### Constraints - The source repo must be in the **same org** as the subscription's home repo. Cross-org source repos are rejected at creation time. - The subscription creator must have **read access to the source repo**. Creation fails if this check does not pass. - `allowTraceReentry` applies normally to cross-repo subscriptions. #### Via CLI ```bash # Webhook subscription in myrepo that fires on writes to other-repo wh sub create cross-hook \ --on Signal \ --kind webhook \ --source myorg/other-repo \ --filter '{"shape":"Signal"}' \ --webhook-url https://example.com/hook ``` #### Via SDK Cross-repo subscriptions are also supported through the SDK by setting `sourceRepoRef` on `client.subscription.create(...)`. ```ts await client.subscription.create({ orgName: 'myorg', repoName: 'myrepo', name: 'cross-hook', kind: 'webhook', shapeName: 'Signal', filterJson: { shape: 'Signal' }, sourceRepoRef: 'myorg/other-repo', webhookUrl: 'https://example.com/hook', }) ``` #### Via MCP Cross-repo subscriptions are also supported through the `warmhub_subscription_create` MCP tool by setting `sourceRepoRef`. ```json { "name": "warmhub_subscription_create", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "cross-hook", "kind": "webhook", "shapeName": "Signal", "filterJson": { "shape": "Signal" }, "sourceRepoRef": "myorg/other-repo", "webhookUrl": "https://example.com/hook" } } ``` There is no public HTTP subscription creation endpoint for cross-repo or same-repo subscriptions. #### Payload Differences Cross-repo deliveries use the same top-level webhook payload shape as same-repo deliveries. Two details matter: | Field | Description | |-------|-------------| | `repo` | The subscription's home repo — where the subscription lives | | `event` | Event discriminator: `"warmhub.write"` or `"warmhub.retract"`. Use this field to branch webhook handler logic by event type. | | `originRepoId` | Identifies the repo where the event originated (read-only). Cross-repo deliveries do not currently add a separate expanded `sourceRepo` object. | When the subscription is not cross-repo, the payload shape is still the same. --- # Subscription Credentials & Signatures > Bind authentication credentials to webhook deliveries and verify WarmHub's HMAC signature on the receiving end. Webhook deliveries can carry authentication headers, and WarmHub can sign each delivery so your receiver can verify it came from WarmHub and wasn't tampered with. Both are configured with [credential binding](#credential-binding). ## Credential Binding Webhook subscriptions can use **credential binding** to inject authentication headers into delivery requests. This keeps secrets out of webhook URLs and subscription configuration. ### Setup 1. **Create a credential set:** ```bash wh credential create webhook-keys ``` 2. **Set authentication keys:** ```bash # Bearer token echo "tok_secret" | wh credential set webhook-keys WEBHOOK_BEARER_TOKEN # Or API key echo "key_secret" | wh credential set webhook-keys WEBHOOK_API_KEY ``` 3. **Bind the credential set to a subscription:** ```bash wh sub bind signal-hook --credentials webhook-keys ``` ### Supported Auth Methods | Key Name | Header Produced | |----------|-----------------| | `WEBHOOK_BEARER_TOKEN` | `Authorization: Bearer ` | | `WEBHOOK_API_KEY` | `X-API-Key: ` (or custom header via `WEBHOOK_API_KEY_HEADER`) | | `WEBHOOK_BASIC_USERNAME` + `WEBHOOK_BASIC_PASSWORD` | `Authorization: Basic ` | | `WEBHOOK_SIGNING_SECRET` | `X-WarmHub-Signature` (HMAC-SHA256) + `X-WarmHub-Timestamp` — see [Verifying Signatures](#verifying-signatures) | | `FALLBACK_BEARER_TOKEN` | `Authorization: Bearer ` on `fallbackWebhookUrl` deliveries | | `FALLBACK_API_KEY` | `X-API-Key: ` on `fallbackWebhookUrl` deliveries (or custom header via `FALLBACK_API_KEY_HEADER`) | | `FALLBACK_BASIC_USERNAME` + `FALLBACK_BASIC_PASSWORD` | `Authorization: Basic ` on `fallbackWebhookUrl` deliveries | | `FALLBACK_SIGNING_SECRET` | `X-WarmHub-Signature` (HMAC-SHA256) + `X-WarmHub-Timestamp` on `fallbackWebhookUrl` deliveries | Credential resolution is best-effort — if a credential set is missing or revoked, the webhook is still delivered without auth headers. ### Unbinding ```bash wh sub unbind signal-hook ``` Removing the credential binding stops auth headers from being injected on future deliveries. ## Verifying Signatures When a subscription binds a `WEBHOOK_SIGNING_SECRET`, WarmHub signs every delivery so your receiver can confirm the request came from WarmHub and the body wasn't modified in transit. Each delivery carries two headers: | Header | Value | |--------|-------| | `X-WarmHub-Signature` | `sha256=` — the HMAC-SHA256 of the signed message, hex-encoded, with a literal `sha256=` prefix | | `X-WarmHub-Timestamp` | The Unix timestamp (seconds) used in the signed message | ### What is signed The signed message is the timestamp and the **raw request body**, joined by a literal period: ``` . ``` WarmHub computes `HMAC-SHA256(secret, message)` and sends the hex digest as `X-WarmHub-Signature: sha256=`. To verify, recompute the HMAC over the **exact bytes you received** — read the raw body *before* any JSON parsing or re-serialization, which would change the bytes and break the match. ### Verify the signature Always (1) recompute the HMAC over `timestamp + "." + rawBody`, (2) compare it to the header with a constant-time comparison, and (3) reject deliveries whose timestamp is outside a freshness window you choose, to limit replay. WarmHub does not enforce a replay window — that check is yours. ```ts // TypeScript (Node) — verify a WarmHub webhook delivery import { createHmac, timingSafeEqual } from 'node:crypto' function verifyWarmHubDelivery( rawBody: string, signatureHeader: string | undefined, // X-WarmHub-Signature timestampHeader: string | undefined, // X-WarmHub-Timestamp secret: string, ): boolean { if (!signatureHeader || !timestampHeader) return false // Replay window — reject deliveries older than 5 minutes (your choice). const age = Math.floor(Date.now() / 1000) - Number(timestampHeader) if (!Number.isFinite(age) || Math.abs(age) > 300) return false const expected = 'sha256=' + createHmac('sha256', secret) .update(`${timestampHeader}.${rawBody}`) .digest('hex') const got = Buffer.from(signatureHeader) const want = Buffer.from(expected) return got.length === want.length && timingSafeEqual(got, want) } ``` ```python # Python — verify a WarmHub webhook delivery import hashlib import hmac import time def verify_warmhub_delivery(raw_body: bytes, signature: str | None, timestamp: str | None, secret: str) -> bool: if not signature or not timestamp: return False # Replay window — reject deliveries older than 5 minutes (your choice). try: age = int(time.time()) - int(timestamp) except ValueError: return False if abs(age) > 300: return False message = f"{timestamp}.".encode() + raw_body expected = "sha256=" + hmac.new( secret.encode(), message, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) ``` To check a captured delivery by hand, recompute the digest with `openssl` and compare it to the `X-WarmHub-Signature` header: ```bash # TIMESTAMP = the X-WarmHub-Timestamp header; body.json = the raw request body printf '%s.%s' "$TIMESTAMP" "$(cat body.json)" \ | openssl dgst -sha256 -hmac "$WEBHOOK_SIGNING_SECRET" -hex # prepend "sha256=" to the output and compare to X-WarmHub-Signature ``` ### Rotating the signing secret `WEBHOOK_SIGNING_SECRET` is a single value per credential set, and WarmHub signs with whatever is bound at delivery time — there is **no dual-secret grace window** on WarmHub's side. To rotate without dropping deliveries, make your receiver accept the new secret *before* you rotate: 1. Update your receiver to verify against **either** the old or the new secret. 2. Set the new value: `echo "new_secret" | wh credential set webhook-keys WEBHOOK_SIGNING_SECRET`. 3. Once you've confirmed deliveries verify against the new secret, drop the old one from your receiver. --- # Subscription Filter JSON > Filter predicates, boolean combinators, glob matching, and shape-lifecycle filters for webhook subscriptions. Webhook subscriptions require a **filter** that determines which write operations trigger the action. Filters are evaluated against each operation; the subscription fires if at least one operation matches. Set a filter with `--filter` (CLI), `filterJson` (SDK/MCP) — see [Creating Subscriptions](/subscriptions/creating/) for the create flow. ## Predicates | Field | Type | Description | |-------|------|-------------| | `operation` | `string \| string[]` | Operation type: `"add"`, `"revise"`, `"retract"` | | `kind` | `string \| string[]` | Entity kind: `"shape"`, `"thing"`, `"assertion"`, `"collection"` | | `shape` | `string` | Shape name of the thing or assertion the operation targets. Filters remain stable if the shape is renamed later. Does not match shape-lifecycle operations — use `{"kind":"shape"}` plus `name` for those. | | `name` | `string \| string[]` | Exact match on the operation's `name`. For thing and assertion operations the name is the full `Shape/name` [wref](/data-modeling/wrefs/) (e.g. `Signal/sensor-1`); for shape lifecycle operations it is the bare shape name (e.g. `Reviewer`). Most useful with `kind: "shape"` to target a single shape's lifecycle (e.g. `{"all":[{"kind":"shape"},{"name":"Reviewer"}]}`). Array form is OR-combined and rejected at create time if it contains non-string entries or is empty. For things/assertions, prefer `match` for glob patterns. | | `match` | `string \| string[]` | Glob match on the operation's `name`. For things and assertions that name is the full `Shape/name` [wref](/data-modeling/wrefs/); for shape lifecycle operations it is the bare shape name (e.g. `Reviewer`, not `Shape/Reviewer`). Uses the same glob syntax as `wh thing list --match`. Array form is OR-combined and must contain at least one pattern. | ## Boolean Combinators | Field | Type | Description | |-------|------|-------------| | `all` | `FilterNode[]` | AND — all children must match | | `any` | `FilterNode[]` | OR — at least one child must match | | `not` | `FilterNode` | NOT — child must not match | ## Examples **Match all operations on a shape** (simplest filter): ```json { "shape": "Signal" } ``` **Match only new things added to a shape:** ```json { "all": [ { "operation": "add" }, { "kind": "thing" } ] } ``` **Match assertions or things but not revisions:** ```json { "all": [ { "any": [{ "kind": "assertion" }, { "kind": "thing" }] }, { "not": { "operation": "revise" } } ] } ``` ## Shape Lifecycle Subscriptions Shape lifecycle subscriptions watch for shape adds, revises, and retracts rather than operations on things of a shape. They do **not** require an `--on ` target; the filter drives matching. **Subscribe to all shape lifecycle events in a repo:** ```json { "kind": "shape" } ``` ```bash wh sub create shape-changes --repo myorg/myrepo \ --kind webhook \ --filter '{"kind":"shape"}' \ --webhook-url https://hooks.example.com/shapes ``` **Subscribe to shape retracts only:** ```json { "all": [ { "kind": "shape" }, { "operation": "retract" } ] } ``` ```bash wh sub create shape-retracts --repo myorg/myrepo \ --kind webhook \ --filter '{"all":[{"kind":"shape"},{"operation":"retract"}]}' \ --webhook-url https://hooks.example.com/shapes ``` **Subscribe to events on a specific shape by name:** ```json { "all": [ { "kind": "shape" }, { "name": "Reviewer" } ] } ``` **Webhook event discriminator:** Shape adds and revises emit `warmhub.write`; shape retracts emit `warmhub.retract`. Branch on `matchedOperations[*].operation.kind` to distinguish shape lifecycle deliveries from thing/assertion deliveries. **`shape` vs. `name` for lifecycle filters:** The `shape` predicate matches things and assertions whose shape is `Reviewer`, so it never matches shape lifecycle operations (a shape add isn't itself "of a shape"). To target a single shape's lifecycle, combine `{"kind":"shape"}` with `{"name":"Reviewer"}` instead of `{"shape":"Reviewer"}`. **`--on Signal` does not subscribe to changes to the `Signal` shape itself.** Binding a subscription to a shape with `--on Signal` (or `shapeName: "Signal"`) scopes it to Signal's things and assertions — adding, revising, or retracting the `Signal` shape definition will not fire it. To subscribe to shape lifecycle events, omit `--on` and use a `{"kind":"shape"}` filter. ### When `--on` may be omitted The `--on` / `shapeName` requirement is waived only when the filter has a literal `kind: "shape"` (or `kind: ["shape"]`) constraint at the **top level** or inside an **`all`** chain. The check is structural, not semantic — `any` disjunctions and `not` branches are not inspected even when every branch happens to be shape-only. | Form | `--on` required? | Notes | |------|------------------|-------| | `{"kind":"shape"}` | no | Top-level exact `"shape"`. | | `{"kind":["shape"]}` | no | Top-level single-element array of `"shape"`. | | `{"all":[{"kind":"shape"}, …]}` | no | `all` chains preserve the shape-only guarantee. | | `{"any":[{"kind":"shape"}, {"kind":"thing"}]}` | yes | `any` is not inspected for the exemption. | | `{"any":[{"all":[{"kind":"shape"}, …]}, {"all":[{"kind":"shape"}, …]}]}` | yes | Even when every branch is shape-only, top-level `any` disqualifies the filter. Express as one `{"all":[{"kind":"shape"}, {"any":[…]}]}` instead. | | `{"not":{"kind":"shape"}}` | yes | Negation does not narrow to shape ops. | | `{"kind":["shape","thing"]}` | yes | Mixed-kind unions match non-shape ops. | | Any filter without a `kind:"shape"` constraint | yes | Standard webhook subscription path. | The create surface rejects filters in the "yes" rows when `--on` (or `filterJson.shape`) is missing. ## Glob Matching **Match things under a naming branch** (everything under `Sensor/hq/`): ```json { "all": [ { "kind": "thing" }, { "match": "Sensor/hq/**" } ] } ``` **Match by deeper glob patterns** — globstars, single-segment wildcards, and brace expansion all work the same as `wh thing list --match`: ```json { "all": [ { "kind": "thing" }, { "match": "Sensor/**/temp" } ] } ``` **Match any of several patterns** (array = OR; empty arrays are invalid): ```json { "all": [ { "kind": "thing" }, { "match": ["Sensor/hq/**", "Sensor/warehouse/**"] } ] } ``` To require two patterns both match, nest under `all` instead of using an array — see [Naming as Navigation](/data-modeling/naming-as-navigation/#subscription-match) for worked examples. Shape names in filters are rename-safe: if a shape is renamed later, existing filters continue to match that shape. For cross-repo subscriptions, shape names are resolved against the **source repo's** namespace at creation time. --- # Managing Subscriptions > Monitor, update, pause, resume, and delete subscriptions. Once a subscription is created, you can list, inspect, update, pause, resume, and delete it through the [`wh sub`](/cli-reference/commands/#sub--subscription-management) CLI commands or the [MCP subscription tools](/agent-integration/mcp-tools-reference/#subscription-tools). Subscription management REST endpoints are not currently mounted; the HTTP API surface below is limited to action delivery observability. ## List Subscriptions ### Via CLI ```bash wh sub list ``` Output shows each subscription's name, kind, and active state. For cross-repo subscriptions, a `← org/repo` marker indicating the source repo being watched is shown when you have read access to that source repo: ``` Subscriptions: myorg/myrepo signal-hook [webhook] active upstream-hook [webhook] active ← myorg/otherrepo ``` ### Via MCP ```json { "name": "warmhub_subscription_list", "arguments": { "orgName": "myorg", "repoName": "myrepo" } } ``` ## View Subscription Details ### Via CLI ```bash wh sub view signal-hook ``` Returns the subscription's kind, active state, and webhook URL. For cross-repo subscriptions, a `source repo:` field is shown when you have read access to the source repo, indicating which repo's events trigger the subscription: ``` upstream-hook kind: webhook active: true source repo: myorg/otherrepo webhookUrl: https://example.com/hook ``` ### Via MCP ```json { "name": "warmhub_subscription_get", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "signal-hook" } } ``` ## Pause and Resume Pausing a subscription stops all new deliveries. Existing in-flight deliveries complete normally. ### Via CLI ```bash # Pause wh sub pause signal-hook # Resume wh sub resume signal-hook ``` ### Via MCP ```json { "name": "warmhub_subscription_pause", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "signal-hook" } } ``` ## Update Subscription Use update when the subscription should keep the same name and lifecycle state, but its trigger or webhook configuration needs to change. Provided fields patch the existing subscription. ### Via CLI ```bash wh sub update signal-hook \ --on Signal \ --filter '{"shape":"Signal","operation":"add"}' \ --webhook-url https://example.com/hook ``` ### Via MCP ```json { "name": "warmhub_subscription_update", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "signal-hook", "shapeName": "Signal", "filterJson": { "shape": "Signal", "operation": "add" }, "webhookUrl": "https://example.com/hook" } } ``` Lifecycle changes still go through pause and resume, and credential bindings remain managed via the bind/unbind commands. ## Delete Deleting a subscription permanently removes it along with its credential bindings. ### Via CLI ```bash wh sub delete signal-hook ``` ### Via MCP ```json { "name": "warmhub_subscription_delete", "arguments": { "orgName": "myorg", "repoName": "myrepo", "name": "signal-hook" } } ``` ## Delivery Feed The delivery feed shows the history of action deliveries for a subscription — what was dispatched, when, and what happened. ### Via CLI ```bash wh sub log signal-hook ``` Output shows each delivery's run status, attempt count, timing, source label, and matched operation indexes. Failed runs display the error code and message: ``` Subscription: signal-hook succeeded 1/1 2m ago run 019d90f0-0000-7000-8000-000000000000 write ops[0,1] [0] add thing Sensor/temp-1 [1] add assertion Reading/temp-1-v1 dead_letter 3/5 15m ago run 019d90e0-0000-7000-8000-000000000000 write ops[0] HTTP_502: Webhook responded 502 [0] revise thing Sensor/temp-1 ``` Follow deliveries in real time with `--live`: ```bash wh sub log signal-hook --live ``` This opens a WebSocket connection that auto-refreshes as new deliveries arrive. ### Via MCP ```json { "name": "warmhub_action_livefeed", "arguments": { "orgName": "myorg", "repoName": "myrepo", "subscriptionName": "signal-hook", "limit": 20 } } ``` The `limit` parameter controls how many deliveries to return (1–500, default 50). Pagination is supported via the `cursor` field in the response — when supplying `cursor` on a follow-up call, you must also pass an explicit `limit` (the tool rejects `cursor` alone with `"cursor" requires "limit"`). See [`warmhub_action_livefeed`](/agent-integration/mcp-tools-reference/#warmhub_action_livefeed) for the full parameter contract. ### Via HTTP API The HTTP API exposes the underlying run list, optionally filtered by `subscriptionName`. The endpoint requires `repo:configure` — anonymous calls return an opaque `404`: ```bash curl -H "Authorization: Bearer $WH_TOKEN" \ "https://api.warmhub.ai/api/repos/myorg/myrepo/actions/runs?subscriptionName=signal-hook&status=failed_terminal&limit=20" ``` This returns the run records themselves. For the richer per-delivery feed (matched-operation context, attempt diagnostics, live updates), use the CLI (`wh sub log`) or MCP (`warmhub_action_livefeed`). See [HTTP API → Actions](/http-api/actions/) for the full parameter list. ## Run Attempts Each delivery can have multiple **attempts** if retries are needed. To inspect the attempt history for a specific run: ### Via CLI ```bash wh sub attempts 019d90f0-1111-7000-8000-000000000001 ``` The argument is the run ID (UUIDv7) shown in `wh sub log` and `wh sub attempts` JSON output. Output shows each attempt's status, duration, HTTP status, and error details: ``` Attempts: run 019d90f0-1111-7000-8000-000000000001 #1 failed (300ms) HTTP 502 HTTP_502: Webhook responded 502 #2 failed (450ms) HTTP 502 HTTP_502: Webhook responded 502 #3 failed (280ms) HTTP 502 HTTP_502: Webhook responded 502 ``` ### Via MCP ```json { "name": "warmhub_action_attempts", "arguments": { "orgName": "myorg", "repoName": "myrepo", "runId": "019d90f0-1111-7000-8000-000000000001" } } ``` Each attempt records: | Field | Description | |-------|-------------| | `attempt` | Attempt number (1-based) | | `status` | `started`, `succeeded`, or `failed` | | `startedAt` | Timestamp when the attempt began | | `finishedAt` | Timestamp when the attempt completed (if finished) | | `httpStatus` | HTTP response status | | `errorCode` | Error classification code | | `errorMessage` | Human-readable error description | ## Run Statuses | Status | Meaning | |--------|---------| | `pending` | Run created, not yet executed | | `running` | Currently executing | | `processing` | Accepted by the handler and continuing asynchronously while WarmHub waits for a callback | | `retry_wait` | Failed, waiting for next retry attempt | | `succeeded` | Completed successfully | | `failed_terminal` | Non-retryable error (no further attempts scheduled) | | `dead_letter` | No recovery path remains | ## Notifications WarmHub records repo-scoped **action notifications** for delivery outcomes. `dead_letter` always records a failure notification. `failed_terminal` records one immediately unless a fallback URL still has a chance to recover the delivery; in that case, notification is deferred until recovery is no longer possible. Success notifications can also be emitted when the subscription has `notifyOnSuccess` enabled. You set that flag through the [SDK `client.subscription.create()` / `update()` surface](/sdk/client/#clientsubscription) or the `warmhub_subscription_create` MCP tool. The CLI and the `warmhub_subscription_update` MCP tool do not expose a setter for it. The common `dead_letter` paths are: - retries were exhausted without a successful delivery - a previously `failed_terminal` run's fallback endpoint was itself rejected by webhook URL validation - a previously `failed_terminal` run's fallback endpoint exceeded the redirect-follow cap and still could not recover the delivery `wh notifications --repo` and the MCP notification tools return repo-scoped delivery notification records for operators, including terminal failures and optional success notifications. Separately, the web app may aggregate related user-facing inbox entries; those inbox entries are not the same API surface as repo-scoped delivery notifications. ### Via CLI ```bash wh notifications --repo myorg/myrepo ``` Filter by time with either epoch milliseconds or an ISO timestamp: ```bash wh notifications --repo myorg/myrepo --since 2026-03-30T12:00:00Z ``` ### Via MCP ```json { "name": "warmhub_action_notifications", "arguments": { "orgName": "myorg", "repoName": "myrepo", "limit": 20 } } ``` The MCP tool is also repo-scoped and returns delivery notification records rather than the web app's user feed. Once a run's failure is no longer recoverable — `dead_letter`, or `failed_terminal` with no fallback left to try — WarmHub creates a repo-scoped action notification record with the error details. Success notifications can also be enabled per-subscription via `notifyOnSuccess` (see above). Those records are visible through the action notification queries: ### Via HTTP API The endpoint requires `repo:configure`: ```bash curl -H "Authorization: Bearer $WH_TOKEN" \ "https://api.warmhub.ai/api/repos/myorg/myrepo/actions/notifications" ``` ## Error Codes Common error codes you may see in attempt records: | Code | Retryable | Description | |------|-----------|-------------| | `WEBHOOK_NETWORK_ERROR` | Yes | Network or transport error while connecting to the webhook target | | `WEBHOOK_TARGET_REJECTED` | No | WarmHub rejected the target URL at dispatch time because it was not reachable or not allowed. See [Webhook URL Requirements](/subscriptions/creating/#webhook-url-requirements) for the allowed scheme, ports, and public-network rules. | | `WEBHOOK_REDIRECT_LIMIT` | No | The webhook target exceeded WarmHub's redirect-follow limit. This is usually a redirect loop or an overly long redirect chain at the partner endpoint. Fix the destination so it returns a terminal response directly or within a small number of redirects. | | `HTTP_` | Depends | Remote returned an HTTP response. `HTTP_429` and `HTTP_5xx` are retryable; other `HTTP_4xx` responses are not | | `WEBHOOK_INPUT_NOT_FOUND` | No | Could not load execution input for delivery | --- # Subscriptions > Event-driven webhook deliveries triggered by writes. A **subscription** watches a repository for changes and delivers a webhook when matching write operations land — it is WarmHub's mechanism for automation. A subscription watches its own repository by default, or a different repository in the same org (a cross-repo subscription); either way, the webhook is delivered using the subscription's home repo for credentials and configuration. ## Create a subscription POST to your webhook when `Signal` writes land (things or assertions): ```bash wh sub create signal-hook --repo myorg/myrepo --on Signal --kind webhook \ --filter '{"shape":"Signal"}' --webhook-url https://example.com/hook ``` When a matching write lands, WarmHub records an action event, matches it against every active subscription's filter, and dispatches the webhook to each match — retrying transient failures with exponential backoff before the run goes terminal. See [Creating Subscriptions](/subscriptions/creating/) for the webhook payload and credential binding. ## Next Steps | Need | Page | |------|------| | Create a subscription and see the webhook payload shape | [Creating Subscriptions](/subscriptions/creating/) | | Watch a different repository in the same org | [Cross-Repo Subscriptions](/subscriptions/creating/#cross-repo-subscriptions) | | The filter grammar that selects which writes fire | [Filter JSON](/subscriptions/filter-json/) | | Bind delivery auth and verify WarmHub's HMAC signature | [Credentials & Signatures](/subscriptions/credentials/) | | List, view, pause, resume, update, or delete subscriptions | [Managing Subscriptions](/subscriptions/managing/) | | Inspect the delivery feed and per-attempt details | [Delivery Feed](/subscriptions/managing/#delivery-feed) | | Understand run statuses, retries, and dead-letter behavior | [Run Statuses](/subscriptions/managing/#run-statuses) | | Delivery-outcome notifications (failures and opt-in success) | [Notifications](/subscriptions/managing/#notifications) | --- # Need help? > How to report a bug, request a feature, or ask a question about WarmHub. import { LinkButton } from '@astrojs/starlight/components'; If you've hit a problem, want to suggest a feature, or have a question, email us. Each option below includes a template — fill in what you can and send, and we'll get back to you. ## Report a bug Compose Information to include: - Product area - Expected behavior - Actual behavior - Steps to reproduce - SDK / CLI / API version - Request ID / timestamp (if any) - Severity / user impact - Screenshot (optional) ## Request a feature Compose Information to include: - Problem / use case - Proposed behavior - What you're doing today (workaround, if any) ## Ask a question Compose Information to include: - What you're trying to do - What you've tried - SDK / CLI / API version (if relevant) - Relevant code, commands, or output - Related docs you've already read - Screenshot (optional) --- ## Looking for something else? - **Service status** — check [status.warmhub.ai](https://status.warmhub.ai/) for current uptime and incidents before reporting an outage. - **Security disclosures** — email [security@warmhub.ai](mailto:security@warmhub.ai). Please don't include exploit details in the initial message. - **Privacy / data requests** — see the [Privacy Policy](https://warmhub.ai/privacy). - **Legal inquiries** — see the [Terms of Service](https://warmhub.ai/tos). --- # Installing Veritas > Install the Veritas component and provision the shapes and subscriptions it needs. Veritas is a registered WarmHub [component](/components/overview/) — a packaged set of shapes, subscriptions, and credentials you install into any repo where you want reputation-weighted consensus over subjective-logic opinions. ## Prerequisites - An existing WarmHub repo to install into. - A `wh` CLI authenticated against the org that owns the repo. See [Getting access](/auth/getting-access/) if you have not set this up yet. ## Install ```bash wh component install warmhub/veritas --repo [your-org]/[your-repo] ``` After the command returns, your repo has the four Veritas shapes and three subscriptions watching for new opinions. The CLI tracks the install so you can manage and tear it down later. (One prerequisite worth knowing: the four shape names `Certainty`, `Support`, `Opposition`, and `Consensus` must be absent or already owned by `com.warmhub.Veritas` — a pre-existing user-owned shape under any of those names will fail the install with a shape-ownership error. Retract or rename the conflicting shape and rerun if you hit that.) Confirm the install succeeded: ```bash wh component list --repo [your-org]/[your-repo] wh component view com.warmhub.Veritas --repo [your-org]/[your-repo] ``` Veritas appears under three identifiers, each used by different commands: | Identifier | Form | Used by | |------------|------|---------| | Install source | `warmhub/veritas` | `wh component install` | | Component ID | `com.warmhub.Veritas` | `wh component view`, `teardown`, and other lifecycle commands | | Display name | `veritas` | `wh component doctor` (additionally; other commands need the full ID) | `wh component list` output shows the component ID — copy from there when you need it. ## What gets installed ### Shapes | Shape | Purpose | About target | |-------|---------|--------------| | `Certainty` | A source's opinion about an assertion. The primitive bet that wagers source reputation. | An assertion (binary proposition). | | `Support` | The belief that one assertion is evidence for another. | A `Pair` collection of two assertions, `first → second`. | | `Opposition` | The belief that one assertion is evidence against another. | A `Pair` collection of two assertions, `first → second`. | | `Consensus` | Veritas's consolidated readout. Written by Veritas only. | An assertion (consolidating its `Certainty` opinions) or a `Pair` (consolidating its `Support` / `Opposition` opinions). | All four shapes carry `belief`, `disbelief`, `uncertainty`, and optional `alpha`. `Certainty`, `Support`, and `Opposition` additionally require a `source` [wref](/data-modeling/wrefs/) — the thing whose reputation is wagered — and accept an optional `rationale` string. `Consensus` has no `source` or `rationale`; it is Veritas's own consolidated readout, not a wager. See [Writing opinions](/veritas/writing-opinions/) for examples. ### Subscriptions Veritas subscribes to changes on the three input shapes so it can re-solve whenever a new opinion lands: | Subscription | Triggers on | |--------------|-------------| | `veritas-certainty` | Add, revise, or retract of any `Certainty` assertion. | | `veritas-support` | Add, revise, or retract of any `Support` assertion. | | `veritas-opposition` | Add, revise, or retract of any `Opposition` assertion. | Webhook delivery is provisioned automatically — no setup required on your end. ## Verifying health Run health checks any time to confirm all declared resources are present and active: ```bash wh component doctor veritas --repo [your-org]/[your-repo] ``` A successful `wh component install` should land Veritas in `ready` — setup provisions the webhook signing secret for you, so you do not need to populate any credential keys yourself. If doctor reports `credentials-required` or `degraded`, something went wrong during setup or a resource has drifted; if it reports `paused`, that's the expected state after `wh component teardown` and is not an error. See [Component lifecycle](/components/lifecycle/) for the full state machine and recovery guidance — these are generic component states, not Veritas-specific. For credential-set inspection and key management specifically, see the [`wh credential` commands](/cli-reference/commands/#credential--credential-sets). ## Pausing Pause all Veritas subscriptions without removing the shapes or assertions: ```bash wh component teardown com.warmhub.Veritas --repo [your-org]/[your-repo] ``` Teardown pauses Veritas's subscriptions — incoming `Certainty`, `Support`, and `Opposition` changes will no longer trigger a re-solve. Existing `Consensus`, `Certainty`, `Support`, and `Opposition` assertions remain in place. Resuming requires explicitly re-enabling each subscription with `wh sub resume ` — reinstalling the component does not unpause them. Teardown is the only safe lifecycle operation today. A complete uninstall flow that removes component-owned shapes and the credential set is not yet supported — use `wh component teardown` to disable Veritas, and contact support if you need a full removal. ## Next steps - [Writing opinions](/veritas/writing-opinions/) — how to author Certainty, Support, and Opposition, and how to read Consensus. - [Overview](/veritas/overview/) — the conceptual model behind the four shapes. --- # Veritas Overview > Veritas computes reputation-weighted consensus over subjective-logic opinions about your assertions. **Veritas** is the WarmHub component for reputation-weighted consensus. It takes opinions that different sources express about your assertions, discounts each opinion by how trustworthy its source has proven to be, and writes back a single consolidated readout — a `Consensus` assertion — that you can query like any other thing. Veritas is installed as a component. Once installed, four shapes appear in your repo: `Certainty`, `Support`, `Opposition`, and `Consensus`. The first three are inputs you write; `Consensus` is the output Veritas writes back. ## A minimal example Veritas operates on an assertion you already have — a binary proposition such as `Proposition/article-is-accurate`. Write a `Certainty` about it, then read back the `Consensus` Veritas computes: ```bash # A source wagers its reputation that the proposition is true wh assertion create --shape Certainty \ --about Proposition/article-is-accurate \ --data '{"belief":0.8,"disbelief":0.1,"uncertainty":0.1,"source":"Source/research-team"}' \ --repo myorg/myrepo # Read Veritas's reputation-weighted readout for that assertion wh thing about Proposition/article-is-accurate --shape Consensus --repo myorg/myrepo ``` The sections below explain the opinion model and the other inputs (`Support`, `Opposition`); the routing table covers the full write and read paths. ## The opinion model Every Veritas input and output is a **subjective-logic opinion**: a four-number tuple describing what a source believes about a binary proposition. | Field | Meaning | |-------|---------| | `belief` | Evidence that the proposition is true. | | `disbelief` | Evidence that the proposition is false. | | `uncertainty` | Residual ignorance — the portion of probability mass not yet committed to either side. | | `alpha` | Optional base rate (prior). Defaults to `0.5` when omitted. | `belief + disbelief + uncertainty` must equal `1`. A vacuous opinion is `(0, 0, 1)` — no evidence either way. A near-dogmatic opinion has uncertainty close to zero. :::note[Binomial only] A subjective-logic opinion models a two-outcome frame. The assertion an opinion is about must be a **binary proposition** — something that is either true or false. Attaching a Veritas opinion to an open-ended claim ("how tall is the building?") produces numbers that satisfy `b + d + u = 1` but carry no meaning. Phrase the underlying claim as true/false first ("the building is at least 30 stories"). ::: ## Trust is wagered, not declared A `Certainty` is the primitive bet. When a source writes a Certainty about an assertion, the source is wagering its reputation: if the Certainty turns out to disagree with other Certainties on the same assertion, the source's reputation may move. Reputation is itself an opinion — `(belief, disbelief, uncertainty, alpha)` describing how trustworthy a source has proven to be in a given scope. Veritas tracks reputation per shape for `Certainty` and per directional shape pair for `Support` / `Opposition`, so a source's track record on one assertion type does not automatically carry over to another. Veritas maintains this state behind the scenes and uses it to discount each incoming Certainty before consolidating. The discount turns a raw Certainty into a **weighted certainty** — what the source believes, multiplied by how much you should trust the source. A confident statement from a high-reputation source counts for more than the same statement from an unknown source. ## Support and opposition Beyond direct certainties, Veritas also models **relationships** between assertions: - A `Support` opinion says "belief in assertion A is evidence for assertion B." - An `Opposition` opinion says "belief in assertion A is evidence against assertion B." Both shapes are written about a [`Pair`](/data-modeling/collections/) collection of two assertions, where the pair's direction matters: the **first** member is the antecedent and the **second** is the consequent. Support and opposition pathways are independent — disbelief on a support pathway means "the support relationship does not hold," not "the opposite relationship holds." Veritas consolidates each pathway separately — multiple `Support` opinions about the same pair are fused into one support readout, and multiple `Opposition` opinions are fused into an opposition readout. Each pathway's consolidated opinion is reputation-weighted in the same way Certainties are. ## Consensus `Consensus` is the only shape Veritas writes back to your repo. It appears in two places: - **About a target assertion**, consolidating that assertion's native `Certainty` opinions — Veritas's reputation-weighted answer to "what do we believe about this assertion?" - **About a `Pair`**, when `Support` and/or `Opposition` opinions exist for that pair — consolidating each pathway's opinions independently. (See [Writing opinions — Reading the Consensus](/veritas/writing-opinions/#reading-the-consensus) for the read path and a current limitation around distinguishing the support and opposition rows.) `Consensus` is recomputed whenever a new, revised, or retracted input lands. For a fixed set of inputs and reputations, the readout is order-independent and deterministic. ## When to use Veritas Veritas is the right fit when: - Multiple sources express **opinions about the same assertion** and you need a single consolidated answer. - You want sources to be **trusted in proportion to their track record**, not equally. - Some assertions support or contradict others, and you want those relationships **consolidated alongside your direct opinions** (Veritas writes per-pair `Consensus` for `Support` / `Opposition` opinions today; propagation of those relations into the consequent assertion's own `Consensus` is not yet implemented). - The underlying claims are **binary propositions** — true/false statements suitable for subjective-logic opinions. It is not the right fit when assertions describe magnitudes, categories, or other non-binary values — keep using regular [assertions](/data-modeling/assertions/) and aggregate them yourself. ## Where the readout shows up `Consensus` assertions are queryable through the same surfaces as any other assertion. `wh thing about ` returns them alongside the raw Certainties; pass `--shape Consensus` to filter to just the consolidated readouts. The SDK and HTTP API treat them identically. ## Next steps | Need | Page | |------|------| | Install Veritas and see what it creates in your repo | [Install Veritas](/veritas/install/) | | Write `Certainty`, `Support`, `Opposition`; read `Consensus` | [Writing opinions](/veritas/writing-opinions/) | --- # Writing opinions > How to author Certainty, Support, and Opposition assertions, and how to read the Consensus Veritas writes back. After [installing Veritas](/veritas/install/), you write three kinds of opinions and read one. This page walks through each. The examples below use placeholder things and assertions. You'll create your own with `wh thing create` and `wh assertion create` — see [Things](/data-modeling/things/) and [Assertions](/data-modeling/assertions/) for those primitives. Veritas operates on assertions you already have. The examples assume `Article/example-claim` exists as the underlying thing, `Proposition/article-is-accurate` is the binary-proposition assertion being evaluated, and `Source/research-team` is the source whose reputation is wagered. Shape names are illustrative — substitute your own domain shapes (`Observation`, `Thesis`, `Finding`, etc.). Examples below use the CLI. SDK callers submit the same operations via `client.commit.apply`; [MCP](/agent-integration/why-agent-native/) callers use the `warmhub_commit_submit` tool. The CLI exposes `--shape`, `--about`, and `--data` as flags on `wh assertion create`; the SDK and MCP equivalents nest the same values inside a commit operation (`{ operation: 'add', kind: 'assertion', name, about, data }`). See [Write operations](/writes/operations/) for the operation contract. ## Writing a Certainty A `Certainty` is the primitive bet — a source expressing how strongly it believes the target assertion is true: ```bash wh assertion create --shape Certainty \ --about Proposition/article-is-accurate \ --data '{ "belief": 0.8, "disbelief": 0.1, "uncertainty": 0.1, "source": "Source/research-team", "rationale": "Two independent reviewers agreed." }' \ --repo [your-org]/[your-repo] ``` Required fields: - `belief`, `disbelief`, `uncertainty` — must each be in `[0, 1]` and must sum to `1`. - `source` — a [wref](/data-modeling/wrefs/) (the WarmHub reference to a thing) whose reputation is wagered by this Certainty. Optional fields: - `alpha` — base rate in `[0, 1]`. Treated as `0.5` when omitted. - `rationale` — freeform text. Veritas ignores it; it is for human readers. :::note[The target must be a binary proposition] Veritas's algorithm assumes the assertion you point `about` at is a true/false claim. Pointing a Certainty at an open-ended assertion produces valid numbers but no meaningful consensus. See [Opinions as Separate Assertions](/data-modeling/patterns/#opinions-as-separate-assertions) for how to phrase claims as binary propositions. ::: ## Writing a Support or Opposition `Support` and `Opposition` express how one assertion bears on another. Both are written `about` a directional `Pair` of assertions: ```bash wh assertion create --shape Support \ --about pair:Proposition/study-A-confirms,Proposition/study-A-conclusion \ --data '{ "belief": 0.7, "disbelief": 0.05, "uncertainty": 0.25, "source": "Source/methodology-reviewer", "rationale": "Replication followed the same protocol." }' \ --repo [your-org]/[your-repo] ``` Pair direction matters. The **first** member of the pair is the antecedent; the **second** is the consequent. - `Support` says: belief in the antecedent is evidence **for** the consequent. - `Opposition` says: belief in the antecedent is evidence **against** the consequent. Field requirements match `Certainty`: `belief + disbelief + uncertainty = 1`, plus `source` and optional `alpha` / `rationale`. The `pair:a,b` shorthand is CLI-only sugar that creates the underlying `Pair` thing automatically. SDK and MCP callers express the same target as a collection object — `"about": { "pair": ["A", "B"] }` — inside the commit operation. See [Collections](/data-modeling/collections/) for the full collection model. :::caution[Disbelief is not the opposite pathway] Disbelief on a `Support` pathway means "this support relationship does not hold" — not "the assertion opposes." If you want to express opposition, write an `Opposition` assertion. The two pathways are tracked independently, and Veritas resolves cross-pathway conflict during the solve. ::: ## Reading the Consensus Veritas writes `Consensus` assertions in two places: - **About a target assertion**, consolidating that target's native `Certainty` opinions. - **About a `Pair`**, when `Support` and/or `Opposition` opinions exist for that pair. Pair-level `Consensus` is currently diagnostic-only — Veritas writes two rows per pair (one per pathway, with the inactive pathway as a vacuous opinion), but the repo-visible payload does not include a pathway label, so the two rows cannot be reliably distinguished from data alone today. Query Consensus on a target assertion the same way as any other assertion: ```bash wh thing about Proposition/article-is-accurate --shape Consensus ``` Querying pair-level Consensus requires the concrete `Pair` wref, not the `pair:a,b` shorthand. List the pairs in your repo to find the generated name: ```bash wh thing list --shape Pair --repo [your-org]/[your-repo] wh thing about Pair/ --shape Consensus ``` A `Consensus` row returns `belief`, `disbelief`, `uncertainty`, and optional `alpha`. There is no `source` or `rationale` field — `Consensus` is Veritas's own consolidated readout, not a wager. A brand-new target with no inputs has no `Consensus` row yet — Veritas writes `Consensus` only after a solve runs. Retracting all inputs from a target that previously had `Consensus` triggers a vacuous `(0, 0, 1)` write. ## When Veritas re-solves Re-solving means recomputing `Consensus` for an affected target or pair after one of its inputs changes. Veritas reacts to changes on the three input shapes: - A new, revised, or retracted `Certainty` triggers a local re-solve of the target assertion and updates that target's `Consensus`. - A new, revised, or retracted `Support` or `Opposition` triggers a local re-solve of the relation and updates the `Pair`'s `Consensus`. For a fixed set of inputs and reputations, the readout is deterministic and order-independent — the same inputs always produce the same `Consensus`. ## Behavior worth knowing - **One Certainty does not move reputation.** A single source's opinion has nothing to disagree with locally, so reputation only adjusts when multiple Certainties on the same target disagree. Cross-pathway conflict between `Support` and `Opposition` on the same `Pair` can similarly adjust the source's relation-scoped reputation. - **Agreeing Certainties do not move reputation either.** Disagreement is the signal Veritas uses to update trust. - **Vacuous opinions are valid input.** `(0, 0, 1)` is a well-formed "no evidence" opinion and contributes nothing to consensus — useful when you want to register a source's existence without committing to a view. - **Retracting an input re-solves locally.** When you retract a `Certainty`, `Support`, or `Opposition`, Veritas re-solves the affected target or pair as if that input never existed. - **Pairs with no `Support` or `Opposition` assertions are dormant.** A `Pair` thing alone does not produce a `Consensus` until at least one pathway opinion is written about it. - **Update an existing opinion with `wh assertion revise`, not a second `wh assertion create`.** When the Veritas worker sees two active `Certainty` assertions from the same `source` about the same target, it blocks the duplicate at consensus time with a message of the form `Source X has multiple active Certainty assertions about Y; Certainty/Z should have been a revision of Certainty/W.` The same guard applies across `Support` and `Opposition` for the same `Pair`. Revising the existing assertion preserves history and updates Consensus; retract + add is the path when the `source` itself needs to change. ## Related - [Overview](/veritas/overview/) — the conceptual model. - [Installing Veritas](/veritas/install/) — the install flow and what gets created. - [Assertions](/data-modeling/assertions/) — the underlying assertion primitive. - [Collections](/data-modeling/collections/) — `Pair` syntax and structure. --- # Home > The home page — quick statistics and navigation to your organizations. The **Home** page is the landing dashboard you see at `/` after signing in. It gives you a quick snapshot of your WarmHub account and links to navigate deeper. ## Stat Cards At the top of the page, stat cards provide at-a-glance numbers and quick navigation: - **Organizations** — The total number of organizations you belong to. This count updates automatically as organizations are created or removed. - **Repositories** — A quick link to browse repositories across your organizations. ## Recent Activity Below the stat cards, a two-column layout shows: - **Recent Organizations** — Your five most recently created organizations, with direct links to their detail pages. - **Recent Repositories** — Your five most recently created repositories across all organizations. For the full list, visit the [Organizations](/web-ui/organizations/) page via the sidebar. --- # Live Flow > Real-time visualization of organization activity and data flow. **Live Flow** is a full-screen visualization page that renders your organization's repositories, subscriptions, and their activity as an interactive graph. ## Accessing Live Flow Live Flow is available from the organization detail page. Click the Live Flow link to open the visualization in a full-screen view. Live Flow is **members-only**: only members of the org can open it. Signed-out visitors get a sign-in prompt; signed-in visitors who aren't members of this org see the same not-found view as for orgs that don't exist. ## Graph Canvas The main area of the page is an interactive graph canvas. Two types of nodes appear: - **Repo nodes** — represent repositories, showing name, description, and recent activity indicators - **Action nodes** — represent [subscriptions](/subscriptions/overview/) (see [Creating Subscriptions](/subscriptions/creating/) for details), showing their kind, status, and latest run details Three types of edges connect them: - **Subscription edges** — link a repo to its subscriptions. Active subscriptions show an animated particle traveling along the edge. - **Cascade edges** — link an action in one repo to a target repo when a cross-repo cascade is detected. These appear in violet to distinguish them from subscription edges. - **Loopback edges** — render as a self-arc on a single repo when a trace returns to the repo it originated from (an action firing within the same repo). They give same-repo cascades a distinct visual rather than collapsing into the existing subscription edge. You can pan and zoom the canvas to explore different parts of the graph. Click any node to open a detail panel with recent activity (for repos) or run history (for actions). ## Top Toolbar A top bar displays the organization name and includes: - A **Back to organization** button - **View mode** and **Density** display controls - A **Fullscreen toggle** - A **Theme toggle** (light/dark) ### View mode Switch between two presentations of the same flow data: - **Canvas** — The interactive graph described above. Default. - **List** — A linear list of repos and actions for keyboard-friendly scanning, useful when you want to read the flow without spatial layout. Same playback, time scrubber, and detail panels apply. ### Density Independent of the view mode, control how much detail each item shows: - **Detailed** — Shows full labels, metadata, and latest run information on every node. - **Compact** — Reduces visual clutter by hiding secondary labels and metadata, making it easier to see the overall structure at a glance. ## Time Scrubber A timeline control sits at the bottom of the screen with a sparkline histogram showing event density over time. Drag the scrubber backward to see how subscription runs and cross-repo cascades unfolded. Playback controls include: - **Play/Pause** — advances through events. Playback defaults to stepped mode (event-by-event with narration describing each run). - **Step toggle** — switch between stepped playback and continuous playback - **Speed selector** — 1x, 2x, 5x, or 10x playback speed - **Go Live** — return to the real-time view In historical mode, run statuses reflect the scrubbed point in time — a run that completed after the selected time appears as still running. ## Availability Live Flow is unique to the web app. There is no CLI or SDK equivalent that draws the graph, but the same underlying activity is available from the terminal: `wh repo list` and `wh repo view` for the repository side, and `wh sub list`, `wh sub view`, `wh sub log`, and `wh sub attempts` for subscriptions and run history. See the [CLI reference](/cli-reference/commands/) for details. **Programmatic equivalents.** The same data is reachable through the SDK: [`client.repo.*`](/sdk/client/#clientrepo) for repositories, [`client.subscription.*`](/sdk/client/#clientsubscription) to manage subscriptions, and [`client.action.*`](/sdk/client/#clientaction) for run and attempt history and delivery feeds (the counterpart of `wh sub log` / `wh sub attempts`). [`client.live.*`](/sdk/client/#clientlive) streams live updates. See [Subscriptions](/subscriptions/overview/) for the narrative. --- # Organizations > Browse organizations, manage members, and access repositories. An **organization** is the top-level grouping in WarmHub. It owns repositories, holds members, and controls access. ## Organizations List When you're signed in, the organizations page displays a grid of cards — one for each organization you belong to. Each card shows the organization's display name and description, and a **Create Organization** button sits at the top to add a new one. If you aren't signed in, private deployments show an in-app welcome card with a **Sign in** button on this route (rather than an automatic redirect). Some deployments allow public browsing; on those, this page shows an empty grid (you only see organizations you belong to) but you can still open a specific org's detail page if you have its URL. ## Organization Detail Click any organization card to open its detail page. The layout is split into two areas. ### Main Content - **Repository list** — Repositories in the organization, displayed in a paginated list. Members see all repos; non-members see only public repos. (Signed-out visitors only reach this page on public-browsing deployments — see above.) If there are more repositories than fit on one page, click **Load More** to fetch the next batch. - **Create Repository** — A button that opens a dialog for adding a new repository. Visible to org owners and admins. (See the [access reference](/auth/access-reference/#roles-and-capabilities) for what each role can do.) ### Right Sidebar - **About section** — Shows the organization description and creation date. Users with `org:settings` permission (admins and above) see a pencil icon next to the "About" heading — click it to edit the description in a markdown editor dialog. - **Members and roles** — Signed-in members can see who is in the org and each member's role. The **Manage members** controls (people icon + role editing dialog) are limited to owners and admins. ### Member Management Users with `org:manage_members` permission (admins and above) see a **people icon** button in the page header (labeled "Manage members" for screen readers). Click it to open a dialog where you can add new members or change existing roles. Each member has one role — viewer, editor, admin, or owner. For exactly what each role can and can't do (and which scope a token needs for a given task), see the [access reference](/auth/access-reference/). For sign-in and account setup, see [Getting Access](/auth/getting-access/). ### Live Flow Each organization has a **Live Flow** link that opens the real-time flow visualization. Live Flow is members-only — only members of the org can open it. See [Live Flow](/web-ui/live-flow/) for details on what this view offers. ## CLI Equivalents Many organization tasks can also be performed from the terminal: - `wh org list` — list organizations you belong to. - `wh org create` — create a new organization. - `wh org view ` — inspect a specific organization. - `wh org member list ` — list members and roles for an organization. - `wh org member add --role ` — invite a member with a role. - `wh org member set-role --role ` — update a member's role. - `wh org member remove ` — remove a member from an organization. See the [CLI documentation](/cli-reference/overview/) for the full command reference. --- # Web App Overview > Browse and manage WarmHub data through the browser dashboard. The **web app** is a browser-based dashboard for browsing organizations, repositories, things, assertions, and action activity in WarmHub. ## Access Modes Most WarmHub deployments require sign-in to reach supported app routes. Some deployments allow **public browsing**, where signed-out visitors can reach a subset of routes without an account. If you don't know which mode applies to your deployment, ask your administrator or check the [authentication documentation](/auth/getting-access/). When public browsing is enabled, here's what you can see at each route: | Route | Signed out | Signed in (non-member) | Signed in (member) | |---|---|---|---| | `/organizations` | Empty grid | Only your orgs | Only your orgs | | `/orgs/` | Org info + public repos | Org info + public repos | Org info + all repos | | `/orgs//repos/` (public) | Full repo page | Full repo page | Full repo page | | `/orgs//repos/` (private) | Authentication required | Not found | Full repo page | | `/orgs//flow` | Sign-in prompt | Not found | Live Flow | A few additional notes: - **Editing is permission-gated.** README edits, commit creation, repo settings, and member management only appear when your role allows them. Tabs for actions you can't perform (such as the Actions tab on a repo where you don't have settings) are hidden, and visiting a hidden tab via URL quietly returns you to the default tab — no error panel. - **Unknown URLs still render 404.** Even on private deployments, unsupported routes (including removed paths) render the Not Found page instead of the sign-in screen. See the [authentication documentation](/auth/getting-access/) for details on signing in and managing your session. ## Navigation Layout The interface is organized around three areas: - **Sidebar** — A collapsible panel on the left with links to Home and the Organizations list. Collapse it to give the main content area more room. - **Header** — Sits at the top of most pages. Contains a user menu with a theme toggle (light/dark) and a sign-out option. (Full-screen pages like [Live Flow](/web-ui/live-flow/) use their own toolbar instead.) - **Breadcrumb trail** — Appears below the header when you are inside an organization or repository, showing your current location so you can navigate back to any parent page with one click. ## Page Hierarchy Pages form a simple drill-down — Home → Organizations → Organization Detail → Repository Detail — with the breadcrumb trail jumping back up at any level. | Need | Page | |------|------| | The landing dashboard — stats and quick links | [Home](/web-ui/home/) | | Browse your organizations, then a single org's repos, members, and settings | [Organizations](/web-ui/organizations/) | | Watch real-time org activity (opened from org detail; members-only) | [Live Flow](/web-ui/live-flow/) | | A repository's things, assertions, actions, and components (tabbed) | [Repository Detail](/web-ui/repo-detail/) | In addition to the main drill-down hierarchy, the header provides access to: - **Notifications** — A bell icon in the header opens your notification feed, showing action run results and other repo-level events. - **Settings** — Accessible from the user menu, this page lets you manage your account preferences. Each level links naturally to the next, and the breadcrumb trail lets you jump back up from organization and repository pages. --- # Repository Detail > The central hub for exploring a repository — header, tabs, and permissions. The **repository detail** page is the main interface for working with a single repository. It combines a header with key metadata, a tab bar for navigating different views of the data, and permission-aware controls for editing. ## Header The top of the page displays: - **Repository name** in large text. - **Visibility badge** — a globe icon for public repositories or a lock icon for private ones. - **MCP badge** — a clickable badge displaying the repository's [Model Context Protocol](/agent-integration/mcp-server/) (MCP) endpoint path. Click it to copy the full MCP URL to your clipboard. - **Actions dropdown** — a pencil icon button (visible only to users with `repo:settings` permission, i.e. admins and above) that opens a menu with options to rename the repository or edit its description. - **Description** — the repository's description is displayed directly below the header line. ## Tab Navigation Below the header, tabs organize the repository's content: 1. **[About](/web-ui/repo-tabs/about-tab/)** (default) — README, data dictionary, and sidebar info. 2. **Things** — Thing browsing and search. See [Things](/data-modeling/things/) and [Wrefs](/data-modeling/wrefs/) for the data model behind this tab. 3. **[Assertions](/web-ui/repo-tabs/assertions-tab/)** — Assertion browsing for the repository, with source reputation, filters, and BDU meters. See [Assertions](/data-modeling/assertions/) for the underlying data model. 4. **[Actions](/web-ui/repo-tabs/actions/)** — Webhook subscription management. Only visible to users with `repo:settings` permission. 5. **[Components](/web-ui/repo-tabs/components/)** — Installed components and their configuration. Non-default tabs are reflected in the URL as a query parameter (for example, `?tab=actions`), so you can bookmark or share a direct link to any tab. The default About tab omits the parameter. ## Permissions Repository actions are gated by role-based permissions. For background on how authentication works, see [Authentication](/auth/getting-access/). The table below maps each permission string to the minimum role required: | Permission | Minimum role | Grants | |---|---|---| | `repo:settings` | Admin | Manage subscriptions, credentials, rename repo, edit description | | `things:write` | Editor | Create and revise things (including the README) | - **Read access** — All tabs except Actions are visible to anyone who can view the repository. Browsing data does not require write access; use `wh thing history` for per-thing version trails. - **Actions tab** — requires `repo:settings` (admins and above). Users without this permission do not see the tab. - **Write access** — Different actions require different permission levels: - **Edit README** — requires `things:write` (editors and above), because the README is stored as a thing and goes through the write path. - **Rename repo / edit description** — requires `repo:settings` (admins and above). - **Actions dropdown** — only visible to users with `repo:settings` permission. ## CLI Equivalent To view repository metadata from the terminal, use `wh repo view`. For a full list of CLI commands, see the [CLI reference](/cli-reference/commands/). --- # About > Repository README, data dictionary, and sidebar information. The **About tab** is the default view when you open a repository. It provides a high-level overview of the repository's purpose and data model. ## Two-Column Layout The About tab uses a two-column layout. The main content area on the left displays the README and data dictionary. A narrower sidebar on the right shows repository metadata. ## README The left column leads with the repository's README, rendered as Markdown with full GitHub Flavored Markdown (GFM) support — including tables, task lists, and fenced code blocks. The README is stored as `Content/Readme` — a well-known instance of the built-in [Content shape](/data-modeling/content-shape/). The same content is accessible via `wh repo content get --kind readme`, `client.repo.getReadme`, and `GET /{org}/{repo}/readme.md`. ### Editing the README If you have write access, an edit button appears above the README. Clicking it switches to a Markdown editor where you can: - Write or revise the content in a plain Markdown textarea. - Save your changes, which are committed automatically with an appropriate message (e.g., "Update README"). If the repository does not yet have a README, authorized users see a prompt to create one. ## Data Dictionary Below the README, the **data dictionary** section lists every [shape](/data-modeling/shapes/) defined in the repository alongside its fields and types — a quick reference for understanding the data model without leaving the About tab. Each shape entry collapses by default. The summary row shows: - **Shape name** — the identifier used in commits and [assertions](/data-modeling/assertions/). - **Description** — a short inline summary, when the shape definition includes one. - **Component badge** — the source component, when the shape was installed via a component. - **Version badge** — the current version of the shape definition (e.g. `v3`). - **Instance count** — the total [things](/data-modeling/things/) and [assertions](/data-modeling/assertions/) for this shape, when greater than zero. - **Field count** — the number of fields the shape defines. Expand an entry to see the **field grid**: each field's name, type (e.g. `string`, `number`, `boolean`), and description. When the shape has matching [things](/data-modeling/things/) or [assertions](/data-modeling/assertions/), jump-to buttons appear at the bottom of the expanded view. Clicking one navigates to the Things or Assertions tab filtered to that shape via `?tab=things&shape=` or `?tab=assertions&shape=` — both query parameters are required, since `shape` alone doesn't switch away from the default About tab. ## Sidebar The right column displays: - **Description** — The repository's description, with an edit button for authorized users. - **Created** — The date the repository was created. ## CLI Equivalents - To list shapes from the terminal, use `wh shape list`. - To inspect shape fields from the terminal, use `wh shape view `. - To inspect repository-level schema context, use `wh repo describe [org/repo]`. - README content is stored as `Content/Readme`; fetch it with `wh repo content get org/repo --kind readme`, edit it with `wh repo content set org/repo --kind readme --file readme.md`, or view version history with `wh thing history Content/Readme`. - See [Content Shape](/data-modeling/content-shape/) for the full CLI, SDK, MCP, and raw HTTP fetch matrix. --- # Actions > Manage subscriptions, view run history, and configure credentials. The Actions tab is the control center for managing a repository's automated triggers. It is only visible to users with `repo:settings` permission (admins and above). ## Subscriptions Subscriptions are displayed as cards in a grid. Each card shows: - **Name** and **kind badge** — webhook subscriptions show a blue badge; other kinds (e.g. deactivated legacy rows) render a muted badge - **Status indicator** — a green dot with "active" label, or a gray dot with "paused" label - **Last run status** — the most recent execution result Click any card to open the subscription detail view. ### Subscription Detail The detail view shows quick stats (last run, total runs in the past 7 days, success rate) and provides: - **Pause / Resume** controls to toggle the subscription on or off - **Configuration** — a collapsible section listing the webhook URL, shape filter, and bound credential sets ### Run History Below the configuration, a list of recent runs (up to 200) shows: - **Status badge** — each run shows one of these states: - **succeeded** — completed successfully - **running** — currently executing - **processing** — accepted by the handler and continuing asynchronously while WarmHub waits for a callback - **pending** — queued and waiting to start - **Retrying** — failed but scheduled for automatic retry - **Failed** — non-retryable terminal failure; requires investigation - **Dead Letter** — retries exhausted without success; will not retry automatically - Run ID - Attempt count (e.g., "1/3 attempts") - Error codes and messages (if any) Expand any run to see individual attempt details — timing, HTTP status, and response snippets. ## Credentials The Actions tab also provides credential management: - **Create** credential sets with a name, scope (repo or organization), and optional description - **View** existing credential sets with their key names, scope, and status - **Bind** a credential set to a subscription for authenticated webhook delivery - **Revoke** a credential set permanently (with an optional reason) ## CLI Equivalents - `wh sub list` — list subscriptions - `wh sub view ` — subscription details - `wh sub log ` — run history - `wh credential list` — list credential sets See the [CLI reference](/cli-reference/commands/) and [subscriptions documentation](/subscriptions/overview/) for more details. --- # Assertions > Browse a repository's assertions with source reputation, filters, and belief meters. The **Assertions tab** is the browsing surface for every [assertion](/data-modeling/assertions/) in a repository. It pairs an assertion list with a detail panel, surfaces per-source reputation, and provides filter controls. For some assertions it also renders a belief/disbelief/uncertainty (BDU) meter — see [When BDU Meters Appear](#when-bdu-meters-appear) for which ones. ## Source Reputation Panel Once any assertions have loaded, the tab leads with a collapsible **Source Reputation** panel that summarizes the loaded assertions by source. Assertions whose payload omits a `source` field are bucketed under a single `unknown` row, so the panel is present even for repositories that don't tag sources — those repos just see one `unknown` row. The panel header shows a shield icon and a count of distinct sources (`unknown` counts as one source). Expanded, each source renders as a row with: - **Source name** and the number of assertions it has authored *in the currently loaded set*. - **Inline BDU meter** showing the source's average belief, disbelief, and uncertainty across its loaded assertions. These counts and averages are computed from the assertions the tab has loaded so far, not the full repository. Authenticated viewers walk every page automatically, so they see the full picture. Anonymous viewers are capped (see [Anonymous viewing](#anonymous-viewing) below) and so see source aggregates only over the loaded subset. Disbelief is derived from `1 − avgBelief − avgUncertainty` (floored at zero), so the meter reflects an aggregate stance per source rather than per assertion. ## Filter Bar Six filter controls sit above the assertion list: - **Search** — free-text match against assertion name, [wref](/data-modeling/wrefs/), shape name, and about target. - **Shape** — restrict to a single [shape](/data-modeling/shapes/), populated from shapes present in the loaded assertions. - **Belief band** — narrows the list by how confident an assertion is: - *All assertions* — no filter. - *High belief* — strong belief, low uncertainty. - *Moderate belief* — middling belief, low uncertainty. - *Low belief* — weak belief, low uncertainty. - *High uncertainty* — uncertain enough that the belief level is unreliable, regardless of where it sits. Any assertion in the high-uncertainty zone is grouped under *High uncertainty* rather than the matching belief band. - **Sort** — *Strongest belief* (default), *Most uncertain*, *Alphabetical*, or *Highest version*. Belief- and uncertainty-based sorts tie-break on the opposite axis, then by name. - **About** — restrict to assertions whose `about` ref contains the chosen target string (substring match, case-insensitive). The selector is searchable and is populated from the about refs present in the loaded assertions; selecting one target can also match assertions whose about-ref includes that string as a substring. - **Group by** — *No grouping* (default), *Source*, *Shape*, or *Target*. A **Clear filters** button appears when any filter is active. Assertions without an opinion are treated as belief 0 / disbelief 0 / uncertainty 1 for filtering and sorting purposes, so they cluster under *High uncertainty* and at the bottom of belief-sorted lists. ## Assertion List The center column lists assertions matching the current filters. The header shows the result count and a `(loading...)` indicator while pages are still being fetched. Each row shows: - **Name** with a **belief band pill** (when an opinion is present). - **Shape name**, an arrow, and a link to the `about` target (when set). - **Source attribution** (`by `) when the assertion declares a source. - **Inline BDU bar** on medium screens and above (when an opinion is present). - **Version pill** (e.g. `v2`). Click any row to select the assertion — the detail panel opens and the selection is reflected in the URL via the `assertion` query parameter, so links are shareable. ### Grouping When **Group by** is set to *Source*, *Shape*, or *Target*, sticky group headers separate the list. Source-grouped headers additionally show **avg belief** and **avg uncertainty** percentages — these averages are the same per-source aggregates shown in the Source Reputation panel (computed over every loaded assertion from that source, not just the assertions visible inside the current filter). ### Anonymous viewing Unauthenticated viewers see a capped result set. When more assertions exist than the anonymous viewing limit allows, the list footer reads *"Showing the first N assertions"* with a sign-in prompt. ## Detail Panel Selecting an assertion opens the detail panel on the right: - **Name** + **belief band pill** (when an opinion is present), **wref link**, and **version pill** in the header. - **Full-width BDU meter** with numeric values, when the assertion carries an opinion. - **Metadata grid** with four fields: - **Shape** — the [shape](/data-modeling/shapes/) the assertion was committed under. Falls back to `unshaped` if the assertion has no shape. - **About** — the [wref](/data-modeling/wrefs/) of the thing this assertion is about, in the form `@v` (the backend version-pins the about-target at commit time). Falls back to `none` for assertions with no `about` target. - **Pinned wref** — the selected assertion's own pinned identity at commit time, in the form `@v`. Falls back to `unavailable`. (Distinct from About above: that's the *target's* pinned wref; this is *this assertion's* pinned wref.) - **Validated shape** — the shape the assertion was validated against at commit time, when recorded. Falls back to `unavailable`. - **Clear selection** button. ### Related Assertions A **Related assertions** section lists other assertions that share the selected assertion's `about` ref, grouped by shape. Each entry shows the assertion name, wref, version, and a full-width BDU meter when an opinion is present. Clicking an entry switches the selection to that assertion. The list is currently capped and the tab doesn't yet paginate the related panel, so larger sibling sets are silently truncated: - **Signed-in viewers** see at most **50** related assertions. - **Anonymous viewers** are additionally clamped by the site-wide anon limit and see at most **25** related assertions, plus a sign-in-to-see-more footer when more exist. When the selected assertion has no `about` ref, the section displays *"This assertion does not have an `about` ref."* ### Raw JSON The detail panel ends with a **Raw JSON** block that pretty-prints the selected assertion's `data` payload — just the payload, not the wrapping name, shape, version, or about ref already shown above. If the assertion has no payload, the block reads *"No raw payload available"*. ## When BDU Meters Appear The tab today only renders meters for assertions whose payload uses **one specific opinion-payload shape** — a nested object at `data.opinion` with the short-form numeric keys `b`, `d`, and `u` (any one of which is sufficient; missing axes default to 0, except `u` which defaults to 1). A nested `opinion` object that uses long-form keys (`opinion.belief`, etc.) does **not** match and won't render a meter. This nested shape is *not* the WarmHub-canonical convention; the [opinions-as-separate-assertions](/data-modeling/patterns/#opinions-as-separate-assertions) pattern stores opinion values at the **top level** of the payload (as `b`, `d`, `u`, `a`), and the [Veritas](/veritas/overview/) shapes `Certainty`, `Support`, `Opposition`, and `Consensus` likewise store `belief` / `disbelief` / `uncertainty` / `alpha` at the top level. Concretely, that means: - Assertions emitted under the canonical separate-opinion pattern with top-level `(b, d, u, α)` keys — **do not** currently render a meter in this tab. - Assertions emitted by Veritas — **do not** currently render a meter in this tab. - Assertions whose payload happens to nest opinion values under an `opinion` field — **do** render a meter. - Assertions with no opinion at all — render without a meter; they still appear in the list and detail panel with shape, source, version, and Raw JSON. When opinion numbers are present in the payload, they're visible in the detail panel's Raw JSON block regardless of whether a meter renders. ## From the terminal To do the same kinds of things from the terminal that this tab supports in the browser: - **Browse every assertion in a repo** — `wh thing list --kind assertion --all --repo `. (The default page size is 50; use `--all` to walk every page, matching what the tab shows authenticated viewers.) - **Inspect a single assertion** — `wh thing view `. Returns the full thing record, including the `data` payload that the tab pretty-prints in **Raw JSON**. - **Find other assertions about the same target** — `wh thing about `. To exactly match the [Related assertions](#related-assertions) panel, pass the **version-pinned** target wref (e.g. `Location/cave@v3`) — the panel queries against the pinned `aboutWref` that the selected assertion was committed against. Passing the bare identity (e.g. `Location/cave`) is also valid CLI usage but returns assertions across *all* versions of the target, which is a broader set than the panel shows after the target has been revised. The CLI also returns the selected assertion itself; the panel filters that row out client-side. - **Trace a single assertion's history** — `wh thing history `. See [Assertions](/data-modeling/assertions/) for the underlying data model and [CLI reference](/cli-reference/commands/) for the full command surface. --- # Components > Browse installed components, metadata, and version history. The Components tab shows installed components in the repository (up to 200), with their metadata and change history. ## Header The page header includes a **Count** summary card displaying the number of currently displayed components. Note that this reflects the entries returned by the current query — on repositories with more records than the page limit, or for anonymous viewers subject to a 25-record cap, this count may be lower than the true repository total. ## Split-Pane Layout The Components tab uses a split-pane layout with an index panel on the left and a detail panel on the right. ### Left Panel — Component Index A fixed-width sidebar (260px) contains a scrollable list of all installed components. Each entry in the list shows: - **Component name** — the primary identifier - **Source repository label** — derived from the install URL, indicating where the component originates - **Last updated timestamp** — when the component was most recently changed - **Version badge** — the current version number - **Commit SHA** — a short hash displayed in monospace, identifying the exact commit Click any entry to select it and view its full details in the right panel. ### Right Panel — Component Detail When a component is selected, the detail panel displays comprehensive information. #### Detail Header The header shows the component name along with badges for the installed component version, record version, and update count (the record version is bumped each time the component is installed or updated; see [Components overview](/components/overview/) for the underlying data model). #### Metadata Grid A responsive grid (two columns on medium screens and above, single column on small screens) presents six metadata fields: - **Source repo** — the repository the component was installed from - **Installed version** — the version that was originally installed - **Commit SHA** — the full commit identifier - **Latest update** — when the component was last modified - **Tracking thing** — the [wref](/data-modeling/wrefs/) of the thing this component tracks, displayed as monospace text - **Status** — the current state of the component The source repository value links out to the external repository URL for easy navigation to the upstream source. #### Recent Changes Timeline Below the metadata, a timeline lists the component's version history (up to 20 entries). Each entry in the timeline shows: - Version number - Operation type — the underlying commit operation that produced this version: - `add` means the component entry was created (initial install) - `revise` means the existing component entry was updated (for example, version, metadata, or status changes) - `retract` means the component entry was removed/deactivated - Component name - Timestamp of when it occurred ## Empty State If no components are installed in the repository, the tab displays an empty state message. ## Related - `wh component list` and `wh component view` are the CLI equivalents for browsing components from the terminal - [Components overview](/components/overview/) for the underlying data model --- # Write History > Auditing changes — per-thing version trails replace the commit log. WarmHub records the full version history of every thing. There is no separate commit log and no repo-wide commit list. Use the per-thing version trail instead. ## Per-Thing History ### Via CLI ```bash # Show every version of a thing, newest first wh thing history Sensor/temp-1 # JSON output for programmatic consumption wh thing history Sensor/temp-1 --json ``` Example output: ``` History: Sensor/temp-1 thing Sensor/temp-1@v3 revise 1m ago Sensor/temp-1@v2 revise 10m ago Sensor/temp-1@v1 add 1h ago ``` Each row shows: pinned wref, operation kind, and relative time. Use `--json` for the full structured history payload. ### Via MCP ```json { "name": "warmhub_thing_history", "arguments": { "wref": "Sensor/temp-1", "limit": 20 } } ``` ### Via SDK ```ts await client.thing.history(orgName, repoName, { wref: "Sensor/temp-1", limit: 20, }) ``` ## What's In The Result | Field | Meaning | |---|---| | `wref` | Pinned wref of the version, e.g. `Sensor/temp-1@v3` | | `version` | Monotonic version number per thing | | `operation` | `add`, `revise`, or `retract` | | `createdAt` | Timestamp (unix milliseconds) | | `data` | The version's payload (snapshot at that version) | | `active` | Whether this version is active in the current history view | | `retractReason` | Optional reason recorded on a `retract` operation | | `thingName`, `thingKind`, `shapeName` | Included when needed to describe rows returned by shape/about/history filters | | `committerWref` | Local or canonical wref of the committer thing when the originating write recorded one. | ## Filtering `wh thing history` accepts `--limit` to bound the page and `--cursor` for pagination. There is no committer filter today — filter the JSON output client-side if you need it. ## Model Per-thing history is the source of truth for write history. Each entry is a versioned snapshot for one thing, so audit and rollback workflows start from the affected wref rather than a repo-wide commit envelope. --- # Operations > Add, revise, and retract operations for shapes, things, and assertions — plus inline collection syntax. Every write request contains one or more **operations**. There are three operation types — **add**, **revise**, and **retract** — across **shape**, **thing**, **assertion**, and **collection** records. Collections provide [inline syntax](/data-modeling/collections/#inline-syntax) for creating grouped things. A write request can include multiple operations. CLI (`wh commit submit`), SDK (`client.commit.apply`), and MCP (`warmhub_commit_submit`) writes apply operations one at a time — if a later operation fails, earlier ones are still in place, and you can retry just the failed ones. See [Writes](/writes/overview/). ## ADD Operations ### ADD Shape Create a new shape with field definitions: ```json { "operation": "add", "kind": "shape", "name": "Location", "data": { "fields": { "x": "number", "y": "number", "label": "string" } } } ``` ### ADD Thing Create a new thing under an existing shape: ```json { "operation": "add", "kind": "thing", "name": "Location/cave", "data": { "x": 3, "y": 7, "label": "Dark Cave" } } ``` ### ADD Assertion Create an assertion about another thing. The `about` field is **required**: ```json { "operation": "add", "kind": "assertion", "name": "Observation/cave-safe", "about": "Location/cave", "data": { "safe": true, "confidence": 0.8 } } ``` `about` accepts local wrefs, canonical wrefs, or [inline collection syntax](/data-modeling/collections/#inline-syntax). ## REVISE Operations Revise only changes data. It does **not** accept an `active` field — to mark an entity inactive, use a [retract operation](#retract-operations) instead. TypeScript SDK users see this as a compile error because `ReviseOperation` declares `active?: never`. ### REVISE Shape Update a shape's field definitions: ```json { "operation": "revise", "kind": "shape", "name": "Location", "data": { "fields": { "x": "number", "y": "number", "z": "number" } } } ``` ### REVISE Thing Update a thing's data: ```json { "operation": "revise", "kind": "thing", "name": "Location/cave", "data": { "x": 5, "y": 3, "label": "Bright Cave" } } ``` ### REVISE Assertion Update an assertion's data. The `about` target **cannot be changed**: ```json { "operation": "revise", "kind": "assertion", "name": "Observation/cave-safe", "data": { "safe": false, "confidence": 0.2 } } ``` ## Collection Operations Collections are a convenience for creating grouped things (pairs, triples, sets, lists). Under the hood, the commit pipeline expands collection operations into standard thing operations — collections are things with built-in shapes. ### ADD Collection Create a collection thing explicitly: ```json { "operation": "add", "kind": "collection", "type": "pair", "members": ["Location/A", "Location/B"] } ``` The `name` field is optional — if omitted, a canonical name is derived from the members (e.g., `Pair/Location/Av1+Bv1`). You can also create collections inline within an assertion's `about` field. See [Collections](/data-modeling/collections/) for the full syntax. ### Retracting a Collection To mark a collection inactive, use a RETRACT operation (see [RETRACT Operations](#retract-operations)): ```json { "operation": "retract", "name": "Pair/Location/Av1+Bv1" } ``` ## RETRACT Operations Mark an entity inactive. The entity's data and version history are preserved; it is hidden from default HEAD queries. ```json { "operation": "retract", "name": "Location/cave", "reason": "replaced by Location/dungeon" } ``` The `reason` field is optional (max 500 chars). The `kind` field is an optional safety hint — the operation will error if the resolved entity's kind doesn't match. Retract is the **only** path to setting an entity inactive. Once retracted, you can add a new entity at the same name to create a fresh identity. ## Operation Rules ### Data Requirements - `data` is **required** on revise for shape, thing, and assertion - **Revise is a full replacement** — you must include all fields in `data`, not just the ones that changed - Omitted fields will be absent in the new version, which will fail shape validation if they are required - `about` is **required** on ADD assertion, **immutable** on REVISE assertion ## Conditional Operations When you retry a write or have more than one writer touching the same data, you often want an operation to apply only under a condition: skip it if the target already exists, reject it if someone changed the target first, or do nothing when the data is unchanged. WarmHub supports three conditional patterns. The first two — `skipExisting` and `expectedVersion` — are opt-in fields you set on an operation. The third happens automatically. ### Add if missing By default, adding a name that already exists fails with a `CONFLICT` error (`Thing "Location/cave" already exists`). Set `skipExisting` on an `add` to return `operation: "noop"` instead — the existing entity is left untouched and the write does not fail. ```json { "operation": "add", "kind": "thing", "name": "Location/cave", "data": { "x": 3, "y": 7, "label": "Dark Cave" }, "skipExisting": true } ``` This makes an `add` idempotent, which is what you want when re-running a seed script or [retrying a write safely](/sdk/transient-retry/). `skipExisting` is available on `add` for shapes, things, assertions, and collections. On the CLI it is the `--skip-existing` flag on `wh commit submit`; in the SDK it is a field on each `add` operation or a top-level option on `client.commit.apply`. ### Revise if version matches By default, a `revise` overwrites the current version no matter who wrote it last. To guard against a lost update — you read v3, another writer commits v4, and your revise would silently clobber it — pass `expectedVersion`. The revise applies only if the target is still at that version; otherwise it is rejected with a `CONFLICT` error whose `details.reason` is `"expected_version_mismatch"`, carrying the expected and current version numbers so you can refetch and retry. ```json { "operation": "revise", "kind": "thing", "name": "Location/cave", "data": { "x": 5, "y": 3, "label": "Bright Cave" }, "expectedVersion": 3 } ``` `expectedVersion` is available on `revise` for shapes, things, and assertions. On the CLI it is the `--expected-version` flag on `wh thing revise` (and `wh commit submit --revise`); in the SDK it is a field on the revise operation. When you need exclusive access across a read-modify-write rather than an optimistic check, use a [read lease](/sdk/write-methods/#read-leases) instead. ### Idempotent revise If a `revise` produces the same data as the current version, it returns `operation: "noop"` — no new version is created. (WarmHub compares server-computed data hashes; clients never generate them.) Re-submitting an unchanged `revise` is therefore safe and leaves version history untouched. For the full SDK signatures see [Write Methods](/sdk/write-methods/); for the CLI flags see [Commands](/cli-reference/commands/); for the MCP tool schema see [MCP Tools Reference](/agent-integration/mcp-tools-reference/). ## Illegal Operation Sequences Within a single chunk, the following sequences on the **same target** are rejected by preflight: | Sequence | Allowed? | |----------|----------| | ADD + ADD | No — duplicate add | | REVISE + ADD | No — can't add something that already exists | | ADD + REVISE | Yes — create then immediately update | | REVISE + REVISE | Yes — multiple updates in sequence | TypeScript callers binding an inline operation literal to a variable should either annotate it `: Operation[]` or use `satisfies Operation[]` to keep the `operation` discriminant narrowed — see [SDK Write Methods — Typing Operation arrays](/sdk/write-methods/#typing-operation-arrays). ## Batch Files The `--file` flag on `wh commit submit` accepts a JSON file containing an array of operations. For large datasets, prefer JSONL streaming (one operation per line): ```bash # JSON array (small/medium batches) wh commit submit --file operations.json --message "batch update" --repo org/repo # JSONL stream (large datasets — chunked automatically; --stream-id + --skip-existing required) wh commit submit --file dataset.jsonl --stream-id bulk-import --skip-existing --progress -m "bulk import" --repo org/repo ``` The JSON file must contain a JSON array of operation objects: ```json [ { "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 } } ] ``` ### Generating templates Use `wh shape template` to scaffold operations from shape definitions (template generation lives in the `shape` domain): ```bash # Single shape -> thing scaffold wh shape template Hypothesis --repo org/repo # Multiple shapes at once wh shape template Hypothesis Evidence Decision --repo org/repo # Assertion scaffold for a shaped claim about a target wh shape template Hypothesis --kind assertion \ --about ResearchTopic/example --repo org/repo # Write to file, then edit and commit wh shape template Hypothesis Evidence -o experiment.json --repo org/repo $EDITOR experiment.json wh commit submit --file experiment.json -m "add experiment" --repo org/repo ``` The template fills fields with placeholder values (`""`, `0`, `false`) and `FILL_IN:` hints for fields with descriptions. Replace placeholders with real data before writing. Shapes define the payload schema, not whether an operation is a thing or an assertion. To scaffold an assertion, pass `--kind assertion`; add `--about ` when you want a concrete target instead of the default placeholder. ### Streaming large files For large datasets, use JSONL format (one operation per line) with streaming. JSONL paths require `--stream-id` and `--skip-existing`: ```bash wh commit submit --file dataset.jsonl --stream-id bulk-ingest --skip-existing --progress -m "bulk ingest" --repo org/repo ``` ## Token System The `$N` (allocate) and `#N` (reference) tokens let you create entities and reference them within the same stream — for example, adding a thing and an assertion about it in the same chunk, or in a later chunk of the same stream. Token state is scoped to the `streamId` and is echoed back on each `stream.append` response so the next chunk can resolve `#N` references against earlier allocations. `#N` cannot reference future tokens — order operations so adds precede the references that depend on them. See [Wrefs: Batch Tokens](/data-modeling/wrefs/#batch-tokens) for the full syntax, rules, and examples. --- # Writes > How WarmHub turns operations into versioned graph state. All mutations in WarmHub flow through one operation pipeline. Operations apply one at a time, and each change is recorded as a new version on its thing's own history (`wh thing history`) — not in a separate commit or batch record. ## Operation Kinds | Operation | Effect | |---|---| | `add` | Create a thing, shape, assertion, or collection at version 1 | | `revise` | Append a new version with changed data | | `retract` | Withdraw a thing/shape/assertion/collection from default reads (name remains reserved) | Use `retract` for lifecycle changes. `revise` changes data only and does not accept `active`. ## Atomicity CLI, SDK, and MCP writes (`wh commit submit`, `client.commit.apply`, and `warmhub_commit_submit`) apply operations one at a time. If a later operation fails, earlier ones are still in place — the per-operation results array tells you what succeeded, what no-op'd, and what failed, so you can retry just the failed ones. Request-level failures such as authentication, malformed operation JSON, rate limits, or infrastructure errors still reject the whole request. Use `#N` references or structured collection `about` to chain operations within a request — a dependent operation fails with a `DEPENDENCY_FAILED` or `UNRESOLVED_TOKEN` error when its prerequisite didn't land. ## Submitting Operations ### Via CLI ```bash # Single operation wh commit submit --add temp-1 --shape Sensor \ --data '{"location": "Building A", "type": "temperature"}' \ -m "Add sensor" --committer Agent/bot-1 # Multiple operations — repeat --add paired with --data (≤20 ops) wh commit submit \ --add Sensor/temp-1 --data '{"location":"A"}' \ --add Sensor/temp-2 --data '{"location":"B"}' \ -m "Two sensors" # Batch from JSON file wh commit submit --file ops.json -m "Bulk import" # Stream large datasets from a JSONL file (--stream-id + --skip-existing required) wh commit submit --file dataset.jsonl --stream-id 100k-import --skip-existing --progress -m "100k import" # Stream from stdin producer | wh commit submit --stream --stream-id pipe-ops --skip-existing -m "Pipe ops" # Resume a partial seed — rerun with the same stream id; --skip-existing makes adds idempotent wh commit submit --file dataset.jsonl --stream-id 100k-import --skip-existing -m "Resume seed" ``` The `--committer` flag is optional. The shorthand flags (`--add`, `--data`, `--shape`, `--about`, `--kind`, `--retract`, `--reason`) are repeatable and pair by index. See [Commit Create Deep-Dive](/cli-reference/write-submit-deep-dive/). `committer` is a [wref](/data-modeling/wrefs/), not a free-form label — `Agent/bot-1` resolves to the `Agent` shape's `bot-1` thing, and that thing must already exist in the repo. A bare name like `eval-runner` is rejected with a "Thing wref required" error. Use a cross-repo wref (`wh:other-org/other-repo/Agent/bot-1`) to attribute writes to an agent that lives in another repo. ### Via MCP ```json { "name": "warmhub_commit_submit", "arguments": { "committer": "Agent/claude", "message": "Add sensor with reading", "operations": [ { "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" } } ] } } ``` ### Via SDK ```ts await client.commit.apply(orgName, repoName, "Add sensor", [ { operation: "add", kind: "thing", name: "Sensor/temp-1", data: { location: "Building A" } }, ]) // Direct streaming surface for large or chunked workloads await client.stream.append({ orgName, repoName, streamId: crypto.randomUUID(), operations: [...], }) ``` `client.stream.append()` is the low-level stream wire API and returns backend entry statuses (`success`, `noop`, `failed`). The commit-oriented surfaces (`client.commit.apply()`, `OperationBuilder.commit()`, CLI, and MCP) normalize those to the public commit statuses shown below: `applied`, `noop`, and `error`. ### HTTP-Oriented Clients WarmHub does not mount a REST endpoint for writes. HTTP clients should use the SDK, CLI, or MCP surfaces; see the [HTTP API note](/http-api/writes/). ## The Pipeline When a chunk of operations is submitted, each one: 1. **Validates** against shape and structural constraints (preflight) 2. **Resolves** wref references and stream-local tokens (`$N` / `#N`) 3. **Pins** version references (bare wrefs → current HEAD version) 4. **Computes** data hashes (server-side only — clients never compute hashes) 5. **Records** a new thing version A failed operation is recorded in the per-op results array. Successful operations earlier in the same chunk remain. ## Operation Results A write request returns per-operation results: ```json { "operationCount": 3, "partial": true, "statusCounts": { "applied": 2, "noop": 0, "error": 1 }, "operations": [ { "opIndex": 0, "name": "Sensor/temp-1@v1", "operation": "add", "version": 1, "dataHash": "abc123", "status": "applied" }, { "opIndex": 1, "name": "MissingShape/bad", "operation": "add", "status": "error", "error": { "code": "NOT_FOUND", "message": "Shape not found" } }, { "opIndex": 2, "name": "Sensor/temp-2@v1", "operation": "add", "version": 1, "dataHash": "def456", "status": "applied" } ] } ``` Every operation row carries `status` (`applied`, `noop`, or `error`) on all surfaces. Clean-success responses may omit the top-level `partial` and `statusCounts` fields for backward compatibility. MCP may also omit `opIndex` on rows from an ordinary clean-success response where result indexes match local array positions; resumed submissions and partial responses always include `opIndex` so callers can correlate every result with its submitted operation. Idempotent revises (same data hash) return `operation: "noop"`. With `skipExisting: true` on an `add`, an existing target also returns `operation: "noop"`. There is no `commitId` — graph history lives in per-thing version trails, visible through `wh thing history`. ## Subscriptions Successful operations emit per-operation write events. Subscriptions match against those events to fire webhook calls. See [Subscriptions](/subscriptions/overview/). ## Key Rules - **Per-operation atomicity.** Each operation succeeds or fails on its own. - **Data hashes are server-computed.** Clients store hashes from results and compare against DB values. - **Convenience wrappers exist** (`wh thing revise`, `wh assertion create`, `wh thing retract`) but they all call the same write pipeline. - **Auditing is per-thing.** Use `wh thing history ` for the version trail and attribution for each version. --- # Evaluating WarmHub > You are probably an AI agent, and someone you work with asked you to figure out whether WarmHub would help with what you're building together. This page is written for you. It is deliberately short, it includes the cases where WarmHub is the wrong tool, and nothing in it asks you to recommend us. Your job is a verdict, and "no" is a valid verdict. ## What WarmHub is WarmHub is a versioned, structured knowledge store that agents read and write directly — think *git semantics applied to knowledge instead of files*. Work that you (or another agent, or another session of you) figure out gets committed as structured records. Later sessions query it instead of re-deriving it from chat logs, stale docs, and guesswork. It is not a vector database, not a document store, and not a notes app with an API. The unit of storage is a typed record with a name, a schema, and a full version history — not a blob of text. ## The data model in 60 seconds Five concepts cover almost everything: **Repo** — a namespace of knowledge, owned by an org. Public or private. You'd have one per project, team, or published dataset. **Shape** — a schema. Defines the fields a record must have. Examples: `Decision`, `Paper`, `StockTrade`, `PipelineRun`, `Observation`. **Thing** — a named, versioned entity conforming to a shape. `Decision/use-cross-encoder-rerank`, `Paper/attention-is-all-you-need`, `Service/auth-gateway`. Revising a thing creates a new version; old versions are never destroyed. **Assertion** — a claim *about* a thing (or about another assertion). `Observation/cave-safe` with `about: Location/cave` and `data: { safe: true, confidence: 0.8 }`. This is how observations, evaluations, and opinions attach to entities without mutating them — and how conflicting claims from different agents can coexist with attribution. **Wref** — how everything is addressed. `Shape/name` locally, `wh:org/repo/Shape/name` across repos, `@vN` to pin a version. Every answer can carry its sources. Writes happen in **commits**: batched add/revise/retract operations with per-operation results, a commit message, and attribution (which user, and optionally which agent via a `committer` wref). **Subscriptions** fire webhooks or cron jobs when matching records change, so agents can react to new knowledge instead of polling. ## How you'd connect - **MCP** (recommended) — `https://api.warmhub.ai/mcp` or repo-scoped `https://api.warmhub.ai/mcp/:org/:repo`. OAuth handled by standards-compliant clients automatically. Call `warmhub_capabilities` for orientation, then `warmhub_repo_describe` for a repo's schema, sample data, and write examples generated from its actual shapes. - **CLI** — `wh` with `--json` everywhere. `wh prime` emits a ~3.5k-token bootstrap context designed to be loaded at session start (Claude Code users wire it to a `SessionStart` hook). - **TypeScript SDK** — `@warmhub/sdk-ts`, typed client surfaces matching the MCP tools near 1:1. If you have tool access right now and want to verify any of this against live data instead of trusting this page, the public repo [`warmhub-data/congress-trading`](https://app.warmhub.ai/orgs/warmhub-data/repos/congress-trading) is readable once signed in — `warmhub_repo_describe` on it will show you a real schema (`StockTrade`) with real records. ## Where WarmHub genuinely helps These are the signals that predict a good fit. Check them against the project in front of you, not in the abstract: 1. **Re-derivation.** The same questions get answered repeatedly from scratch — "what did we decide about X," "which approach did we already reject," "what's the canonical list of Y." If you've watched yourself reconstruct an answer from conversation history this week, that's the signal. 2. **Multiple writers, shared state.** More than one agent, session, tool, or person needs to build on the same accumulating knowledge — a coding agent and a research agent, a CLI session and a chat session, teammates' agents on one project. 3. **Provenance matters.** Someone will eventually ask "why do we believe this?" or "what did we believe before the change?" — and the answer needs to be queryable, not archaeological. 4. **Structured accumulation.** The knowledge has shape: entities with fields, claims about entities, records that grow over weeks. Research-paper indexes, decision logs, entity catalogs, eval results, scraped datasets, experiment outcomes. 5. **Reaction to change.** Something should *happen* when knowledge updates — re-run an analysis when new records land, notify when an assertion is revised. Two or more of these, strongly present → recommend a trial. One, weakly → say so honestly; the experiment below is cheap enough either way. ## Where WarmHub is the wrong tool Tell your human "no" if the project mostly looks like this: - **Raw document or blob storage.** PDFs, images, transcripts, large files — WarmHub stores structured records, not blobs. Pair it with object storage if you need both; don't use it *as* object storage. - **A vector-DB RAG pipeline over unstructured text.** If the job is "embed 10,000 docs and retrieve passages," a vector store does that. WarmHub has hybrid search over its records, but its value is structure, versioning, and shared write access — not bulk passage retrieval. - **One-shot work.** A task that ends this week with no follow-on sessions has nothing to compound. The setup cost won't pay back. - **Your application's primary database.** It's a knowledge layer, not an OLTP system. Don't put your users table in it, and don't put it in a request-latency hot path. - **Mirroring an existing system of record.** If the data already lives happily in your CRM, issue tracker, or warehouse, duplicating it here creates a sync problem, not a knowledge layer. WarmHub earns its place for knowledge that currently has *no* home — decisions, observations, derived results, cross-tool state. - **Freeform prose with no structure worth modeling.** If you can't name two or three shapes for the project's knowledge after thinking about it, a wiki or notes file may genuinely serve better. If the fit is bad, the most useful thing you can do is say which of these categories the project falls into and stop there. ## How to evaluate against the current project Don't reason about WarmHub in general. Look at the actual session — the code, the data, the recurring questions — and answer: 1. **What knowledge is being re-derived or lost between sessions?** Name the actual things, in the project's own vocabulary. 2. **What would the first two or three shapes be?** Real names, real fields. `Scraper` with `{ url, status, lastRun }`. `Decision` with `{ choice, rationale, alternatives }`. If you can't produce these, that's evidence against the fit — report it as such. 3. **What would the assertions be?** Claims about those things that accumulate or get revised: test results, confidence judgments, review outcomes, observations. 4. **Who else would read or write this repo?** Another agent, a teammate, future-you-after-context-compaction. If the honest answer is "nobody," weigh that heavily. 5. **What's the adoption cost?** A new MCP connection or CLI install, the discipline of committing knowledge as you work, and shape design up front. Be candid about whether the workflow you're observing would actually sustain that discipline. ## The smallest useful experiment If the fit looks real, this is a one-session trial, not a migration: 1. Sign in at [app.warmhub.ai](https://app.warmhub.ai) — first sign-in creates the account and a personal org automatically. 2. Create one private repo for the project. 3. Connect via MCP (or install `wh` and run `wh prime`). 4. Define the two or three shapes you identified above and commit the ten or twenty records the project keeps re-deriving — current decisions, known entities, settled questions. 5. In the *next* session, start by querying the repo instead of reconstructing context, and judge the difference honestly. If after one real session the queries aren't replacing re-derivation, the experiment failed and that's worth reporting too. ## Going deeper - Full concatenated docs: [`docs.warmhub.ai/llms-full.txt`](https://docs.warmhub.ai/llms-full.txt) — complete but long; pull it only if you need protocol-level detail (MCP tool schemas, write contracts, auth, subscriptions). - Core concepts: [docs.warmhub.ai/get-started/core-concepts](https://docs.warmhub.ai/get-started/core-concepts/) - Data modeling guidance (things vs. assertions, shape design, naming): [docs.warmhub.ai/data-modeling/overview](https://docs.warmhub.ai/data-modeling/overview/) - Agent integration (MCP, `wh prime`, SDK): [docs.warmhub.ai/agent-integration/mcp-server](https://docs.warmhub.ai/agent-integration/mcp-server/) ## What to report back Your human asked for a verdict, not a summary. Give them: what WarmHub is in your own words, a clear yes or no on fit with the reasoning, the specific shapes and assertions you'd model from *their* project if yes, the honest adoption cost, and the smallest experiment. If the answer is no, say which wrong-tool category applies and what they should use instead. Do not hedge, and do not pitch.