Errors and responses
The response envelope
Section titled “The response envelope”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.
The error shape
Section titled “The error shape”Each entry in Errors and Warnings has:
| Property | Meaning |
|---|---|
Code | Stable machine-readable code — branch on this, never on the message. |
Message | Human-readable text in the caller’s interface language: the personal locale chosen on the Profile page, otherwise the workspace locale. |
Field | The request property the error refers to, when field-specific. |
Target | Optional identifier of the object the error refers to. |
Metadata | Optional string map with error-specific context values. |
Type | 0 Business (state/rules), 1 Validation (bad input), 2 System. |
Severity | 0 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 ismoltaro.security.concurrency.staleRowVersion, whilemoltaro.instances.conflictmeans 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 HTTP409.
Field paths
Section titled “Field paths”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.
HTTP status codes
Section titled “HTTP status codes”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.
Installed error catalog
Section titled “Installed error catalog”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-catalogAny 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:
| Property | Meaning |
|---|---|
SchemaVersion | Version of the catalog format, 1. It changes only with an incompatible change: new members and new enum values keep version 1. |
Revision | Opaque identifier of the catalog content. Equal content has an equal revision on every installation; compare it for equality only. |
ProductVersion, BuildId | The answering API’s release and build, as in the agent-integration manifest; null for a source build. |
Coverage | What is covered: the surfaces, their operations, HTTP status precedence and Fallbacks for answers no entry describes. |
Entries | One 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, orKeyUsedByAnotherRequest(3).Action— the recommended reaction, such asCorrectRequest,ReadAndReconcile,ReconcileCommittedOperationorRetrySameRequest.AutomaticRetry—None(0): never repeat on your own;SameRequest(1): the identical request (sameOperationKey, body includingRowVersion, and optionalX-Moltaro-Markdown-*headers) may be repeated after a delay, a bounded number of times;Conditional(2): only when every listed condition is verified.ConditionsandGuidance— 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.
Coverage is partial
Section titled “Coverage is partial”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.
Different kinds of 409
Section titled “Different kinds of 409”| Code | What happened | What to do |
|---|---|---|
moltaro.security.concurrency.staleRowVersion | The 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.conflict | Another 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.keyReuse | A 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.replayInvalid | The 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.
Versions
Section titled “Versions”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.
A client that handles unknown codes
Section titled “A client that handles unknown codes”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 understandsconst 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.