Skip to content

Errors and responses

JSON endpoints return one envelope shape:

{
"Data": { },
"Errors": [],
"Warnings": [],
"Success": true
}

Check Success first. On failure, Data is usually null and Errors explains why; Warnings uses the same error shape for non-blocking notices (for example, schema-change plans that need confirmation). The exceptions to the envelope are file downloads (raw content), low-level protocol failures such as malformed JSON or an unknown route, and unhandled server failures. Protocol and unhandled failures return an HTTP problem response. Entity record create, update, archive, restore, and delete, and the composite Entity mutation, answer malformed JSON in the envelope instead, with moltaro.request.binding.invalid.

An unhandled server failure returns HTTP 500 with a safe Problem Details body. It does not expose the exception message, stack trace, SQL, paths, or other internal details:

{
"type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
"title": "An error occurred while processing your request.",
"status": 500,
"correlationId": "e4318a5f51994841ad733f40b5f216d8"
}

The same value is returned in the X-Correlation-ID response header. Record it when reporting an unexpected failure; operators can use it to find the corresponding server-side diagnostics without exposing those diagnostics to the caller. A caller may also send X-Correlation-ID on the request to carry an existing trace identity through the operation.

Each entry in Errors and Warnings has:

PropertyMeaning
CodeStable machine-readable code — branch on this, never on the message.
MessageHuman-readable text in the caller’s interface language: the personal locale chosen on the Profile page, otherwise the workspace locale.
FieldThe request property the error refers to, when field-specific.
TargetOptional identifier of the object the error refers to.
MetadataOptional string map with error-specific context values.
Type0 Business (state/rules), 1 Validation (bad input), 2 System.
Severity0 Info, 1 Warning, 2 Error.

Codes are stable dotted strings shaped as domain.subject.reason, for example moltaro.definitions.name.required or moltaro.fields.key.exists. They double as localization keys, so the same failure always carries the same code in every locale. For the single-record Entity mutations and the composite Entity mutation, the installation serves a machine-readable installed error catalog; the error code reference documents the same definitions and lists other common codes. Other published modules return the same error shape and stable codes without being enumerated there.

Suffixes are a naming convention, not a contract:

  • *.required — a mandatory request value is missing;
  • *.invalid — a value is present but not acceptable;
  • *.exists / *.duplicate — a uniqueness rule was hit;
  • *.notFound — the addressed object does not exist or is not visible;
  • *.conflict — the request collided with the current state of the object. What that state is depends on the code: a stale Entity record version is moltaro.security.concurrency.staleRowVersion, while moltaro.instances.conflict means that another write held the record at the same moment. Look up the exact code instead of inferring its reaction from the suffix or from HTTP 409.

Field names the request property using its PascalCase JSON name, e.g. AuditTrailRetentionDays. For items inside collections the path includes the index and nested property: DisplayFields[3].Key, PresentationRules[0].Template.

Failures still use meaningful HTTP statuses — 400 validation, 403 forbidden, 404 not found, 409 state conflict, 503 temporarily unavailable — with the envelope in the body. The status belongs to the endpoint that answered, not to a code everywhere in the product. Treat the envelope’s Errors as the source of detail and the status code as a coarse category. When an answer has several errors, the Entity record mutations use the first of 404, 403, 503 and 409 that one of the codes has, and 400 otherwise; the composite Entity mutation uses 404, 403, 413, 503 and 409 in that order.

Unhandled 500 responses use Problem Details instead of the envelope. Treat them as unknown outcomes: preserve correlationId, verify whether the intended mutation committed, and follow the operation’s documented recovery or idempotency procedure before retrying.

For a saved operation handle, the separate mutation confirmation GET can check an own Entity receipt without resending the body. An error from that GET describes the check, not the write; NoConfirmedReceipt also leaves the write unresolved. This read is outside the installed catalog’s mutation surfaces.

Do not retry every non-success response. Correct a 400, ask the user or administrator to resolve a missing credential or permission on 401/403, verify the workspace API origin and re-discover ids on 404, and branch on the code of a 409. Replay answers are the exception: after 403 moltaro.instances.replay.resultNotViewable the record already exists, 409 moltaro.security.idempotency.keyReuse calls for a new key only for a genuinely new operation, and 409 moltaro.security.idempotency.replayInvalid must not be retried. A coding agent must not create credentials or grant itself permissions through administrative APIs.

The complete response strategy, including safe retries after unknown outcomes, idempotency keys, build/job polling, concurrency values, and a recovery table, is in Reliable API automation.

Every installation describes the errors of its single-record Entity mutations (create, update, archive, restore and permanent delete) and of its composite Entity mutation in a machine-readable catalog:

GET ${WORKSPACE_API_BASE_URL}/api/workspace/error-catalog

Any active workspace user may read it, including a Service user with its API key; no administrative role is needed. The installation serves it locally, with Cache-Control: no-store, in the usual ApiResponse envelope. Its schema is in the installation’s Runtime OpenAPI (operation error-catalog-get). The administrative agent-integration manifest links to it as ErrorCatalogUrl; you do not need that manifest to use it.

Data contains:

