standards
ErrorsHTTP API

Errors

Read one error envelope everywhere, branch on its code, and know how the HTTP API, the SDK, the CLI and MCP report it.


Every failed request answers with the same body, whichever door you use: the HTTP API, the SDK, the CLI or MCP. Branch on code; fall back to statusCode for coarse categories. Keep the x-request-id header when you report a problem. The sections below start with the envelope, then show what each door does with it.

HTTP

The error envelope

A lookup of a missing record returns:

{
  "success": false,
  "statusCode": 404,
  "message": "Record \"abc123\" not found.",
  "code": "SCHEMA_RECORD_NOT_FOUND"
}
FieldDescription
successAlways false for errors.
statusCodeThe HTTP status, mirrored in the body.
messageA human-readable explanation. Every 5xx message is masked to Internal server error.
codeA stable machine-readable code. Absent when the request body failed validation.

When request-body parsing fails, message is Validation failed, no code is set, and an errors array points at each invalid field. path is an array because it can point into nested fields:

{
  "success": false,
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [{ "path": ["limit"], "message": "Expected number, received string" }]
}

A value that fails its attribute's type or constraints is a different case: it answers SCHEMA_VALIDATION_FAILED, with a code.

Request ids

Every response, success or error, carries an x-request-id header holding a UUID that identifies that exact request on the server side:

x-request-id: 4b2f1c9e-8a3d-4f6b-9c1e-2d7a5e8b0c4f

It is the only response header the API promises. Include it when reporting an issue.

Error codes

The codes you can meet from the SDK, the CLI and MCP:

