Entity lifecycle and reference candidates
All /api/workspace/... routes below resolve against WORKSPACE_API_BASE_URL,
the installation API origin supplied by the administrator. They do not resolve
against moltaro.com. Workspace UI code uses its authenticated
Moltaro API client.
Check the answering installation
Section titled “Check the answering installation”An administrator with existing access can read
GET /api/workspace/admin/agent-integration. Record Data.ProductVersion and
Data.BuildId, then use Data.PublicOpenApiUrl and Data.ConfigurationOpenApiUrl
for the installation’s Runtime and Configuration schemas. A source build without
package identity returns null version/build values; older installations may omit
these properties. Neither case identifies a release. Ordinary users do not gain
administrative access through this endpoint.
Without administrative access, any active user or Service user can read
GET /api/workspace/error-catalog: its ProductVersion and BuildId identify
the answering build, and its entries describe the error codes of the mutations
below and of the composite Entity mutation
for that build. See Installed error catalog.
Data.ReleaseNotesVersionUrl, when present, links the installed release note.
The public documentation and /docs/llms-manifest.json describe the documentation
deployment, not the answering API or independently updated WebApp/Worker. These
examples require a build containing the documented contract; confirm its local
schema before applying them to an older installation. Do not provision new access
merely to run these examples.
To check a lost write response with a handle saved before sending, use Read a mutation confirmation. This GET needs no original body and adds no write. Absence of a confirmed receipt is never permission to retry the mutation.
Archive, restore, and permanent delete
Section titled “Archive, restore, and permanent delete”| Operation | Method and suffix after /api/workspace/entity/{entityIdOrKey}/instances/{instanceId} | Required state |
|---|---|---|
| Archive | DELETE /archive | Active record |
| Restore | POST /restore | Archived record |
| Permanent delete | DELETE | Archived record; referencing Restrict relationships may block deletion |
Each operation takes this JSON body. Replace the illustrative identities and versions with values discovered from the installation:
{ "OperationKey": "b84c10ed-6824-4a59-989f-f2f8117cf0c1", "EntityDefinitionKey": "SupportTicket", "EntityInstanceId": "record-id-from-get", "RowVersion": "53cedb7e-dad6-425c-86c1-edd23dacfd59", "Reason": "Duplicate confirmed by the record owner"}Reason is required for all three operations, including privileged callers.
Missing, null, empty, and whitespace-only values fail. After trimming leading and
trailing whitespace, the reason must contain 1–1000 characters. The length rule
applies to the trimmed value; it is not a raw-JSON maxLength rule. A reason does
not grant permission or bypass lifecycle, relation, or concurrency checks.
The trimmed reason is stored with the operation; the record change feed returns
it as Lifecycle.Reason on archive and restore entries. See
Lifecycle reasons.
Read the record first and send its current RowVersion. Use a new OperationKey
for each new logical operation. Reuse a key only for an exact retry of the same
request after an uncertain outcome: for seven days after the first execution the
retry confirms the committed operation with IsIdempotentReplay: true and writes
nothing again. Reusing a key with a different request fails with HTTP 409
moltaro.security.idempotency.keyReuse. After archive or restore, use the new
authorized row version before the next operation. An AuthorizedEntityDetail
result contains Data.Data.RowVersion; a committed Receipt after loss of read
access contains no record values or row version. A retry after you lost read
access to the record, or after it was permanently deleted, also answers a
Receipt. Do not treat that receipt as a failed write or invent a version.
A retry after the operation was disabled for the record type answers HTTP 409
moltaro.instances.operation.disabled instead of the stored result. Permanent
delete returns a receipt. See mutation results.
Request errors
Section titled “Request errors”Create, update, archive, restore, and permanent delete check the request in one
order: the body, OperationKey, the definition and record of the route, then the
members of the operation. The first failure returns HTTP 400 with Success: false
and one structured error whose Field is the public request member:
Errors[].Code | Field | Action |
|---|---|---|
moltaro.request.required | Request | Send a JSON body |
moltaro.security.operationKey.required | OperationKey | Send a new UUID for each logical operation |
moltaro.request.required | EntityDefinitionKey, EntityInstanceId, RowVersion, Patch, Values, ResponsibilityCandidates, ExpectedDefinitionRowVersion | Supply the member; a missing or empty UUID counts as missing |
moltaro.request.invalid | EntityDefinitionKey, EntityInstanceId | Send the definition and record of the route |
moltaro.instances.reason.required | Reason | Supply a nonblank reason |
moltaro.instances.reason.tooLong | Reason | Reduce the trimmed reason to 1000 characters |
moltaro.request.binding.invalid | Safe request field, else Request | Correct malformed JSON, unsupported properties, or value types using local OpenAPI |
JSON that cannot be bound is answered before the route is resolved, so it can
precede a 404 for an unknown entity; field keys inside Values and Patch are
reported on their member and never echoed. A malformed Guid is a binding failure;
a well-formed stale version is a concurrency conflict (HTTP 409
moltaro.security.concurrency.staleRowVersion with Field RowVersion), not an
instruction to force the write: read the record again. HTTP 409
moltaro.instances.conflict means that a concurrent write held the record; nothing
was written, and the identical request can be repeated after a delay. Server
authorization remains authoritative even when a menu previously showed the action
as allowed. The installed error catalog
lists these codes with their Field rules and reactions. A
composite Entity mutation that
creates or updates several records in one transaction checks its steps with
its own request rules and names the failing step in Field.
Select the correct reference context
Section titled “Select the correct reference context”The prefix in this table is /api/workspace/entity/{entityIdOrKey}/ui.
For Form/Table routes it identifies the source entity; for the generic picker
it identifies the target entity. IDs and revision tokens come from the current
saved runtime surface, not guessed field names or an unsaved Constructor draft.
| Context | Read route after prefix | Context and confirmation |
|---|---|---|
| Saved Form List or LongList | POST /form/{formSurfaceId}/items/{formItemId}/reference-candidates/query | ExpectedFormSurfaceRowVersion, SourceInstanceId, allowed CurrentValues; confirm through the same prefix ending /reference-candidates/selection |
| Saved Form flat Lookup | POST /form/{formSurfaceId}/items/{formItemId}/lookup-table/bootstrap | Saved Lookup item and its revision; table preflight identity when supplied; confirm through anchored /reference-candidates/selection |
| Saved Form Tree Lookup | Same bootstrap, then /lookup-table/tree/children | Use the returned tree context and /lookup-table/tree/selection; navigation-only nodes cannot be confirmed |
| Saved Table reference filter | POST /table/{tableSurfaceId}/filters/{filterItemKey}/reference-candidates/query | ExpectedTableSurfaceRowVersion, bounded DraftFilterTree; confirm through sibling /reference-candidates/selection |
| Generic target picker | POST /reference-picker/table/bootstrap | Target entity and role-resolved table, optionally tableSurfaceKey; has no source Form field/draft eligibility context |
The anchored candidate query/selection also supports a flat Lookup item, but only Lookup input mode supports lookup-table bootstrap. List/LongList cannot consume a Tree table. These are supported-context differences, not interchangeable URLs.
CurrentValues is a direct map of server-declared writable draft fields to values;
scalar references use raw IDs. Do not add a Fields wrapper. Use the runtime
ReferenceContext.CurrentValueInputs allow-lists for Create/Update and any owned
locked-create inputs. Omitted server-owned values are reconstructed by the server.
An editable effect target can still be a permitted user-owned draft input: follow
the operation’s allow-list and its current field origin, not the mere existence
of an effect. Do not submit hidden, non-allowed, or currently server-owned values
as user overrides; calculated targets are not draft inputs.
Read the response’s State and, when present, ErrorCode before using Items.
A successful HTTP response can describe ContextIncomplete, ConditionNotMet,
DependencyUnavailable, or another non-ready state; an empty list alone does not
mean that no records exist. Stale surface revisions return a conflict; refresh
the surface and rebuild its context. Wrong input mode is a validation failure.
Selection is independently revalidated and may no longer be eligible.
Never retry a failed anchored request through the generic picker to bypass the context. Generic visibility is not proof of source-field eligibility. Use the anchored selection check before updating a Form draft, and let the save command validate the final write again.
Search and page eligible candidates
Section titled “Search and page eligible candidates”For the anchored candidate query, send:
{ "ExpectedFormSurfaceRowVersion": "0d834bd9-dc7a-438a-9bf6-005434d3b901", "SourceInstanceId": null, "CurrentValues": {}, "Query": { "Search": "river", "Page": 1, "PageSize": 250 }}Use the saved Form revision and fill the allowed current-value inputs required by
that actual context. SourceInstanceId: null means Create; use the existing source
record ID for Update. Query.Search is canonical. Query.SearchText is rejected.
- Omitted, null, empty, or whitespace-only
Searchadds no search predicate. - Ordinary search accepts 3–256 trimmed characters and uses configured
SearchTargets. Matching is case-insensitive substring search;%,_, and\are literals. Exact#numberand#{id}shortcuts follow the existing Entity search rules. - Nonblank search on an entity without configured search targets returns
moltaro.search.notAvailable. It does not silently search all columns. - Search, an optional ordinary
Query.Filter, eligibility and access rules are intersected. Candidate filtering occurs before pagination; counts describe the authorized result, and an unmatched valid search returns an empty Ready page. Query.Pagedefaults to 1 and must be at least 1.Query.PageSizedefaults to 25 and must be 1–250. Omission uses the default; zero, negative, null, a wrong type, or a value above 250 is not a request for an unlimited page.- A Ready result returns
Page,PageSize,TotalCount, andItems. IfPage * PageSize < TotalCount, request the next page with the same search, filter, sort and current-value context. Stop at the total; a page beyond the result is empty. Requery after changes: separate requests are not one immutable snapshot. The 250 limit applies to one page, not the entire accessible set.
Out-of-range integers return moltaro.referenceCandidates.page.invalid on
Query.Page or moltaro.referenceCandidates.pageSize.invalid on Query.PageSize.
Missing/null Query returns moltaro.request.required on Query.
Malformed JSON, a wrong type, or an unknown property returns
moltaro.request.binding.invalid; error fields are bounded public paths and
messages do not echo submitted values. Read codes and fields, not translated text.
These strict rules belong to candidate queries. Ordinary lists and flat lookup
bootstraps have their existing page normalization and effective Table Surface
defaults; their request shape uses top-level Page, PageSize, and Filter.
They do not accept the candidate Query.Search envelope. Express search there
with the existing SearchTerm filter in the
Entity Instance Query contract.