Error Handling
This chapter describes how errors are returned in GraphQL responses and how to handle them.
Error response structure
GraphQL errors are returned in the errors array of the response. Each error contains:
{
"errors": [
{
"message": "Entity not found",
"locations": [
{
"line": 3,
"column": 5
}
],
"path": ["runtime", "industryEnergyEnergyMeter", "items"],
"extensions": {
"code": "ENTITY_NOT_FOUND",
"details": "No entity found with rtId: 65dc6d24cc529cdc46c84fcc"
}
}
],
"data": null
}
Error fields
| Field | Description |
|---|---|
message | Human-readable error description |
locations | Position in the GraphQL query where the error occurred |
path | Path to the field that caused the error |
extensions.code | Machine-readable error code |
extensions.details | Additional error details |
Partial responses
GraphQL can return partial data alongside errors. If some parts of a query succeed while others fail, you may receive both data and errors:
{
"errors": [
{
"message": "Access denied to field 'configuration'",
"path": ["runtime", "systemCommunicationAdapter", "items", 0, "configuration"]
}
],
"data": {
"runtime": {
"systemCommunicationAdapter": {
"items": [
{
"rtId": "65d5c447b420da3fb12381bc",
"name": "My Adapter",
"configuration": null
}
]
}
}
}
}
Common error codes
Query errors
| Code | Description |
|---|---|
ENTITY_NOT_FOUND | The requested entity does not exist |
TYPE_NOT_FOUND | The specified ckTypeId is not valid |
INVALID_FILTER | Filter parameters are invalid |
INVALID_SORT | Sort parameters are invalid |
INVALID_CURSOR | Pagination cursor is invalid or expired |
Mutation errors
| Code | Description |
|---|---|
VALIDATION_ERROR | Input data failed validation |
REQUIRED_FIELD_MISSING | A required field was not provided |
INVALID_FIELD_VALUE | Field value does not match expected type or format |
ENTITY_ALREADY_EXISTS | Entity with given identifier already exists |
REFERENCE_NOT_FOUND | Referenced entity (e.g., in association) not found |
CONSTRAINT_VIOLATION | Operation violates a constraint (e.g., multiplicity) |
Authorization errors
| Code | Description |
|---|---|
UNAUTHORIZED | Authentication required |
FORBIDDEN | Insufficient permissions for the operation |
ACCESS_DENIED | Access to specific resource denied |
BLUEPRINT_LOCKED | The entity is locked by a blueprint and cannot be changed or deleted by users, see Blueprint-locked entities |
Blueprint-locked entities
A blueprint can ship product-owned entities, for example the categorization rules an accounting blueprint relies on.
For a CK type that is protected by a data policy with ProtectBlueprintLocked (see
Read-only for users), a user can no longer
change or delete such an entity. The write is refused with the stable error code BLUEPRINT_LOCKED and the engine
message number 6384. The entity is unchanged; reads are never affected.
GraphQL
The error is returned for create, update and delete mutations. The extensions carry everything a client needs
to show "locked by the product" instead of a generic failure:
{
"errors": [
{
"message": "Access denied: entity 'Accounting-1.0.0/CategorizationRule-1@65dc6d24cc529cdc46c84fcc' is locked by blueprint and cannot be changed.",
"path": ["runtime", "runtimeEntities", "update"],
"extensions": {
"code": "BLUEPRINT_LOCKED",
"messageNumber": 6384,
"ckTypeId": "Accounting-1.0.0/CategorizationRule-1",
"rtId": "65dc6d24cc529cdc46c84fcc",
"reason": "EntityLocked",
"items": [
{
"ckTypeId": "Accounting-1.0.0/CategorizationRule-1",
"rtId": "65dc6d24cc529cdc46c84fcc",
"reason": "EntityLocked"
}
]
}
}
],
"data": null
}
| Extension | Description |
|---|---|
code | Always BLUEPRINT_LOCKED. Match on this value, not on the message. |
messageNumber | The engine message number, 6384. |
ckTypeId, rtId | The first refused entity. rtId is the id the engine assigned to a refused insert. |
reason | EntityLocked: the stored entity has rtBlueprintLocked = true and cannot be updated, replaced or deleted. ProtectedAttributes: the change sets or changes rtBlueprintLocked, rtBlueprintSource or rtBlueprintAppliedAt, which only blueprints may manage on protected types. |
items | One entry (ckTypeId, rtId, reason) for every refused entity of the request. |
A mutation is applied as a whole: if one entity of a batch is locked, nothing of the batch is written, including the entities that are not locked. Remove the locked entities from the request and send it again.
REST
A REST action that surfaces the refusal answers with HTTP status 403 Forbidden and application/problem+json
(RFC 9457) that carries the same stable code:
{
"type": "https://schemas.meshmakers.cloud/errors/blueprint-locked",
"title": "Entity is locked by a blueprint",
"status": 403,
"detail": "Access denied: entity 'Accounting-1.0.0/CategorizationRule-1@65dc6d24cc529cdc46c84fcc' is locked by blueprint and cannot be changed.",
"code": "BLUEPRINT_LOCKED",
"messageNumber": 6384,
"ckTypeId": "Accounting-1.0.0/CategorizationRule-1",
"rtId": "65dc6d24cc529cdc46c84fcc",
"reason": "EntityLocked",
"items": [
{ "ckTypeId": "Accounting-1.0.0/CategorizationRule-1", "rtId": "65dc6d24cc529cdc46c84fcc", "reason": "EntityLocked" }
]
}
Runtime entities are written through GraphQL; the REST surface of the asset repository has no direct entity write.
The import job (POST {tenantId}/v1/Models/ImportRt) applies the same rule with the same message number 6384 when it
runs on behalf of the initiating user: a file that would overwrite a locked entity fails as a whole, the job result
lists every offending ckTypeId@rtId, and nothing is written (AB#6392). The initiating caller travels with the import
job, so no separate restriction of ImportRt to administrators is needed for this protection.
Who is affected
| Caller | Locked entity of a protected type |
|---|---|
| Tenant user, including a user with a write grant for the type | Refused with BLUEPRINT_LOCKED |
Pipeline node with Identity: ServiceAccount | Refused: a configured service account is guarded like a user. A pipeline that must write product-owned entities has to use Identity: System |
System context: blueprint install, update and forced re-apply, Identity: System pipelines, internal services | Allowed |
Unlocked entity (rtBlueprintLocked is false or absent), or a type that is not protected | Allowed, nothing changes |
If the data policy runs in AuditOnly mode, the change succeeds and an audit event DataPermissions.BlueprintLockViolation
is published instead of the error.
Handling errors in code
TypeScript/JavaScript example
const response = await client.query({ query: GET_ENERGY_METERS });
if (response.errors) {
for (const error of response.errors) {
console.error(`Error: ${error.message}`);
if (error.extensions?.code === 'ENTITY_NOT_FOUND') {
// Handle missing entity
} else if (error.extensions?.code === 'FORBIDDEN') {
// Handle permission error
} else if (error.extensions?.code === 'BLUEPRINT_LOCKED') {
// The entity is owned by the product: show it read-only
console.warn(`Locked by blueprint: ${error.extensions.rtId}`);
}
}
}
if (response.data) {
// Process successful data
}
Checking for partial success
const response = await client.mutate({
mutation: CREATE_ENTITIES,
variables: { entities: [...] }
});
// Check if any entities were created despite errors
const createdEntities = response.data?.runtime?.myEntities?.create ?? [];
const failedCount = entities.length - createdEntities.length;
if (response.errors && failedCount > 0) {
console.warn(`${failedCount} entities failed to create`);
}
Validation errors
Validation errors typically include details about which field failed and why:
{
"errors": [
{
"message": "Validation failed",
"extensions": {
"code": "VALIDATION_ERROR",
"validationErrors": [
{
"field": "voltage",
"message": "Value must be between 0 and 500"
},
{
"field": "name",
"message": "Name must not be empty"
}
]
}
}
]
}
Best practices
- Always check for errors: Even successful-looking responses may contain partial errors
- Use error codes: Match on
extensions.coderather than parsing error messages - Handle partial data: Process any valid data returned alongside errors
- Log error details: Include
pathandextensionsin logs for debugging - User-friendly messages: Map error codes to localized user-facing messages
- Retry transient errors: Network or timeout errors may succeed on retry