CodeHTTPMeaning
SCHEMA_VALIDATION_FAILED400Input failed schema validation. Also refuses revoking the last Owner credential.
SCHEMA_AMBIGUOUS_CHANGE400A sync would rename-shape or retype a system attribute (a drop plus an add of the same type, or a type the stored values cannot be read as). Nothing is applied. On a source push the message names the exact resolutions entry the next push can carry (a rename, text → phone or number → currency); relations and other retypes take a SQL migration, or ship the removal and the addition separately.
RECORD_GRANT_ON_WORKSPACE400A share was requested on a workspace record; only a private record can be shared. See Sharing.
RECORD_GRANT_INVALID_ACTOR400The share's beneficiary is not an active member of this workspace, or is the record's owner.
RECORD_GRANT_UNSHAREABLE_OBJECT400Records of memory and skill are never shared.
AGENT_GRANT_ON_WORKSPACE400A share was requested on a workspace agent or routine; only a private one can be shared. See Sharing.
AGENT_GRANT_INVALID_ACTOR400The member to share an agent with is not an active member of this workspace, or is its owner.
PAGE_GRANT_ON_WORKSPACE400A share was requested on a page the whole workspace already sees; only a private page can be shared.
PAGE_GRANT_INVALID_ACTOR400The member to share a page with is not an active member of this workspace, or is its owner.
AGENT_SCHEDULE_AUDIENCE_ON_WORKSPACE400A routine was asked to run for each person but is not a private routine.
AGENT_SCHEDULE_AUDIENCE_HARNESS400A routine on a harness or an environment cannot run for each person.
AGENT_SCHEDULE_AUDIENCE_TOOL400A routine that runs for each person holds a tool outside its scope; the message names it.
AGENT_TOOL_GROUP_ROUTINE_ONLY400The notifications group was granted to an agent; only a routine holds it.
RECORD_COUNT_GROUP_BY_MULTI_VALUED400A grouped count named an attribute that holds several values per record (multiselect, a multiple user, a many relation). See Counting by group.
RECORD_COUNT_GROUP_BY_UNSUPPORTED400A grouped count named an attribute that cannot be grouped: not a status, select, single user or single relation, hidden, archived, unknown, or a system field.
SCHEMA_PROTECTED_ROLE400A built-in role (owner, member) cannot be updated, deleted or have its permissions replaced. Create a custom role instead.
SCHEMA_INVALID_OBJECT_NAME400The object name is malformed.
SCHEMA_RESERVED_OBJECT_NAME400The object name belongs to a framework-native object (skill, memory, drives, artifact, meeting).
SCHEMA_INVALID_ATTRIBUTE_NAME400An attribute name is malformed.
DOCUMENT_MIME_NOT_ACCEPTED400The file matches no entry of the document attribute's accepts list.
DOCUMENT_MULTIPLE_NOT_ALLOWED400The document attribute takes a single file.
DOCUMENT_FILE_TOO_LARGE400The file exceeds the document attribute's maxFileSize.
FILE_PUBLIC_NOT_ATTACHABLE400A public file cannot join a document. Upload it again without public to attach it.
ENVIRONMENT_NOT_READY400The environment was never saved, is archived, or its snapshot is gone.
ENVIRONMENT_MODEL_ACCESS_MISSING400A harness has no model access: no key in the Variables and nothing usable on its machine.
ENVIRONMENT_BUILD_FAILED400An environment's setup script exited non-zero. Recorded on the environment rather than answered.
ENVIRONMENT_RUN_FAILED400A harness run or turn crashed, was interrupted, waited for input, or lost its history. Surfaces as the run's error.
SCHEMA_FORBIDDEN403The actor lacks permission. The message never names what was refused.
ACTOR_CONTEXT_REQUIRED401The request authenticated but carried no actor, on a route that acts as one (record operations, agent sessions, your own profile).
SCHEMA_ACTOR_CONTEXT_REQUIRED403No authenticated actor reached the handler.
SCHEMA_PROTECTED_OBJECT403The object is system-protected.
SCHEMA_PROTECTED_ATTRIBUTE403The attribute is system-protected.
SCHEMA_SYSTEM_ENTITY_IMMUTABLE403A system entity declared in code cannot be modified at runtime. A write on a built-in role answers SCHEMA_PROTECTED_ROLE. Also answers every create, delete or visibility change of a memory record, and every update except a person's write of a page's content alone, from their own session: memory is written by the workspace.
SCHEMA_WORKSPACE_INVARIANT403A workspace rule refuses the write. The message names the remedy.
SCHEMA_API_KEY_OWNER_ROLE_REFUSED403owner was asked for a workspace API key. Only a personal key may hold it.
SCHEMA_RECORD_NOT_FOUND404No record with that id.
CLOUD_BROWSER_CLOSED404The cloud browser has closed: a command or a live view reached it too late. The agent may launch it again.
SCHEMA_OBJECT_NOT_FOUND404No object with that name.
SCHEMA_ATTRIBUTE_NOT_FOUND404No such attribute.
SCHEMA_SOURCE_NOT_FOUND404No schema source with that id.
SCHEMA_ROLE_NOT_FOUND404No such role.
NEWS_ENTRY_NOT_FOUND404A news entry is not available to you.
DOCUMENT_NOT_FOUND_FOR_OPERATION404The target document does not exist.
PAGE_REFERENCED400The page is still referenced by a page tab of a record view, so it cannot be deleted or made private. Remove the tab first.
SCHEMA_TIMEOUT408The operation timed out. Retry after backing off.
SCHEMA_DUPLICATE_OBJECT409An object with that name already exists, or is owned by core, a bundle or another source.
SCHEMA_DUPLICATE_ATTRIBUTE409An attribute with that name already exists.
SCHEMA_DUPLICATE409A view with that name is owned by another source, or a source's view clashes with the workspace's views: a second default view for the same object and context, or a name held by a system view.
SCHEMA_CONFLICT409The request conflicts with current state.
SCHEMA_RECORD_ID_UNAVAILABLE409An operation tried to create a record under an id that is already taken. Retry with a fresh id.
SCHEMA_OBJECT_REFERENCED409The object is still the target of relations on other objects. Drop those relation attributes first.
SCHEMA_ATTRIBUTE_IN_USE409The attribute cannot be deleted while something still depends on it: a kanban grouped by it, a view whose last main content is sourced on it, or the object's label or embedding expression. The message names the use to change first.
SCHEMA_SYNC_CONFLICT409A declaration cannot be reconciled with what the workspace already holds, such as a runtime attribute whose type clashes with the code. Relax the constraint, migrate the values, or rename the declaration.
SCHEMA_MEMORY_PROPOSAL_RATE_LIMITED429More than 30 propose_memory calls for one person in the last hour. The proposal was not queued; propose it later.
AGENT_INVALID_STATE409The agent session cannot take the request in its current state, such as editing a message while a turn runs (stop the turn first) or in a conversation run by a harness (see Environments).
ENVIRONMENT_IN_USE409The environment cannot be archived while a definition still uses it. The definitions' ids are in the details.
ENV_VAR_NOT_FOUND404No private secret of yours has this id: it does not exist, or it is someone else's.
BROWSER_PROFILE_LOCKED409Preparing a browser profile while another browser writes it. details.holder is agent or prepare. Retry once it has closed.
AGENT_SESSION_STREAM_CURSOR_EXPIRED410The events after the stream cursor are no longer kept. Reload the history page and reconnect at its committedSeq. See Agent conversations.
EXPORT_TOO_LARGE413The export would exceed 100 000 records; narrow the filter.
SCHEMA_SYNC_FAILED500A sync stopped partway. The message is masked; use the request id.
SCHEMA_ACCESS_DENIED500Deleting a file someone else uploaded. The message is masked.
CLOUD_BROWSER_LAUNCH_FAILED502The browser provider could not start a cloud browser. The message is masked.
CLOUD_BROWSER_PROVIDER_FAILED502The browser provider failed on anything but a start: closing, waking, a live view, creating or deleting a profile. The message is masked.
CLOUD_BROWSER_EXTENSION_TIMEOUT504A cloud browser did not connect within 15 seconds of its start; it is closed. The message is masked.
QUERY_TIMEOUT504A bounded read, such as an aggregate, ran past its time limit. Narrow the filter or the grouping and retry.
CONFIGURATION_REQUIRED503An optional capability is not wired on this deployment. Permanent: do not retry.

