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"
}| Field | Description |
|---|---|
success | Always false for errors. |
statusCode | The HTTP status, mirrored in the body. |
message | A human-readable explanation. Every 5xx message is masked to Internal server error. |
code | A 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-2d7a5e8b0c4fIt 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:
| Code | HTTP | Meaning |
|---|---|---|
SCHEMA_VALIDATION_FAILED | 400 | Input failed schema validation. Also refuses revoking the last Owner credential. |
SCHEMA_AMBIGUOUS_CHANGE | 400 | A 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_WORKSPACE | 400 | A share was requested on a workspace record; only a private record can be shared. See Sharing. |
RECORD_GRANT_INVALID_ACTOR | 400 | The share's beneficiary is not an active member of this workspace, or is the record's owner. |
RECORD_GRANT_UNSHAREABLE_OBJECT | 400 | Records of memory and skill are never shared. |
AGENT_GRANT_ON_WORKSPACE | 400 | A share was requested on a workspace agent or routine; only a private one can be shared. See Sharing. |
AGENT_GRANT_INVALID_ACTOR | 400 | The member to share an agent with is not an active member of this workspace, or is its owner. |
PAGE_GRANT_ON_WORKSPACE | 400 | A share was requested on a page the whole workspace already sees; only a private page can be shared. |
PAGE_GRANT_INVALID_ACTOR | 400 | The member to share a page with is not an active member of this workspace, or is its owner. |
AGENT_SCHEDULE_AUDIENCE_ON_WORKSPACE | 400 | A routine was asked to run for each person but is not a private routine. |
AGENT_SCHEDULE_AUDIENCE_HARNESS | 400 | A routine on a harness or an environment cannot run for each person. |
AGENT_SCHEDULE_AUDIENCE_TOOL | 400 | A routine that runs for each person holds a tool outside its scope; the message names it. |
AGENT_TOOL_GROUP_ROUTINE_ONLY | 400 | The notifications group was granted to an agent; only a routine holds it. |
RECORD_COUNT_GROUP_BY_MULTI_VALUED | 400 | A 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_UNSUPPORTED | 400 | A 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_ROLE | 400 | A built-in role (owner, member) cannot be updated, deleted or have its permissions replaced. Create a custom role instead. |
SCHEMA_INVALID_OBJECT_NAME | 400 | The object name is malformed. |
SCHEMA_RESERVED_OBJECT_NAME | 400 | The object name belongs to a framework-native object (skill, memory, drives, artifact, meeting). |
SCHEMA_INVALID_ATTRIBUTE_NAME | 400 | An attribute name is malformed. |
DOCUMENT_MIME_NOT_ACCEPTED | 400 | The file matches no entry of the document attribute's accepts list. |
DOCUMENT_MULTIPLE_NOT_ALLOWED | 400 | The document attribute takes a single file. |
DOCUMENT_FILE_TOO_LARGE | 400 | The file exceeds the document attribute's maxFileSize. |
FILE_PUBLIC_NOT_ATTACHABLE | 400 | A public file cannot join a document. Upload it again without public to attach it. |
ENVIRONMENT_NOT_READY | 400 | The environment was never saved, is archived, or its snapshot is gone. |
ENVIRONMENT_MODEL_ACCESS_MISSING | 400 | A harness has no model access: no key in the Variables and nothing usable on its machine. |
ENVIRONMENT_BUILD_FAILED | 400 | An environment's setup script exited non-zero. Recorded on the environment rather than answered. |
ENVIRONMENT_RUN_FAILED | 400 | A harness run or turn crashed, was interrupted, waited for input, or lost its history. Surfaces as the run's error. |
SCHEMA_FORBIDDEN | 403 | The actor lacks permission. The message never names what was refused. |
ACTOR_CONTEXT_REQUIRED | 401 | The request authenticated but carried no actor, on a route that acts as one (record operations, agent sessions, your own profile). |
SCHEMA_ACTOR_CONTEXT_REQUIRED | 403 | No authenticated actor reached the handler. |
SCHEMA_PROTECTED_OBJECT | 403 | The object is system-protected. |
SCHEMA_PROTECTED_ATTRIBUTE | 403 | The attribute is system-protected. |
SCHEMA_SYSTEM_ENTITY_IMMUTABLE | 403 | A 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_INVARIANT | 403 | A workspace rule refuses the write. The message names the remedy. |
SCHEMA_API_KEY_OWNER_ROLE_REFUSED | 403 | owner was asked for a workspace API key. Only a personal key may hold it. |
SCHEMA_RECORD_NOT_FOUND | 404 | No record with that id. |
CLOUD_BROWSER_CLOSED | 404 | The cloud browser has closed: a command or a live view reached it too late. The agent may launch it again. |
SCHEMA_OBJECT_NOT_FOUND | 404 | No object with that name. |
SCHEMA_ATTRIBUTE_NOT_FOUND | 404 | No such attribute. |
SCHEMA_SOURCE_NOT_FOUND | 404 | No schema source with that id. |
SCHEMA_ROLE_NOT_FOUND | 404 | No such role. |
NEWS_ENTRY_NOT_FOUND | 404 | A news entry is not available to you. |
DOCUMENT_NOT_FOUND_FOR_OPERATION | 404 | The target document does not exist. |
PAGE_REFERENCED | 400 | The 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_TIMEOUT | 408 | The operation timed out. Retry after backing off. |
SCHEMA_DUPLICATE_OBJECT | 409 | An object with that name already exists, or is owned by core, a bundle or another source. |
SCHEMA_DUPLICATE_ATTRIBUTE | 409 | An attribute with that name already exists. |
SCHEMA_DUPLICATE | 409 | A 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_CONFLICT | 409 | The request conflicts with current state. |
SCHEMA_RECORD_ID_UNAVAILABLE | 409 | An operation tried to create a record under an id that is already taken. Retry with a fresh id. |
SCHEMA_OBJECT_REFERENCED | 409 | The object is still the target of relations on other objects. Drop those relation attributes first. |
SCHEMA_ATTRIBUTE_IN_USE | 409 | The 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_CONFLICT | 409 | A 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_LIMITED | 429 | More than 30 propose_memory calls for one person in the last hour. The proposal was not queued; propose it later. |
AGENT_INVALID_STATE | 409 | The 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_USE | 409 | The environment cannot be archived while a definition still uses it. The definitions' ids are in the details. |
ENV_VAR_NOT_FOUND | 404 | No private secret of yours has this id: it does not exist, or it is someone else's. |
BROWSER_PROFILE_LOCKED | 409 | Preparing a browser profile while another browser writes it. details.holder is agent or prepare. Retry once it has closed. |
AGENT_SESSION_STREAM_CURSOR_EXPIRED | 410 | The events after the stream cursor are no longer kept. Reload the history page and reconnect at its committedSeq. See Agent conversations. |
EXPORT_TOO_LARGE | 413 | The export would exceed 100 000 records; narrow the filter. |
SCHEMA_SYNC_FAILED | 500 | A sync stopped partway. The message is masked; use the request id. |
SCHEMA_ACCESS_DENIED | 500 | Deleting a file someone else uploaded. The message is masked. |
CLOUD_BROWSER_LAUNCH_FAILED | 502 | The browser provider could not start a cloud browser. The message is masked. |
CLOUD_BROWSER_PROVIDER_FAILED | 502 | The 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_TIMEOUT | 504 | A cloud browser did not connect within 15 seconds of its start; it is closed. The message is masked. |
QUERY_TIMEOUT | 504 | A bounded read, such as an aggregate, ran past its time limit. Narrow the filter or the grouping and retry. |
CONFIGURATION_REQUIRED | 503 | An 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:
| Bucket | Routes | Per minute |
|---|---|---|
| Default | Records, schema, keys, and every route not listed below. A per-object record search is a list read and counts here. | 100 |
| Upload | File uploads (POST under /files, and /upload routes). | 20 |
| Global search | /search and /global-search, cross-object queries. | 30 |
| Bulk | bulk-create, bulk-update, bulk-delete. | 10 |
| AI | Agent 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
| Class | Extends | When |
|---|---|---|
StandardsRequestError | Error | Any non-2xx answer, plus status: 0 when the API could not be reached. |
StandardsAuthError | StandardsRequestError | 401 or 403 on any route. |
SchemaSourceConflictError | StandardsRequestError | 409 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. |
SchemaSourceValidationError | StandardsRequestError | 400 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. |
StandardsConfigurationError | Error | The 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
403says nothing and how to grant the missing action. - CLI: exit codes, output formats and the
--instanceflag. - 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