Neuroloom

Reference

Error Catalog

Neuroloom's API returns two error shapes depending on when the error family was added. Both carry a stable code string a client can branch on — check code, not the human-readable message text, which may change without notice.

Legacy shape (most existing errors): {"detail": "...", "code": "..."}.

Current shape (workspace resolution errors, this page's subject, and the standard for new error families going forward): a nested {"error": {...}} envelope with no detail wrapper at all.

This page documents the three workspace-resolution errors introduced by OAuth workspace simplification, plus two memory-write and memory-lookup errors introduced in D176. All five fire when the server can't determine which workspace an authenticated request should operate on, or when a memory-write or memory-lookup request can't be satisfied as sent.


Why these errors exist

An OAuth-authenticated request no longer carries a workspace inside its access token — the token proves who you are, not which workspace you're working in. Every request picks a workspace fresh: send X-Workspace-Id to name one explicitly, or omit it and let the server auto-resolve when you belong to exactly one workspace. These three errors cover the cases where the server can't make that call for you. See Per-Project Workspace Routing for the full mental model.

Note

API key authentication is unaffected — API keys are workspace-scoped by their key binding and never hit these errors.


workspace_not_specified

HTTP status: 400 Bad Request

Raised when an OAuth-authenticated request omits X-Workspace-Id and the caller belongs to two or more active workspaces. The server has no way to guess which one you mean.

{
  "error": {
    "code": "workspace_not_specified",
    "message": "You belong to 3 workspaces. Specify which one with the X-Workspace-Id header, or update your MCP/API client config to include it.",
    "request_id": "5f9e1c2a-...",
    "workspaces": [
      { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "personal-blog" },
      { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "company-api" },
      { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "name": "oss-library" }
    ],
    "hints": {
      "claude_code_plugin": "Run `/neuroloom:init` (or restart your session) — the plugin re-resolves and writes X-Workspace-Id to .mcp.json automatically once your membership is unambiguous.",
      "manual_mcp_config": "Add \"X-Workspace-Id\": \"<id>\" to the headers field in your MCP client config, using one of the workspace IDs listed above.",
      "rest_api": "Send the X-Workspace-Id header on every request, set to one of the workspace IDs listed above."
    },
    "setup_url": "https://neuroloom.dev/docs/guides/multi-workspace"
  }
}

Fix by client:

ClientFix
Claude Code pluginRun /neuroloom:init or restart your session — the plugin re-resolves automatically once membership is unambiguous and writes the result to .mcp.json.
Manual MCP config (Cursor, VS Code, Windsurf)Add an X-Workspace-Id header to your client's config, set to one of the IDs in workspaces. See Per-Project Workspace Routing.
REST API / cURLSend X-Workspace-Id on every request, set to one of the IDs in workspaces.

no_workspace_membership

HTTP status: 400 Bad Request

Raised when an OAuth-authenticated request omits X-Workspace-Id and the caller belongs to zero active workspaces. This is a 400, not a 403 — the server is only ever reporting the authenticated caller's own membership count, never a third party's, so there's no cross-tenant information to protect by using 403's unified-denial convention.

{
  "error": {
    "code": "no_workspace_membership",
    "message": "You don't belong to any workspace yet. Create one at the Neuroloom dashboard, then retry — the X-Workspace-Id header isn't needed until you have at least one membership.",
    "request_id": "5f9e1c2a-...",
    "workspaces": [],
    "hints": {
      "claude_code_plugin": "Create a workspace at the Neuroloom dashboard, then restart your session — the plugin will pick it up automatically.",
      "manual_mcp_config": "Create a workspace at the Neuroloom dashboard, then set the X-Workspace-Id header to its ID in your MCP client config.",
      "rest_api": "Create a workspace at the Neuroloom dashboard before calling any workspace-scoped endpoint."
    },
    "setup_url": "https://neuroloom.dev/docs/guides/multi-workspace"
  }
}

workspaces is always empty for this error — there's nothing to list.

Fix by client:

ClientFix
Claude Code pluginCreate a workspace at app.neuroloom.dev/settings/workspace, then restart your session — the plugin picks it up automatically.
Manual MCP configCreate a workspace at the dashboard, then set X-Workspace-Id to its ID in your client config.
REST API / cURLCreate a workspace at the dashboard before calling any workspace-scoped endpoint.

This also fires from the OAuth consent screen — a zero-workspace user is blocked from completing consent (the approve branch only; denying consent always succeeds regardless of membership count).


invalid_workspace_id

HTTP status: 400 Bad Request

Raised when X-Workspace-Id is present but either isn't a valid UUID, or names a workspace the caller isn't an active member of. Both cases return the same code and status — the response never discloses whether a malformed value or a well-formed-but-foreign one caused the failure, which keeps this consistent with the rest of the API's non-enumeration behavior.

{
  "error": {
    "code": "invalid_workspace_id",
    "message": "The X-Workspace-Id header is not valid — it must be a UUID matching one of your active workspace memberships.",
    "request_id": "5f9e1c2a-...",
    "workspaces": [
      { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "personal-blog" }
    ],
    "hints": {
      "claude_code_plugin": "Run `/neuroloom:init` (or restart your session) to let the plugin re-resolve and overwrite the invalid X-Workspace-Id value.",
      "manual_mcp_config": "Fix the X-Workspace-Id header in your MCP client config — it must exactly match one of the workspace IDs listed above.",
      "rest_api": "Send a valid X-Workspace-Id header, set to one of the workspace IDs listed above."
    },
    "setup_url": "https://neuroloom.dev/docs/guides/multi-workspace"
  }
}

workspaces lists the caller's own active memberships — fetched specifically for this error, not carried over from an auto-resolve check that didn't run (the header was present, so auto-resolve never fires).

Fix by client:

ClientFix
Claude Code pluginRun /neuroloom:init or restart your session — the plugin re-resolves and overwrites the invalid value.
Manual MCP configFix the X-Workspace-Id header in your client config so it exactly matches one of the IDs in workspaces.
REST API / cURLSend a valid X-Workspace-Id header, set to one of the IDs in workspaces.

narrative_tool_markup_leak

HTTP status: 422 Unprocessable Entity

Raised by every memory write path — POST /memories/store, POST /memories/batch (per item), PATCH /memories/{id}, and the MCP memory_store/memory_store_batch tools that call through to them — when the narrative field contains leaked tool-call markup: a </content> token immediately followed by <parameter name=...>, <summary>, or <tags> blocks running to the end of the field. This happens when a calling agent accidentally serializes its own remaining call arguments into the content string instead of passing tags, summary, and files as separate structured parameters. PATCH /memories/{id} only checks narrative when it's included in the update — a PATCH that doesn't touch narrative never re-validates it.

Non-retryable — resubmitting the identical payload fails identically. Fix the payload, not the request.

{
  "error": {
    "code": "narrative_tool_markup_leak",
    "message": "Field 'narrative' contains tool-call markup at offset 412 (a '</content>' token followed by '<parameter name=\"tags\">'). The call's own arguments were serialized into the content string instead of staying structured. Re-issue the call with 'content' holding only prose, and pass 'tags', 'summary', and 'files' as separate parameters.",
    "field": "narrative",
    "offset": 412,
    "token": "<parameter name=\"tags\">"
  }
}
FieldTypeDescription
fieldstringWhich request field carried the leak. Currently always narrative — title is not checked.
offsetintegerCharacter offset of the matched </content> token within that field.
tokenstringThe literal opening tag that immediately followed </content> — e.g. <parameter name="tags">, <summary>, or a shorthand direct tag.
Tip

POST /memories/batch never fails the whole batch for one bad item — a batch containing this error alongside good items still returns 207 Multi-Status, with the failing item's code set to narrative_tool_markup_leak in its results[i] entry and success: false.

A memory that legitimately discusses tool-call XML syntax (for example, a memory about this very error) is not flagged — detection only matches when the trailing markup runs unbroken to the end of the field, with no other prose after it.


memory_prefix_ambiguous

HTTP status: 409 Conflict

Raised by GET /memories/{id} when the supplied id is a hex prefix (not a full UUID) that matches two or more memories in the authenticated workspace. The request itself is well-formed — the conflict is a data-state fact, not a validation failure — which is why this is a 409, not a 422.

{
  "error": {
    "code": "memory_prefix_ambiguous",
    "message": "The prefix 'd9213537' matches 2 or more memories in this workspace. Supply more characters, or use the full UUID.",
    "candidate_ids": [
      "d9213537-1234-4abc-9def-000000000001",
      "d9213537-5678-4abc-9def-000000000002"
    ]
  }
}
FieldTypeDescription
candidate_idsarray of strings (UUID)Full UUIDs of the matching memories, capped at 10 entries. When more than 10 memories match, only the first 10 are listed — the response does not indicate the true total beyond "more than 10."

Fix: supply more characters of the ID, or the full UUID. A prefix matching zero memories returns 404 Not Found instead of this error. See Memory IDs for the full UUID-or-prefix lookup rule.


Reading the envelope

Every error object carries a stable code and message — branch on code, not on message text, which may change without notice. Beyond that, the extra fields are error-specific, not a fixed schema shared by every nested-envelope error:

FieldTypeDescription
codestringStable machine-readable identifier. Present on every nested-envelope error.
messagestringHuman-readable explanation, self-sufficient without a docs lookup.
request_idstring (UUID)Workspace-resolution errors only. Matches the response's X-Request-ID header. Include this when filing a support request.
workspacesarray of {id, name}Workspace-resolution errors only. The caller's own active workspace memberships, scoped to this specific error. Empty for no_workspace_membership.
hintsobjectWorkspace-resolution errors only. Per-client next-action text, keyed by claude_code_plugin, manual_mcp_config, rest_api.
setup_urlstring (URL)Workspace-resolution errors only. Links to Per-Project Workspace Routing.
candidate_idsarray of strings (UUID)memory_prefix_ambiguous only. See that section above.
field / offset / tokenstring / integer / stringnarrative_tool_markup_leak only. See that section above.
Tip

Filing a bug or support request about one of these errors? Include the request_id — it lets Neuroloom's team find the exact request in server logs.


Ready to get started?

Start building with Neuroloom for free.

Start Building Free