The HTTP status derives from the code. A code without an explicit mapping answers 500.

SCHEMA_FORBIDDEN is deliberately vague

A 403 carrying SCHEMA_FORBIDDEN always reads You do not have permission to perform this action. The envelope never names the object or the action, because the message reaches end users who have no access to those names. Branch on the code, not on the sentence. The other 403 codes are refusals a permission grant would not lift, so each keeps a message naming the remedy.

Rate limits

The hosted API counts requests per client IP in five buckets, each per minute:

BucketRoutesPer minute
DefaultRecords, schema, keys, and every route not listed below. A per-object record search is a list read and counts here.100
UploadFile uploads (POST under /files, and /upload routes).20
Global search/search and /global-search, cross-object queries.30
Bulkbulk-create, bulk-update, bulk-delete.10
AIAgent chat and streaming.5

A few routes carry their own limit instead of their bucket's: the relation graph (POST /relations/graph) allows 60 per minute and resolving recent activity (POST /recent-activity/resolve) 30.

Pushing a schema source (PUT /schema/sources/:id) is exempt: a CI run carries a whole schema in one call.

Exceeding a limit returns a 429 in the same envelope, without a code:

{ "success": false, "statusCode": 429, "message": "ThrottlerException: Too Many Requests" }

No rate-limit or retry-after header is promised. x-request-id is the only guaranteed header. Treat a 429 as transient: back off and retry.

Handling errors

Branch on code for precise handling. Fall back to statusCode for categories: 4xx means the request is wrong, so fix it before retrying; 5xx is server-side and safe to retry with backoff. 408 and 429 are safe to retry as-is after backing off. CONFIGURATION_REQUIRED is the one 5xx not worth retrying: the capability is absent from the deployment, and the answer will not change.

SDK

Every failed request throws. The client never returns an error object, never retries, and never swallows a status. Catch by class, read status and code, and decide.

import { StandardsAuthError, StandardsRequestError } from "@standards/client";
import { getStandards } from "./client";
import { incident } from "./schema";

try {
  await getStandards().incidents.get("inc_404");
} catch (error) {
  if (error instanceof StandardsAuthError) throw error; // 401 or 403: fix the key, not the code
  if (error instanceof StandardsRequestError && error.status === 404) return null;
  throw error;
}

The classes

ClassExtendsWhen
StandardsRequestErrorErrorAny non-2xx answer, plus status: 0 when the API could not be reached.
StandardsAuthErrorStandardsRequestError401 or 403 on any route.
SchemaSourceConflictErrorStandardsRequestError409 from schema.sync: a name this source declares is owned by core, a bundle or another source. Carries objectName and owner. A declared view that clashes with the workspace's views (a second default view for the same object and context, or a name held by a system view) is a 409 too, with objectName and owner empty.
SchemaSourceValidationErrorStandardsRequestError400 from schema.sync: bad id, oversize payload, hash mismatch, invalid definition, an ambiguous rename or retype (SCHEMA_AMBIGUOUS_CHANGE) or a declared view the workspace cannot seed, such as a change of context (Failed to sync declared views: <object>:<view> (<reason>)). The server message is kept verbatim.
StandardsConfigurationErrorErrorThe client or the source was built in a way it refuses to run: an apiKey in a browser context, a baseUrl / apiKey neither passed nor set in STANDARDS_API_URL / STANDARDS_API_KEY (the message names the variable), or object names that collide as client keys (all thrown by createStandards, before any request); or schema.sync on a source without an id.

