Skip to content

Troubleshooting

Something broke at runtime and you want the fix, not a tour. There are two fast paths:

  • Your problem belongs to one feature — a subscription that won’t deliver, a stuck action run, a token-scope question. Jump straight to its runbook in the table below.
  • It’s a cross-cutting runtime error — a write conflict, shape mismatch, auth failure, empty query result, or a rate/payload limit. Those don’t belong on any single feature page, so they’re covered here, each as symptom → cause → fix.

To look up what a specific error code means, see the error-code table and the SDK ErrorKind reference.

Troubleshooting that belongs to one feature lives on that feature’s page:

SymptomGo to
A subscription stopped delivering, is retrying, or dead-letteredDebugging a Failing Subscription
An action webhook failed, stalled, or hit dead_letterAction Delivery Lifecycle
You need to know what an error code meansError codes · SDK ErrorKind
Request-rate limits and per-tier quotasRate Limiting
Installing or repairing a component (e.g. Veritas)Installing Veritas · Authoring Components
Token scopes, expiry, and accessPersonal Access Tokens · Access Reference

The fixes below use the CLI for brevity; the SDK, HTTP API, and MCP expose the same operations where each surface supports them (token management, for example, is CLI/SDK only). WarmHub returns errors in a consistent envelope, so the fastest triage is to read the code:

{
"error": {
"code": "CONFLICT",
"message": "Expected version 3 but current version is 5"
}
}

Symptom — a write is rejected with a 409 CONFLICT:

{
"error": {
"code": "CONFLICT",
"message": "Expected version 3 but current version is 5",
"details": { "reason": "expected_version_mismatch" }
}
}

Or, when adding a name that already exists:

CONFLICT: Thing "Location/cave" already exists

Cause — WarmHub uses optimistic concurrency and does not lock things on read. A CONFLICT with details.reason: "expected_version_mismatch" means you passed expectedVersion and another writer advanced the thing’s version before your write landed. A bare ... already exists means you tried to add a name that is already taken.

Fix

  1. Re-read the current version of the thing:
    Terminal window
    wh thing view Location/cave
  2. Decide whether your change still applies against the new version. Remember that a revise is a full replacement — include every field in data, not just the ones you changed.
  3. Resubmit with expectedVersion set to the version you just read:
    Terminal window
    wh thing revise Location/cave --data '{"x":3,"y":7}' --expected-version 5
    The same --expected-version flag is available on wh commit submit --revise.
  4. To create or skip instead of failing on an existing name, set skipExisting on the add — it returns operation: "noop" rather than a CONFLICT.

Tip — when you need exclusive access across a read-modify-write rather than a single optimistic check, take a read lease instead of retrying conflicts.

Symptom — a write is rejected with a 400:

{
"error": {
"code": "SHAPE_MISMATCH",
"message": "Field 'status' expected string, got number"
}
}

Related 400 codes on the write path include VALIDATION_ERROR (the operation itself is malformed) and ILLEGAL_OP_SEQUENCE (operations submitted in an order the pipeline can’t apply).

Cause — the data you sent doesn’t match the thing’s shape: a field has the wrong type, a required field is missing, or you’re writing against a repo whose shape you haven’t inspected.

Fix

  1. Inspect the current shape and compare it field-by-field against your payload:
    Terminal window
    wh shape view Sensor
  2. Correct the payload — fix the mismatched type, add the missing field — and resubmit.
  3. If the schema change is intentional and you own the shape, revise it in place. A revise increments the shape’s version; things already written under the old schema are unaffected.
    Terminal window
    wh shape revise Sensor --fields '{"status":"string","reading":"number"}'

Note — shapes are not created implicitly on first write; you add a shape explicitly before writing things against it. See Shapes for field types and constraints.

Symptom — a request returns 401 UNAUTHENTICATED or 403 FORBIDDEN:

{ "error": { "code": "UNAUTHENTICATED", "message": "Token expired" } }
{ "error": { "code": "FORBIDDEN", "message": "Missing scope: repo:write" } }

