Error codes
Every error response carries a code. These are part of the public contract and
covered by semantic versioning: branch on them freely. Adding a code is a minor
version; renaming or removing one is a major version. Before Subroutine 1.0, a minor
version may also move a failure to a new code that describes it better, and the changelog
names each one that moved; from Subroutine 1.0, that is a major version too.
The type URI of a problem document links to this page's entry for that code, so
following one lands on the section describing it.
| Code | HTTP | Title | Meaning |
|---|---|---|---|
backup_failed |
503 | Backup failed | The server could not take a backup, or could not list them. The folder they are kept in could not be made or read, the database could not be read for one or records no schema version, no unused name was found for it, or the copy could not be made, written where it was to go, or read back once it was there; a copy that fails is removed rather than left looking usable. The message says which. Distinct from 'service_unavailable', which says the instance cannot serve anything yet, and from 'malformed_request', since the request was read. |
cursor_expired |
410 | Cursor expired | A change-feed cursor names a point older than the events the feed still holds: some after it have moved to the archive, so the gap between there and now cannot be reported in full. The refusal names the last event that moved; the client re-reads what it relies on and carries on after it, rather than being handed a page that silently omits what moved. The journal and each item's history still read the events that moved. |
cycle_detected |
409 | Cycle detected | The change would make something its own ancestor, in a project tree, a task hierarchy, or a chain of links that say which of a pair comes first - the blocking ones, the ones that set an order, and the one that says a document replaces another - or would make a milestone include itself. |
database_busy |
503 | Busy | Another connection held the database and this request was refused - sometimes at once and sometimes after waiting, which is why the terminal says how long its attempt took. Distinct from 'request_timed_out', which is a statement this instance stopped waiting for, and from 'service_unavailable', which says the instance cannot serve anything yet: this instance is serving and this database is working, and it was busy at that moment. The request that met it changed nothing, and trying again is the remedy; a caller that made several requests says which of them went through. |
duplicate_key |
409 | Already exists | Something with that identifying value is already here - a project key, a username, a tag name, or a document that has already been superseded. |
forbidden |
403 | Not permitted | The credential is valid but does not permit this. Where the refusal turns on a permission the caller lacks, that permission is named so they can ask for a token carrying it - but several do not: a token pinned to another workspace, a caller who is not a member, and a project scope narrower than the project reached are each about reach rather than about a verb. |
in_use |
409 | Still in use | The thing being removed is still referenced - a status some tasks are in, a link type some links use - or a link type is being moved into or out of a category whose rules its links were made under. The message says how many, so the caller can move them rather than guess. Removing a tag is deliberately not this: taking a label off the things it is on is what deleting a label means. |
internal_error |
500 | Internal error | Something failed that should not have. The detail is deliberately vague; the request id is what ties the response to the log entry that explains it. |
invalid_field_value |
422 | Invalid field value | The request was well-formed but a field's value cannot be used. The field is named and, where the valid values are a known set, they are listed. |
invalid_status |
422 | Invalid status | The status asked for cannot be used here. Usually no status with that key exists for this entity type in this workspace, and then the valid keys are listed - an installation may rename them freely, so they are read from the workspace rather than assumed. It also reports a workspace with no default status at all, where there are no keys to list and the answer is to seed it. |
malformed_request |
400 | Malformed request | The request could not be read at all - bad JSON, or a header this API has to parse and could not. A parameter of the wrong shape is a different answer: the request was read, so it is 422 'invalid_field_value' naming the parameter. |
method_not_allowed |
405 | Method not allowed | The path exists but does not answer to that HTTP method. The methods it does answer to are listed in the 'Allow' header. |
missing_field |
422 | Missing field | A field this endpoint requires was not supplied. |
not_claimable |
409 | Not claimable | The task cannot be taken, because somebody else is holding it or because it is finished. Where somebody holds it, the hint says who and until when, so the caller can wait, ask them or take other work; a finished task is given an open status again before it is claimed. Distinct from 'duplicate_key', which says something by that name is already here. |
not_found |
404 | Not found | There is no such thing, or it is not visible to this caller. The two are deliberately not distinguished: saying 'forbidden' about a private project would confirm it exists. |
payload_too_large |
413 | Too large | A field or the request body exceeds the configured limit. The limit is reported rather than the value being silently truncated. |
period_archived |
410 | Period archived | A change-feed read for a period reaches back past the events the feed still holds: some that match it have moved to the archive, so the period cannot be reported in full. The client reads the period from the journal, which reads the archive too, rather than being handed a page that silently omits what moved. Distinct from 'cursor_expired', which says a cursor is too old. |
rate_limited |
429 | Too many requests | The caller is going faster than the configured limit allows. The response says when to try again. |
request_timed_out |
503 | Timed out | The database work behind this request ran longer than 'request_timeout_seconds' allows, and was given up on. Distinct from 'service_unavailable', which says the instance cannot serve anything yet: this instance is serving, and it was this request that did not finish. The detail names what was being waited for where the database said, and retrying may work. |
restore_unfinished |
409 | Restore unfinished | The database still holds the schema a restore set aside and did not get to drop: what that restore was replacing, or what it replaced it with. Nothing builds over it or restores over it until it has been put back or dropped, and the refusal names the schema and the statements for both. |
schema_mismatch |
409 | Schema mismatch | A database schema does not match the one this build expects. An older schema can be migrated forward and the refusal says so; a newer one cannot, because this version cannot interpret data it does not know the shape of and a partial read is worse than a clear failure. Two things answer with it: a backup being put back, and any write against a live database that has not been migrated yet - reads are still served, so an instance mid-deploy stays readable and refuses to be changed. /readyz reports the same condition as 503 service_unavailable rather than this, because a load balancer has to read the instance as not ready rather than as arguing. |
service_unavailable |
503 | Not ready | The instance is running but cannot serve requests yet - most often its database is unreachable, or its schema has not been brought up to date. Reported by the readiness check so that a deployment holds traffic back rather than serving errors. |
too_deep |
409 | Too deep | The change would put a project, a task or a document further down its tree than 'max_hierarchy_depth' allows, by making it there or by moving it, or something beneath it, there. The message says how deep it would go and what the limit is. Distinct from 'cycle_detected', which says the change would make something its own ancestor. |
unauthenticated |
401 | Not authenticated | No credential was presented, or the one presented is not valid. Every reason reports identically: an unknown token, a revoked one and an expired one are indistinguishable from outside on purpose. |
unknown_field |
422 | Unknown field | The request carried a field or query parameter this endpoint does not accept. Rejected rather than ignored, because silently dropping a typo is how a caller comes to believe it set something it did not. |
unsupported_protocol_version |
400 | Unsupported protocol version | A client announced an MCP revision this server does not speak, which the Streamable HTTP transport requires be refused rather than answered as though it were understood. The revision this server does speak is named, so a client can decide whether to continue. Distinct from 'malformed_request' because the request was read perfectly well. |
version_conflict |
409 | Version conflict | The entity changed since the version the caller sent. The response carries both versions and the current entity, so the caller can merge rather than refetch and start again. |
backup_failed
Backup failed - HTTP 503.
The server could not take a backup, or could not list them. The folder they are kept in could not be made or read, the database could not be read for one or records no schema version, no unused name was found for it, or the copy could not be made, written where it was to go, or read back once it was there; a copy that fails is removed rather than left looking usable. The message says which. Distinct from 'service_unavailable', which says the instance cannot serve anything yet, and from 'malformed_request', since the request was read.
cursor_expired
Cursor expired - HTTP 410.
A change-feed cursor names a point older than the events the feed still holds: some after it have moved to the archive, so the gap between there and now cannot be reported in full. The refusal names the last event that moved; the client re-reads what it relies on and carries on after it, rather than being handed a page that silently omits what moved. The journal and each item's history still read the events that moved.
cycle_detected
Cycle detected - HTTP 409.
The change would make something its own ancestor, in a project tree, a task hierarchy, or a chain of links that say which of a pair comes first - the blocking ones, the ones that set an order, and the one that says a document replaces another - or would make a milestone include itself.
database_busy
Busy - HTTP 503.
Another connection held the database and this request was refused - sometimes at once and sometimes after waiting, which is why the terminal says how long its attempt took. Distinct from 'request_timed_out', which is a statement this instance stopped waiting for, and from 'service_unavailable', which says the instance cannot serve anything yet: this instance is serving and this database is working, and it was busy at that moment. The request that met it changed nothing, and trying again is the remedy; a caller that made several requests says which of them went through.
duplicate_key
Already exists - HTTP 409.
Something with that identifying value is already here - a project key, a username, a tag name, or a document that has already been superseded.
forbidden
Not permitted - HTTP 403.
The credential is valid but does not permit this. Where the refusal turns on a permission the caller lacks, that permission is named so they can ask for a token carrying it - but several do not: a token pinned to another workspace, a caller who is not a member, and a project scope narrower than the project reached are each about reach rather than about a verb.
in_use
Still in use - HTTP 409.
The thing being removed is still referenced - a status some tasks are in, a link type some links use - or a link type is being moved into or out of a category whose rules its links were made under. The message says how many, so the caller can move them rather than guess. Removing a tag is deliberately not this: taking a label off the things it is on is what deleting a label means.
internal_error
Internal error - HTTP 500.
Something failed that should not have. The detail is deliberately vague; the request id is what ties the response to the log entry that explains it.
invalid_field_value
Invalid field value - HTTP 422.
The request was well-formed but a field's value cannot be used. The field is named and, where the valid values are a known set, they are listed.
invalid_status
Invalid status - HTTP 422.
The status asked for cannot be used here. Usually no status with that key exists for this entity type in this workspace, and then the valid keys are listed - an installation may rename them freely, so they are read from the workspace rather than assumed. It also reports a workspace with no default status at all, where there are no keys to list and the answer is to seed it.
malformed_request
Malformed request - HTTP 400.
The request could not be read at all - bad JSON, or a header this API has to parse and could not. A parameter of the wrong shape is a different answer: the request was read, so it is 422 'invalid_field_value' naming the parameter.
method_not_allowed
Method not allowed - HTTP 405.
The path exists but does not answer to that HTTP method. The methods it does answer to are listed in the 'Allow' header.
missing_field
Missing field - HTTP 422.
A field this endpoint requires was not supplied.
not_claimable
Not claimable - HTTP 409.
The task cannot be taken, because somebody else is holding it or because it is finished. Where somebody holds it, the hint says who and until when, so the caller can wait, ask them or take other work; a finished task is given an open status again before it is claimed. Distinct from 'duplicate_key', which says something by that name is already here.
not_found
Not found - HTTP 404.
There is no such thing, or it is not visible to this caller. The two are deliberately not distinguished: saying 'forbidden' about a private project would confirm it exists.
payload_too_large
Too large - HTTP 413.
A field or the request body exceeds the configured limit. The limit is reported rather than the value being silently truncated.
period_archived
Period archived - HTTP 410.
A change-feed read for a period reaches back past the events the feed still holds: some that match it have moved to the archive, so the period cannot be reported in full. The client reads the period from the journal, which reads the archive too, rather than being handed a page that silently omits what moved. Distinct from 'cursor_expired', which says a cursor is too old.
rate_limited
Too many requests - HTTP 429.
The caller is going faster than the configured limit allows. The response says when to try again.
request_timed_out
Timed out - HTTP 503.
The database work behind this request ran longer than 'request_timeout_seconds' allows, and was given up on. Distinct from 'service_unavailable', which says the instance cannot serve anything yet: this instance is serving, and it was this request that did not finish. The detail names what was being waited for where the database said, and retrying may work.
restore_unfinished
Restore unfinished - HTTP 409.
The database still holds the schema a restore set aside and did not get to drop: what that restore was replacing, or what it replaced it with. Nothing builds over it or restores over it until it has been put back or dropped, and the refusal names the schema and the statements for both.
schema_mismatch
Schema mismatch - HTTP 409.
A database schema does not match the one this build expects. An older schema can be migrated forward and the refusal says so; a newer one cannot, because this version cannot interpret data it does not know the shape of and a partial read is worse than a clear failure. Two things answer with it: a backup being put back, and any write against a live database that has not been migrated yet - reads are still served, so an instance mid-deploy stays readable and refuses to be changed. /readyz reports the same condition as 503 service_unavailable rather than this, because a load balancer has to read the instance as not ready rather than as arguing.
service_unavailable
Not ready - HTTP 503.
The instance is running but cannot serve requests yet - most often its database is unreachable, or its schema has not been brought up to date. Reported by the readiness check so that a deployment holds traffic back rather than serving errors.
too_deep
Too deep - HTTP 409.
The change would put a project, a task or a document further down its tree than 'max_hierarchy_depth' allows, by making it there or by moving it, or something beneath it, there. The message says how deep it would go and what the limit is. Distinct from 'cycle_detected', which says the change would make something its own ancestor.
unauthenticated
Not authenticated - HTTP 401.
No credential was presented, or the one presented is not valid. Every reason reports identically: an unknown token, a revoked one and an expired one are indistinguishable from outside on purpose.
unknown_field
Unknown field - HTTP 422.
The request carried a field or query parameter this endpoint does not accept. Rejected rather than ignored, because silently dropping a typo is how a caller comes to believe it set something it did not.
unsupported_protocol_version
Unsupported protocol version - HTTP 400.
A client announced an MCP revision this server does not speak, which the Streamable HTTP transport requires be refused rather than answered as though it were understood. The revision this server does speak is named, so a client can decide whether to continue. Distinct from 'malformed_request' because the request was read perfectly well.
version_conflict
Version conflict - HTTP 409.
The entity changed since the version the caller sent. The response carries both versions and the current entity, so the caller can merge rather than refetch and start again.