StandardsRequestError carries four fields: status (the HTTP status), message (the server's message, else its error, else its detail, or Request failed with status N when the body has none of them), code (the server's error code, when it sent one, such as SCHEMA_SOURCE_NOT_FOUND) and details (the server's details value, as sent, when there is one). An error body that is not JSON still throws a StandardsRequestError with the fallback message, never a parse error. A 204 answer, or a 200 with an empty body, resolves to null and throws nothing.

The builders throw their own family, at declaration time and without a network: ValidationError for a missing name or label, DuplicateError for a repeated attribute or object name, and SchemaError with a code for a bad object name, a reserved name or a missing labelExpression. All of them extend SchemaError.

standards.files and standards.documents reject with the same ValidationError when a file input is not a Blob, or has no filename or MIME type. They reject before any request is sent.

Order your instanceof checks

Three classes extend StandardsRequestError. An instanceof StandardsRequestError test placed first matches all four, so check the specific classes first:

import {
  SchemaSourceConflictError,
  SchemaSourceValidationError,
  StandardsAuthError,
  StandardsRequestError,
} from "@standards/client";
import { getStandards } from "./client";
import { statusSource } from "./schema";

try {
  await getStandards().schema.sync(statusSource);
} catch (error) {
  if (error instanceof SchemaSourceConflictError) {
    console.error(`"${error.objectName}" is owned by ${error.owner}; rename it`);
  } else if (error instanceof SchemaSourceValidationError) {
    console.error(`the server refused the source: ${error.message}`);
  } else if (error instanceof StandardsAuthError) {
    console.error(`the key lacks architect:update (${error.status})`);
  } else if (error instanceof StandardsRequestError) {
    console.error(`sync failed with ${error.status} ${error.code ?? ""}`);
  } else {
    throw error;
  }
  process.exit(1);
}

Status 0

A refused connection and a timeout both throw StandardsRequestError with status: 0, the message Could not connect to <baseUrl> and no code. The default timeout is 30 seconds per request and covers the wait for the response headers: once they arrive, reading the body is not timed, so a large export finishes however long it takes (race the call against your own deadline if you need a bound on the whole read). set timeoutMs on createStandards to change it for every request, or pass timeoutMs to files.upload or documents.attachToRecord to change it for that one upload. The client makes one attempt. A retry, a backoff or a circuit breaker belongs in your code, where you know whether the call is idempotent.

A 429 is a plain StandardsRequestError with status: 429. The client does not read Retry-After and does not wait.

What the SDK drops

The REST API answers an error with { statusCode, message, code, details, errors[] } and an x-request-id header. The client keeps message, code and details. It drops the errors[] array, so a validation failure on create tells you the first problem in message but not the per-field list, and it drops x-request-id, so a support request has no correlation id from the client side. When you need either, make the call with createTransport and read the response yourself, or reproduce it with the CLI, which prints both.

The three-way degradation

A server that talks to Standards has three distinct failure modes, and a status page should answer each differently. The status page cookbook sorts them with two predicates:

import { StandardsAuthError, StandardsRequestError } from "@standards/client";

/** Connection refused or timeout (status 0), a 5xx, or a key that no longer works. */
export function isStandardsUnreachable(error: unknown): boolean {
  if (error instanceof StandardsAuthError) return true;
  return error instanceof StandardsRequestError && (error.status === 0 || error.status >= 500);
}

/** A 404 on a records route: the workspace answers but has none of our objects. */
export function isSchemaMissing(error: unknown): boolean {
  return (
    error instanceof StandardsRequestError &&
    !(error instanceof StandardsAuthError) &&
    error.status === 404
  );
}

The cron handler turns them into three responses:

try {
  result = await runChecks(deps);
} catch (error) {
  if (error instanceof StandardsAuthError) {
    return json(500, { error: error.code ?? "auth" }); // our configuration is wrong
  }
  if (isStandardsUnreachable(error)) {
    return json(503, { error: "standards_unreachable" }); // try again later
  }
  if (isSchemaMissing(error)) {
    return json(503, { error: "schema_missing" }); // run the sync script
  }
  throw error;
}

An auth failure is a 500 with the server's code: the key is wrong and a retry will not help. An unreachable API is a 503: the next cron run may succeed. A 404 on a records route is also a 503, with a different reason: the workspace answers but the schema source was never synced, so the fix is schema.sync, not waiting. The public page uses the same split to serve the last stored snapshot as stale during an outage, and an empty one when the schema is missing.

CLI

The CLI prints ✗ Error (status): message on stderr and exits with code 1. It exits with exit code 2 when no instance is configured: ✗ Error: No Standards instance configured. Run "standards login" or pass --api-key. Unlike the SDK, the CLI prints the errors[] array and the x-request-id header, so reproduce a failure there when you need either.

MCP

An unknown tool, or a tool your key or scopes withheld, answers a JSON-RPC -32602. Inside a call, the result carries isError: true and a text content Access denied: not authorized to <action> "<object>".

Next steps

  • Sync: the two sync errors and what the summary looks like.
  • Records: what each verb sends and which status it can answer.
  • API keys: why a 403 says nothing and how to grant the missing action.
  • CLI: exit codes, output formats and the --instance flag.
  • MCP: how an agent sees a withheld tool and a refused call.
  • Status page cookbook: the degradation above, wired end to end.
  • Book a demo