PropertyMeaning
SchemaVersionVersion of the catalog format, 1. It changes only with an incompatible change: new members and new enum values keep version 1.
RevisionOpaque identifier of the catalog content. Equal content has an equal revision on every installation; compare it for equality only.
ProductVersion, BuildIdThe answering API’s release and build, as in the agent-integration manifest; null for a source build.
CoverageWhat is covered: the surfaces, their operations, HTTP status precedence and Fallbacks for answers no entry describes.
EntriesOne entry per code, surface and group of operations: Code, Operations, HttpStatus, Description, Field rule, Metadata keys and Reaction.

A Reaction tells what the answer establishes and what to do next:

  • Outcome — Unknown (0), NotApplied (1): this request changed nothing, AlreadyCommitted (2): the same operation committed earlier and its result is not returned, or KeyUsedByAnotherRequest (3).
  • Action — the recommended reaction, such as CorrectRequest, ReadAndReconcile, ReconcileCommittedOperation or RetrySameRequest.
  • AutomaticRetry — None (0): never repeat on your own; SameRequest (1): the identical request (same OperationKey, body including RowVersion, and optional X-Moltaro-Markdown-* headers) may be repeated after a delay, a bounded number of times; Conditional (2): only when every listed condition is verified.
  • Conditions and Guidance — English conditions and explanation.

A Reaction is a recommendation, not a permission. SameRequest alone does not allow a repeat: the OperationKey prevents a second execution only while the first attempt of the operation is less than seven days old, so check that age before every automatic repeat.

Enums are numeric; read their names and descriptions from x-enum-varnames and x-enum-descriptions in the installed OpenAPI. The meaning of each code is in the error code reference.

Version 1 is curated, not a registry of every error. Validation rules, Business Invariants, modules and installation-wide checks such as the license can answer with codes it does not list; an unlisted code is a valid answer. Look up an error by Code, surface and operation; a code can have separate entries for different operations. If any error of an answer is not listed there, use the surface’s UnlistedAnswer fallback: show the code and message to a person and keep the OperationKey and body until the outcome is clear. The fallbacks also cover 401 and a bodiless 403 (AccessRejected), a 5xx without the envelope or no answer at all (ServerFailureOrNoAnswer), and any other answer without the envelope, such as a 404, 413 or 415 from a proxy or the web server (UnlistedAnswer again). There is no error code for a transport failure. The record, composite and Form submission surfaces all treat an unlisted code this way.

The field checks of a record save are listed on all three surfaces as NotApplied without an automatic repeat: required fields (moltaro.instances.validation.requiredFields, then one moltaro.instances.field.required or moltaro.instances.field.requiredNotWritable per field or Table cell) and values that do not fit their field (moltaro.instances.field.invalid, moltaro.instances.field.currency.invalid, moltaro.instances.field.timeZone.invalid, moltaro.instances.field.addressCoordinatesRequired, moltaro.instances.classifier.category.invalid, moltaro.instances.classifier.accessDenied, moltaro.associations.endpoint.immutable and moltaro.referenceEligibility.failed). Nothing of the request was written: correct the named field and send the corrected request. Only these exact codes are listed; other field-level codes, such as a Markdown source limit or an unknown field key, stay unlisted.

Later catalogs can add members and enum values without a new SchemaVersion. Ignore members you do not recognize. Treat an Outcome, Action or AutomaticRetry value you do not recognize like member 0, its most conservative reading: a reaction with any unrecognized value never permits a repeat, even when its AutomaticRetry is SameRequest, and neither does an answer in which one error has such a reaction. Skip entries of a Surface you do not recognize, ignore an unrecognized value in Operations, show Field without relying on it when its rule has an unrecognized Kind, and ignore a fallback whose Answer kind you do not recognize. A client generated from the OpenAPI must accept enum values outside the listed ones.

The catalog describes the REST endpoints only, each under its own surface: EntityRecordRest (0) for the single-record mutations and EntityCompositeRest (1) for the composite mutation, whose entries name the failing step in Field patterns such as Steps[i].Update.RowVersion and in the StepId metadata. C# application automation (IEntityAutomationCommandService and IEntityCompositeMutationService) can report another code for the same condition, because codes are canonicalized at the HTTP boundary; bulk, import and record merge have their own result shapes and are not covered.

CodeWhat happenedWhat to do
moltaro.security.concurrency.staleRowVersionThe record (Field RowVersion) or, on create, the record type (Field ExpectedDefinitionRowVersion) changed after you read it. Nothing was written.Read it again, compare, decide again; send a reconciled change as a new operation. Never copy the new version into the old request.
moltaro.instances.conflictAnother write held the record at the same moment. Nothing was written and the key stays unused.Repeat the identical request after a delay, a few times at most.
moltaro.security.idempotency.keyReuseA different request already committed with this OperationKey.An exact retry resends the original request unchanged; a new key only for a genuinely new operation.
moltaro.security.idempotency.replayInvalidThe same operation committed, but its stored result can no longer be read. This is not a rollback.Do not send it again with any key; check the record state.

Committed but unreadable, and unknown outcomes

Section titled “Committed but unreadable, and unknown outcomes”