Cause — the two codes mean different things:

CodeMeaning
401 UNAUTHENTICATEDNo token, or the token is invalid, expired, or revoked.
403 FORBIDDENThe token is valid but lacks the scope or repo role the operation requires.

Fix

For a 401, re-establish a valid token:

  1. Sign in interactively, or set WH_TOKEN to a valid personal access token:
    Terminal window
    wh auth login
  2. Check which of your tokens are active, expired, or revoked — wh token list shows active tokens only, so pass --all to see expired and revoked ones too:
    Terminal window
    wh token list --all

For a 403, the token authenticated but isn’t authorized. Scopes and repo membership are composed with AND — a token needs both the right scope and a repo role that permits the operation. A repo:read token can’t write even where your role would allow it, and a repo:write token can’t write to a repo where your role is only viewer.

  1. Create (or recreate) a token with the scope the operation needs:
    Terminal window
    wh token create --name ci-bot --scope myorg/myrepo=repo:read,repo:write
  2. If you’re hitting a repo you don’t own, confirm the owner has granted you a role that permits the operation. See the access reference for the minimum scope per task.

Note — MCP, the SDK, and the HTTP API all authenticate with the same token, so an auth failure is a credential problem, not a transport one: fix the token or its scope rather than switching surfaces. The exact code can vary by surface — some read endpoints return an opaque 404 instead of 401/403.

Symptom — a query returns fewer results than you expected, or none at all.

Cause — usually a filter that’s narrower than you think, a single page of a paginated result, or the wrong repo scope. There is one genuine timing case: a filtered read — one with a match glob — may lag briefly right after a write while WarmHub updates its read indexes. An unfiltered read reflects a write immediately, so if a thing you just wrote is missing from an unfiltered list, the cause is a filter, pagination, or scope — not propagation.

Fix

  1. List the repo without filters to confirm the things exist at all:
    Terminal window
    wh thing list --repo myorg/myrepo
  2. Add filters back one at a time to find the one that excludes your results:
    Terminal window
    wh thing query --shape Observation --about Location/cave --repo myorg/myrepo
  3. Check for pagination. A query returns one page at a time; if the response carries a nextCursor, follow it for the next page. The page size defaults to 50 and caps at 500. Anonymous callers on public repos are capped at 25 per page and stop after two pages — following the cursor past the second page returns 404, so authenticate to page through larger result sets. See Anonymous Pagination Caps.
  4. Confirm the repo. Querying one repo won’t surface things that live in another, and a cross-repo wref lookup needs repo:read on the target repo.
  5. For a thing you just wrote that’s missing from a filtered read, retry the read after a moment, or drop the filter — the read index catches up shortly. See Filtered Read Freshness.

Tip — authenticated full-text and hybrid search pages can be sparse: a page may hold fewer items than limit, or zero, while nextCursor is still present. Paginate until nextCursor is gone before concluding a result set is empty.

Symptom — a request returns 429 RATE_LIMITED or 413 PAYLOAD_TOO_LARGE:

{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded",
"retryAfter": 4
}
}

Cause — you exceeded a request-rate limit (a per-IP cap on unauthenticated traffic, or a per-principal write limit) or sent a request body larger than the endpoint accepts.

Fix

  • For 429, back off until the Retry-After response header (mirrored as error.retryAfter, in seconds) has elapsed, then retry. Authenticate to escape the per-IP anonymous cap. See Rate Limiting for the per-tier write limits.
  • For 413, split the work into smaller writes, each independently valid. The SDK’s write helpers stay under the size limit automatically, so you generally only hit 413 on oversized requests built by hand.

If none of the above resolves it:

  1. Re-run the failing command with --debug to print the full stack trace on failure:
    Terminal window
    wh --debug thing view Location/cave
  2. Check status.warmhub.ai for an active incident before reporting an outage.
  3. Contact support with the error code and message, the --debug output (redact any tokens), the wrefs or repo involved, and roughly when the failure started.