A successful answer is never an error, even when it is a Receipt without record data or a replay (IsIdempotentReplay: true); do not look it up in the catalog. After 403 moltaro.instances.replay.resultNotViewable, the create already happened although you cannot see the record: never create it again with a new key.

The composite Entity mutation adds one listed unknown outcome: 503 moltaro.instances.composite.outcomeUnknown says that a commit of the request may have happened. Repeat the identical request to settle it, as below; never send the steps with a new key before a repeat answers.

A timeout, a lost connection or a 5xx without the envelope does not prove a rollback. Keep the OperationKey, the body, its optional X-Moltaro-Markdown-* headers and the X-Correlation-ID, and repeat the identical request: within seven days a committed operation returns its stored result instead of executing again. A check that runs before the replay, such as access, the record type’s state or a disabled operation, can reject the repeat with another error; that error describes only the repeat, so read the target or ask a person before sending anything with a new key. After the seven days a repeat can execute again, so reconcile the target state first. Count the seven days from the first attempt of the operation: store that time with the OperationKey and body, and do not restart it after a reload or a later attempt. When the time is unknown, do not repeat automatically. A missing receipt does not prove that the operation did not run.

The catalog describes the build that answered. When comparing installations or diagnosing a client, record the API base URL together with Revision, ProductVersion and BuildId. The public documentation describes the build of the documentation site; read the installed catalog of the API you call. SchemaVersion changes only with an incompatible format change, so a newer build can describe more codes and values under the same version.

This sketch loads the catalog once and looks up each error of a mutation answer. It keeps the catalog’s recommendation (recommended) apart from the decision to send the identical request again now (repeat), which also needs the time the operation’s first attempt was sent. An unrecognized value never leads to a repeat, and the OperationKey never changes. Pass status as undefined when no answer arrived, body as undefined when the answer is not JSON, and firstAttemptAt in milliseconds since the epoch:

const catalogResponse = await fetch(`${baseUrl}/api/workspace/error-catalog`, { headers });
const catalog = (await catalogResponse.json()).Data;
const OPERATIONS = ['Create', 'Update', 'Archive', 'Restore', 'Delete'];
const KNOWN_VALUES = { Outcome: 4, Action: 9, AutomaticRetry: 3 }; // values 0..n-1 this client understands
const SAME_REQUEST = 1;
const UNLISTED_ANSWER = 0, ACCESS_REJECTED = 1, SERVER_FAILURE_OR_NO_ANSWER = 2;
const REPLAY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
const SAFETY_MARGIN_MS = 60 * 60 * 1000; // the repeat must still arrive inside the window
const isKnown = (reaction, member) => Number.isInteger(reaction?.[member])
&& reaction[member] >= 0 && reaction[member] < KNOWN_VALUES[member];
const recommendsRepeat = reaction => isKnown(reaction, 'Outcome') && isKnown(reaction, 'Action')
&& reaction.AutomaticRetry === SAME_REQUEST;
const insideReplayWindow = (firstAttemptAt, now) => Number.isFinite(firstAttemptAt)
&& now >= firstAttemptAt && now - firstAttemptAt < REPLAY_WINDOW_MS - SAFETY_MARGIN_MS;
function decide(operation, status, body, firstAttemptAt, now = Date.now()) {
if (body?.Success) return { kind: 'success', recommended: false, repeat: false }; // includes receipts and replays
const surface = catalog.Coverage.Surfaces.find(item => item.Surface === 0);
const fallback = answer => surface.Fallbacks.find(item => item.Answer === answer)?.Reaction;
let kind = 'known', reactions;
if (!Array.isArray(body?.Errors)) { // no ApiResponse envelope
kind = 'fallback';
reactions = [fallback(status === 401 || status === 403 ? ACCESS_REJECTED
: status === undefined || status >= 500 ? SERVER_FAILURE_OR_NO_ANSWER
: UNLISTED_ANSWER)];
} else {
const entries = body.Errors.map(error => catalog.Entries.find(entry =>
entry.Code === error.Code && entry.Surface === 0
&& entry.Operations.includes(OPERATIONS.indexOf(operation))));
[kind, reactions] = entries.length === 0 || entries.includes(undefined)
? ['unlisted', [fallback(UNLISTED_ANSWER)]]
: [kind, entries.map(entry => entry.Reaction)];
}
const recommended = reactions.every(recommendsRepeat);
return { kind, reaction: reactions[0], recommended,
repeat: recommended && insideReplayWindow(firstAttemptAt, now) };
}

The sketch reads the single-record surface (Surface 0). For the composite mutation, look up entries and fallbacks of surface 1 with operation CompositeMutation (5); the replay-window rules are the same.

Show an unlisted code with its Message and Field to a person. A repeat decision allows only a few attempts of the unchanged request with a delay. Record firstAttemptAt once, when the first attempt of the operation is sent, and store it with the OperationKey and body; a reload or a later attempt never changes it. The sketch stops automatic repeats an hour before the seven days end, so that the last repeat still reaches the server while the stored result exists. When recommended is true but repeat is false, keep the operation and settle its outcome by reading the target or asking a person.