This is the full developer documentation for Moltaro
Documentation-Version: b867ef98f2e1
Product-Version: 0.0.32-beta
Source-Commit: b867ef98f2e1a4de828b8c59873637a69200315e
Source-Tree-State: Clean
Generated-At-UTC: 2026-08-29T10:30:31.267Z
Change-Base-Commit: 12e1a63c4a485b6a653a3eeb209a62b2a5528cb0
Manifest: https://moltaro.com/docs/llms-manifest.json
Release-Notes: https://moltaro.com/docs/developer/release-notes/
Release-Notes-For-Version: https://moltaro.com/docs/developer/release-notes/0.0.32-beta/
Change-List-Coverage: Partial
Changed-Pages-Since-Base: 7
- developer/api-reference
- developer/configuration-api-reference
- developer/dotnet-reference
- developer/package-sdk
- developer/release-notes
- developer/release-notes/0.0.31-beta
- developer/release-notes/0.0.32-beta
Removed-Pages-Since-Base: 0
Changed-Artifacts-Since-Base: 6
- api/dotnet
- api/openapi/moltaro-config-v1.json
- api/openapi/moltaro-public-v1.json
- llms-full.txt
- llms-manifest.json
- llms-small.txt
# Moltaro documentation
> Find the guide that matches what you need to do in Moltaro.
Choose a documentation track by what you need to do: use Moltaro, configure an application, extend it with code, or operate a deployment.
## Choose your documentation
[Section titled “Choose your documentation”](#choose-your-documentation)
### [User documentation](/docs/user/)
[Section titled “User documentation”](#user-documentation)
For people who work in Moltaro day to day. Start here for product concepts, records, boards, Work Schedule, entitlements, access, and everyday workflows.
Go directly to the [Work Schedule guide](/docs/user/work-schedule/) to plan worker and site availability, manage working calendars, and understand the effective schedule.
### [Configuration](/docs/configuration/)
[Section titled “Configuration”](#configuration)
For administrators and solution builders who set up data models, modules, team access, screens, record behavior, automation rules, and the working environment for a Moltaro application.
### [Developer documentation](/docs/developer/)
[Section titled “Developer documentation”](#developer-documentation)
For developers building API integrations, C# business logic, AI-agent automation, or custom workspace pages. Start with the developer overview to choose the right quickstart, programming model, or reference.
Use the [Work Schedule developer guide](/docs/developer/work-schedule/) for the supported C# application-automation facade and its generated .NET reference.
### [On-premise operations](/docs/operations/)
[Section titled “On-premise operations”](#on-premise-operations)
For deployment, updates, backups, monitoring, and operational administration of an on-premise Moltaro installation.
## Reference
[Section titled “Reference”](#reference)
### [Glossary](/docs/glossary/)
[Section titled “Glossary”](#glossary)
For concise definitions of Moltaro terms, with links back to the sections where each concept is explained in context.
# Connect to a workspace API
> Distinguish the public Moltaro website and documentation from the Web Application and API host of a configured workspace.
Moltaro’s public website and a customer’s Moltaro workspace are different systems. Before making an API request, identify which URL the request belongs to. A path that starts with `/api/workspace/` is always served by the API host of a configured Moltaro workspace. It is never served by `moltaro.com`.
Do not send workspace requests to the public website
`https://moltaro.com/docs/...` is documentation. It is not a workspace API base URL. Do not append `/api/workspace/...` to `https://moltaro.com`.
## The URLs have different roles
[Section titled “The URLs have different roles”](#the-urls-have-different-roles)
| URL | Example | Purpose |
| ----------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Public website, portal, and documentation | `https://moltaro.com/docs/` | Product documentation, public reference copies, account and deployment entry points. It has no access to a customer’s workspace data. |
| Workspace Web Application | `https://acme.apps.example.com/` | The signed-in product UI. The workspace owner completes administrative setup and prepares Agent integration through the Portal or administrative interface. |
| Workspace API base URL | `https://acme.api.example.com` | The origin for `/api/workspace/...` and the installation-local `/openapi/...` documents. Use this URL for development and integration requests. |
Depending on the deployment, the Web Application and API may share an origin or use different origins. Never derive one by changing the hostname yourself. The workspace-specific guide prepared by the administrator provides the authoritative workspace API base URL.
The documentation uses `WORKSPACE_API_BASE_URL` for that value. For example:
```text
GET ${WORKSPACE_API_BASE_URL}/api/workspace/context
GET ${WORKSPACE_API_BASE_URL}/openapi/moltaro-public-v1.json
GET ${WORKSPACE_API_BASE_URL}/openapi/moltaro-config-v1.json
```
In command examples, `https://ops.example.com` is a placeholder for the same `WORKSPACE_API_BASE_URL`; replace the whole origin, not only part of the path.
## Administrative handoff before API development
[Section titled “Administrative handoff before API development”](#administrative-handoff-before-api-development)
Web Application installation or registration and access provisioning happen in the Moltaro Portal or administrative Web Application interface. They are workspace-owner responsibilities, not steps for a coding agent to automate. This developer documentation intentionally starts after that setup.
Under **Administration > Agent integration**, the administrator selects **Create service user for agent** and confirms the high-privilege handoff. Moltaro creates a distinct Service user with the Admin and Configurator roles, issues a 90-day API key, and downloads a ZIP containing:
* `AGENTS.md` and `CLAUDE.md` with workspace-specific instructions;
* `.moltaro/credentials.env` with the exact API URL, service-user identity, key expiration, and one-time API key;
* `.moltaro/.gitignore`, which excludes `credentials.env` from version control.
Extract the kit into the intended project and delete the downloaded ZIP. Keep `.moltaro/credentials.env` local: never commit, upload, paste, echo, or print it. The API key is not shown again after the download.
If any of these inputs is missing, an agent must stop and ask the user to complete Agent integration in the Portal or administrative interface. It must not try to install or register the Web Application, create a service account, rotate credentials, or grant itself permissions through an API.
The developer or agent loads `.moltaro/credentials.env` and sends `MOLTARO_API_KEY` as `Authorization: Bearer `. Do not put the key in the guide, source code, a prompt, logs, or version control.
The two Markdown files contain the same workspace-specific guide under conventional filenames. They describe the particular installation that generated them; they are not generic copies of the public documentation.
## What the Agent integration endpoints are for
[Section titled “What the Agent integration endpoints are for”](#what-the-agent-integration-endpoints-are-for)
The following are authenticated endpoints on the workspace API host:
```text
GET ${WORKSPACE_API_BASE_URL}/api/workspace/admin/agent-integration
GET ${WORKSPACE_API_BASE_URL}/api/workspace/admin/agent-integration/agents-md
POST ${WORKSPACE_API_BASE_URL}/api/workspace/admin/agent-integration/access-kit
```
These are administrative endpoints of the configured Moltaro Application, not endpoints of the public Portal. An administrator, provisioning tool, or coding agent may call them after it knows the workspace API base URL and has suitable credentials. They remain part of the published Configuration API.
They are not global discovery endpoints. The `access-kit` operation is the administrator-owned UI action that performs the one-time handoff; an agent must not call it to create or elevate its own access. After receiving the kit, the agent may use the two read endpoints to re-read the manifest or guide.
## Public and installation-local contracts
[Section titled “Public and installation-local contracts”](#public-and-installation-local-contracts)
The public documentation hosts reference copies of the Runtime and Configuration OpenAPI documents. They are useful before a workspace is available and for browsing supported concepts. For actual development, use the OpenAPI documents served by the target installation:
```text
${WORKSPACE_API_BASE_URL}/openapi/moltaro-public-v1.json
${WORKSPACE_API_BASE_URL}/openapi/moltaro-config-v1.json
```
Those documents match the installed release. The downloaded guide also links to the installation-local developer surface for supported C# assemblies and injectable services. Public documentation explains the model; the target workspace remains authoritative for its URL, schema, modules, permissions, OpenAPI contract, and C# developer surface.
Continue with the [Integration quickstart](/docs/developer/integration-quickstart/) for record operations, the [Configuration quickstart](/docs/developer/configuration-quickstart/) for schema authoring, or the [AI agent development quickstart](/docs/developer/ai-agent-development/) for the complete API-first C# and Workspace UI workflow. Apply the concurrency, retry, recovery, and audit rules in [Reliable API automation](/docs/developer/reliable-api-automation/) to both human and agent-driven integrations.
# AI agent development quickstart
> API-first workflow for an AI developer to create entities, C# business logic, board and entitlement operations, audit events, and Workspace UI pages.
An AI coding agent can develop a Moltaro workspace without downloading a project or reading product source code. The generated `AGENTS.md` and `CLAUDE.md` are intentionally compact bootstrap guides: they identify the workspace, credentials, safety rules, and authoritative sources instead of repeating feature playbooks. Use the full agent-readable documentation for product semantics and examples, the installation’s two OpenAPI documents for exact HTTP contracts, and the server-side C# language service for source work.
This workflow starts after workspace setup
`moltaro.com` hosts public documentation; it does not host a customer’s workspace API. Web Application registration and Agent integration are completed by the workspace owner in the Portal or administrative interface, not by the agent. The agent starts only after the user provides the generated access kit with `AGENTS.md` or `CLAUDE.md` and `.moltaro/credentials.env`. If that handoff is incomplete, stop and ask the user to complete it. See [Connect to a workspace API](/docs/developer/workspace-api-connection/).
Load `.moltaro/credentials.env` and use `MOLTARO_API_KEY` as a bearer token. The generated identity has Admin and Configurator access for the authoring operations on this page. Never put the key in source, prompts, logs, or generated output, and never try to create, rotate, or expand the account’s permissions yourself.
## Read the installed release context first
[Section titled “Read the installed release context first”](#read-the-installed-release-context-first)
Before starting or resuming work, fetch [`llms-manifest.json`](/docs/llms-manifest.json), record its `ProductVersion` and `DocumentationVersion`, and open the matching detail page from the [versioned release notes](/docs/developer/release-notes/). Read the summary, detailed changes, migrations, operator actions, and compatibility guidance for that version. Repeat the check when the manifest version or HTTP `ETag` changes. Release notes explain what changed; the current documentation and the installation-local OpenAPI remain authoritative for supported behavior and exact request shapes.
For hierarchical records, follow the installation-safe discovery, configure, read-back, root/child, filter, and picker sequence in the [Parent Tree View developer guide](/docs/configuration/hierarchies/developer-guide/#agent-workflow). For an embedded child table on a Form, follow the complete options, read-modify-write-read, YAML, locked-filter, security, and concurrency workflow in the [Form Related Table guide](/docs/developer/form-related-tables/); never invent its inverse target or submit a client-owned child filter.
## 1. Connect to the configured workspace
[Section titled “1. Connect to the configured workspace”](#1-connect-to-the-configured-workspace)
Read `WORKSPACE_API_BASE_URL` from the downloaded guide. Every route on this page that starts with `/api/workspace/` or `/openapi/` is relative to that URL, never to `https://moltaro.com`.
The guide and contracts for the configured installation are:
```text
GET ${WORKSPACE_API_BASE_URL}/openapi/moltaro-public-v1.json
GET ${WORKSPACE_API_BASE_URL}/openapi/moltaro-config-v1.json
GET ${WORKSPACE_API_BASE_URL}/api/workspace/admin/net-operation-project/developer-surface
```
`developer-surface` is the machine-readable catalog of supported assemblies, versions, injectable service types, lifetimes, module keys, and .NET reference links. Do not infer support from every public CLR type in a DLL.
The following are Moltaro Application Configuration API endpoints, not Portal endpoints:
```text
GET ${WORKSPACE_API_BASE_URL}/api/workspace/admin/agent-integration
GET ${WORKSPACE_API_BASE_URL}/api/workspace/admin/agent-integration/agents-md
POST ${WORKSPACE_API_BASE_URL}/api/workspace/admin/agent-integration/access-kit
```
An authenticated agent may call the two read endpoints after the handoff to re-read the installation manifest or generated guide. The `access-kit` operation belongs to the administrator’s one-time handoff and must not be called by the agent to provision or elevate itself. These routes cannot discover an unknown workspace; the caller must already know `WORKSPACE_API_BASE_URL` and have permission. The same distinction applies to all `/api/workspace/admin/...` routes in this guide: they are supported administrative APIs of the configured Application and remain part of the agent development workflow.
## 2. Create and inspect an entity
[Section titled “2. Create and inspect an entity”](#2-create-and-inspect-an-entity)
Follow the [Configuration quickstart](/docs/developer/configuration-quickstart/) to create the entity definition and its fields. Then read it back before writing code:
```text
GET /api/workspace/entity-definitions
GET /api/workspace/entity-definitions/{entityDefinitionId}
```
Field keys, ids, types, and generated CLR names are data from the installation; never guess them.
Searchability is also explicit installation data. Read the administrative definition’s `SearchTargets`, `SearchMaxPathDepth`, and `RowVersion`, then use the replace-all `search-targets` operation when the requested solution needs record search. Do not generate legacy full-text flags or a rebuild step. The [Entity search contract](/docs/developer/entity-search/) contains the exact Configuration API and runtime `SearchTerm` payloads.
When authoring a `Reference` field through the Configuration API, use the installed OpenAPI enum metadata and choose deletion semantics explicitly: `ReferenceDeleteBehavior` is `0` None, `1` Cascade, or `2` Restrict. New references default to Restrict. A blocked permanent delete returns HTTP 409 with `moltaro.instances.delete.referenced`; branch on the stable code rather than its localized message. See [Configuration API quickstart](/docs/developer/configuration-quickstart/#relational-and-typed-fields). YAML has the same semantics: omission means Restrict for a newly imported Reference or a non-reference field converted to Reference, and preserves the current value when updating a matched Reference; exports contain the effective value.
After any schema change, read the entity again and call the language endpoints again. Every request receives the current server-generated `GeneratedWorkspaceContract`; completion results captured before a field was added, renamed, or removed are stale.
## 3. Preview a C# source template
[Section titled “3. Preview a C# source template”](#3-preview-a-c-source-template)
List templates first because the response states the default folder and which inputs each template requires:
```text
GET /api/workspace/admin/net-operation-project/source-templates
POST /api/workspace/admin/net-operation-project/source-templates/preview
```
The preview does not create a revision. Entity-bound templates accept a target entity definition id and return source using the current generated CLR type.
## 4. Explore the generated contract in an unsaved buffer
[Section titled “4. Explore the generated contract in an unsaved buffer”](#4-explore-the-generated-contract-in-an-unsaved-buffer)
Send the current `Content`, source `Path`, and one-based cursor position to:
```text
POST /api/workspace/admin/net-operation-project/source-language/completions
POST /api/workspace/admin/net-operation-project/source-language/hover
POST /api/workspace/admin/net-operation-project/source-language/signature-help
```
Moltaro automatically adds the current `GeneratedWorkspaceContract` to every Roslyn snapshot using the same two-project graph, controlled references, language options, and analyzers as Check. Send the `CurrentSchemaContractHash` from `GET .../status` as `SchemaContractHash` on every source-language request. If the schema changes meanwhile, Moltaro returns HTTP 409 with `netOperationProject.language.schemaContract.stale`; re-read status, schema, and the source buffer before retrying. Hover and signature help also read XML documentation for the supported SDK and active module assemblies. There is no generated-contract download or edit step in the agent workflow.
Before writing module code, inspect `developer-surface`. For Boards, Entitlement Operations, and Work Schedule, choose the entries with `ServiceKind: ApplicationAutomation` and `Preferred: true`:
```text
IBoardAutomationCommandService / IBoardAutomationQueryService
IEntitlementAutomationCommandService / IEntitlementAutomationQueryService
IWorkScheduleAutomationCommandService / IWorkScheduleAutomationQueryService
```
These services need no manually assembled actor, role set, or Board Data. They run the complete application flow as `moltaro-system-automation`, retain the original user/function run/correlation in origin metadata, and commit in a separate application scope. Use them only from Action, TriggerHandler, Command, Job, or HttpEndpoint code; Validation and BeforeSaveMutation return `moltaro.automation.executionPhase.unsupported`. Entitlement commands need a stable business `IdempotencyKey`. Boards commands deliberately mirror Runtime API behavior without a separate receipt, except Board admission requires a caller-owned UUID `OperationKey`. Preserve it for an exact retry after an unknown outcome; exact replay returns the original item and changed-payload reuse conflicts. Query the current Board item before starting another logical admission. Returned `RowVersion` values are concurrency tokens for later mutations.
For Work Schedule, read the [Work Schedule C# guide](/docs/developer/work-schedule/) and the generated [Work Schedule XML reference](/docs/api/dotnet/Moltaro.Package.NET.WorkSchedule.xml). Use a new stable `OperationId` for each logical command and preserve it only for an exact retry. Calendar writes use the current `RowVersion`; assignment and exception changes use the last returned `ChainVersion`. Requests never accept an actor or authority tier. Do not model Operational Location as an exception scope: the supported scopes are Workspace, Site, and Worker.
Inside one explicit function transaction, invoke a Boards command before staging or saving Entity or owned-table mutations. An Entity-first sequence is rejected with `moltaroBoards.runtime.entityMutationBeforeBoardCommandUnsupported` before the Boards facade opens its separate application scope. Start a new transaction instead of retrying the same ordering.
Boards may have enabled event and status-invariant Constraints. Treat every ordered runtime `Errors[]` entry as authoritative: branch on `Code`, retain `Target` and safe `Metadata` in diagnostics, and show the workspace-authored `Message` to the operator without attempting value interpolation. `ValidateMoveAsync` returns `CapturedAtUtc` and ordered `ConstraintDecisions`, but it is only a dry run; execute with the current `RowVersion` and handle a newly rejected result. Never retry a rejected Constraint as though it were a transient transport failure.
Normal generated-context `MoltaroDbContext.SaveChanges[Async]` also runs every registered Entity pre-commit provider. A rejection throws `MoltaroPreCommitSaveRejectedException`; catch it only when the function must shape a controlled domain result, and branch on its ordered `Errors[].Code` values rather than its message. If function code does not catch it, Moltaro records the run as `ValidationFailure`, copies the safe provider errors into the run’s validation errors, and sets `FailureReasonCode` to the stable `moltaro.preCommit.rejected`. This is a business rejection, not a transient failure. The exact public namespace is `Moltaro.Package.NET.ModuleRuntime.Runtime`; generated code that catches the exception must include:
```csharp
using Moltaro.Package.NET.ModuleRuntime.Runtime;
```
The run-level failure code is deliberately general. One rejected save may contain several provider failures, so code that needs the specific reasons must inspect every ordered `Errors[].Code` value instead of expecting one exception-level reason code. Inside one explicit transaction, stage every Entity identity in a cross-record unit of work before the first `SaveChanges`; introducing a new identity in a later save is rejected before lock acquisition. This rule is global to generated-context Entity writes, not conditional on Board usage. Assign all new string ids up front, connect the complete related graph in memory, and issue one `SaveChangesAsync`. Do not generate sequential-save code merely to obtain a parent’s id. Use the [atomic related-graph recipe](/docs/developer/business-logic/csharp-business-logic/recipes/#create-a-related-entity-graph-in-one-atomic-save) when generating an import or cross-record command.
## 5. Diagnose the unsaved buffer
[Section titled “5. Diagnose the unsaved buffer”](#5-diagnose-the-unsaved-buffer)
```http
POST /api/workspace/admin/net-operation-project/source-language/diagnostics
Content-Type: application/json
{
"RevisionId": "working-revision-id",
"Path": "src/Functions/CloseTicket.cs",
"Content": "// complete current C# buffer",
"SchemaContractHash": "hash returned by GET .../status"
}
```
Diagnostics compile the supplied buffer together with the other files in the revision. They do not save, build, or activate anything. Each diagnostic has a stable `Code`, `Severity`, `Origin` (`Source` or `GeneratedContract`), and a source `Path` and `Range` when Roslyn provides a location. Treat any generated contract error as a platform/schema problem; do not rewrite unrelated user source to hide it.
On success, read `Limits` rather than assuming source-size, timeout, or concurrency values. HTTP `503` with `netOperationProject.language.analysis.timeout` is terminal for that request: read status and retry from the current revision after the returned bound. If the client cancels, Moltaro safely finishes cleanup in the background without holding the project write lock.
## 6. Create an immutable revision
[Section titled “6. Create an immutable revision”](#6-create-an-immutable-revision)
Read `GET .../status`, then submit `BaseRevisionId`, `BaseSourceChecksum`, and file changes to `POST .../source-revisions/manual-edit`. For an existing file, also send its `ExpectedContentHash`. A stale revision or checksum returns HTTP 409; a stale file hash does as well. Re-read the source instead of overwriting another change. Successful manual edits return the new revision id, checksum, and effective `Limits`. HTTP `503` with `netOperationProject.source.manualEdit.timeout` creates no revision; read status and retry from fresh revision, checksum, and file-hash values.
Read `GET .../source-language/source-index?revisionId=...` after saving to verify the functions, data sources, services, bindings, keys, and diagnostics Moltaro discovered.
## 7. Check, build, and invoke
[Section titled “7. Check, build, and invoke”](#7-check-build-and-invoke)
Net Operation Project numeric values are:
| Kind | Value | Effect |
| ----- | ----: | ---------------------------------------------------------- |
| Build | `0` | Compile and automatically activate the successful artifact |
| Check | `1` | Compile only; never activate an artifact |
### Post-schema-change artifact checkpoint
[Section titled “Post-schema-change artifact checkpoint”](#post-schema-change-artifact-checkpoint)
After any schema-affecting change, and before handing off C# business logic, read:
```text
GET /api/workspace/admin/net-operation-project/status
```
Inspect `CurrentSchemaContractHash`, `IsActiveArtifactSchemaStale`, and `NeedsValidation`, together with `ActiveArtifact.SchemaContractHash`. These values are independent. A stale active artifact was compiled against an older contract but may still be running. `NeedsValidation = false` does not prove that the active artifact is current. A successful Check may clear `NeedsValidation`, but it compiles only and cannot clear the stale-artifact warning. A successful Build activates the replacement; a failed Build preserves the previous active artifact.
When `IsActiveArtifactSchemaStale` is true, use the exact intended revision from the fresh status, normally `WorkingRevision.Id`:
1. Queue Check (`Kind: 1`), retain its build id, and poll that exact build to terminal `Succeeded`, `Failed`, or `Cancelled`. Inspect the terminal result and diagnostics. Stop without queueing Build unless Check succeeded.
2. Queue Build (`Kind: 0`) for the same revision, retain its build id, and poll that exact build to a terminal result.
3. Read status again and require `IsActiveArtifactSchemaStale = false` plus a non-empty `ActiveArtifact.SchemaContractHash` equal to `CurrentSchemaContractHash` before handoff.
If a matching operation is already queued or running, poll it instead of enqueueing a duplicate. Never select another revision merely because a stale warning exists. If the schema changes again during the sequence, re-read status and restart the decision from the new current contract. The API-first workflow does not download or edit `GeneratedWorkspaceContract`.
Queue `POST .../builds` with the saved `RevisionId` and `Kind: 1`, then poll `GET .../builds/{buildId}`. When Check succeeds, queue `Kind: 0`. A failed Build leaves the previous active artifact running.
Verify the result in `GET /api/workspace/admin/function-catalog`, then invoke a published command with `POST /api/workspace/commands/{functionKey}` or enqueue a published job with `POST /api/workspace/functions/{functionKey}/enqueue`.
Before invoking it, determine the execution model from the installed API: `CurrentPublishedContract` in the Function Catalog, `Source` on entity bindings, and `Kind` in `GET /api/workspace/admin/api-functions`. For a CRON schedule, join its `BusinessFunctionId` to the catalog; a valid target is a global `Job` function, and each due occurrence is queued for a worker. C# `async`/`Task` and HTTP `200` do not distinguish blocking from queued work. See [Asynchronous operations and polling](/docs/developer/async-operations/) for the complete decision table and schedule-observation flow.
Do not queue another build merely because one remains queued or running. Poll the returned build id until a terminal status and inspect stored diagnostics on failure. Preserve stable keys across source revisions and reuse a durable idempotency key when retrying the same supported business operation.
Continue with the [verified C# recipes](/docs/developer/business-logic/csharp-business-logic/recipes/) for entity events, Boards, Entitlement Operations, and Currency Rates. Read record history through the [audit API](/docs/developer/audit-trail/). Use [Reliable API automation](/docs/developer/reliable-api-automation/) for the complete retry, concurrency, polling, recovery, source-limit, and safety rules.
## Complete a user-facing entity UI
[Section titled “Complete a user-facing entity UI”](#complete-a-user-facing-entity-ui)
Creating an entity schema is not the completion boundary for a user-facing record type. Configure localized presentation, table/card/form leaf surfaces, Entity List, Details and Drawer hosts, valid selectors, reachable `Preview`/`OpenDetails` navigation, menu placement, and least-privilege permissions. Read every configuration back and verify it as both an administrator and an ordinary reader when test identities are available.
`Default` selectors may validly use `SurfaceKey = null` and resolve the configured default. `CustomKey` always requires an existing compatible key. Read the [Runtime screens](/docs/user/data-structure/entity-definitions/runtime-screens/) and [UI surface library](/docs/user/data-structure/entity-definitions/ui-surface-library/) guides for the full product model, then use the installation-local Configuration OpenAPI for exact requests. The generated agent guide routes to these sources and intentionally does not duplicate this feature-specific workflow. The Workspace UI Project is for custom pages and is not a replacement for completing built-in entity surfaces.
### Design table filters and sorting
[Section titled “Design table filters and sorting”](#design-table-filters-and-sorting)
Before configuring a table, read its `/ui/table-surfaces/configuration/options` response and choose `FilterItems`, `SortItems`, and `DefaultSort` from the returned typed targets. Do not guess field paths or infer query behavior from the column renderer. Visible business dimensions that users need to segment or order by—especially statuses and other references—should normally support both operations unless an omission has a recorded rationale.
Reference targets are not interchangeable. A root `Status` target is distinct from the value displayed to users; `Status/Name` sorts by its label, while `Status/SortOrder` is meaningful only when the referenced records actually contain a business order. Use technical paths from the options response and localize only the workspace-authored labels.
Related targets are identified by their complete path. Copy the entire configured `Target.Path` into runtime conditions; never replace it with `ReferencePath`, which controls lookup breadcrumb rendering only. See [Table filters and sorting](/docs/configuration/table-filters-and-sorting/) and [Entity Instance Query](/docs/developer/entity-instance-query/).
Read the saved configuration back and verify labels, visibility, `VisibleByDefault`, item order, typed paths, and `DefaultSort`. In the Web Application, apply a representative filter and sort and verify totals, every visible row value, and ordering—not merely that the choices appear in a drawer. Then clear the test filter and restore the intended default sort. When possible, repeat the query as an ordinary reader and confirm that inaccessible fields, records, and result counts remain protected by server-side permissions.
## 8. Use managed secrets safely
[Section titled “8. Use managed secrets safely”](#8-use-managed-secrets-safely)
When trusted C# needs an outbound credential, first inspect `developer-surface` for `Moltaro.Package.NET.Functions.ISecretService`. Inject that service into the function constructor and use its synchronous `GetRequired(key)` or `TryGet(key, out value)` methods. Do not invent an async secret API, pass the value in function arguments, or place it in source.
The owner or administrator supplies exact key names and enters values directly through **Administration > Secrets** or an explicitly authorized write-only API flow. The agent does not need the values: it creates the source revision, runs Check and Build, invokes the intended function flow, reports only a non-secret business marker, and asks the owner to confirm Audit Trail evidence. Provisioning, rotation, external-system changes, rollout, and schedule enablement remain separate owner/operator actions unless the task explicitly authorizes them.
Only an owner or administrator can manage keys through `/api/workspace/admin/secrets`; Configurator access is not sufficient. An agent must not list, create, rotate, disable, enable, or retire keys unless the user explicitly authorizes that administration work. There is no reveal endpoint.
Trusted C# can resolve every active key it knows. Never log, persist, return, or copy resolved values or derived bearer tokens. Required unavailable values raise stable `moltaro.secrets.missing`, `moltaro.secrets.disabled`, or `moltaro.secrets.retired` codes. One invocation uses one immutable snapshot; after provisioning or rotation, retry with a new invocation rather than looping inside the old one. See the complete [Managed secrets guide](/docs/developer/business-logic/managed-secrets/).
## 9. Create a custom Workspace UI page
[Section titled “9. Create a custom Workspace UI page”](#9-create-a-custom-workspace-ui-page)
Workspace UI is also API-first:
All routes in this section still use the same `WORKSPACE_API_BASE_URL`; they are not PublicSite or portal endpoints.
1. Inspect `GET /api/workspace/admin/ui-project/source-templates` or the page generators, then apply one to create a revision.
2. Make subsequent changes with `POST .../source-revisions/manual-edit`.
3. Queue Check with `POST .../builds` and `Kind: 0`; poll the build.
4. Queue Build with `Kind: 1`; it creates an inactive immutable artifact.
5. Activate it with `POST .../artifacts/{artifactId}/activate`.
The Workspace UI values deliberately differ from the C# project: `Check = 0`, `Build = 1`, and Build does not activate automatically. See the complete [Workspace UI Project lifecycle](/docs/developer/workspace-ui-project/build-publish-and-upgrade/).
## Optional local IDE workflow
[Section titled “Optional local IDE workflow”](#optional-local-ide-workflow)
`GET .../net-operation-project/download` produces a ready-to-build ZIP for a human working in a local IDE. It includes the generated workspace contract and matching SDK files. It is transport convenience, not a requirement for an AI agent and not an extra source of truth.
# Reliable API automation
> Post-bootstrap operating rules for human developers and AI agents: discovery, concurrency, retries, polling, recovery, limits, and audit.
This page defines the safe operating contract for a developer or AI agent that changes a Moltaro workspace through its APIs. It starts **after** the workspace owner has prepared access in the Moltaro Portal or administrative Web Application interface.
Workspace setup is an administrator task
Installing or registering the Web Application, creating or rotating credentials, and preparing Agent integration are not part of the agent workflow. If the handoff below is incomplete, the agent must stop and ask the user to complete Agent integration in the Portal or administrative interface. It must not try to provision its own access through workspace APIs.
## Required handoff
[Section titled “Required handoff”](#required-handoff)
The workspace owner gives the developer or agent:
1. the downloaded workspace-specific `AGENTS.md` or `CLAUDE.md`;
2. the exact `WORKSPACE_API_BASE_URL`;
3. an API key through an appropriate secret channel;
4. a task and an account with the narrowest roles needed for that task.
The guide must not contain the key. A missing URL, guide, key, or permission is an administrative prerequisite, not an API-discovery problem.
This prerequisite boundary does **not** remove the configured Application’s administrative APIs from the agent workflow. Once authenticated with the provided roles, the agent uses documented `/api/workspace/admin/...` Configuration API routes for entity definitions, Net Operation Project, Boards and Entitlement configuration, Agent integration, and Workspace UI. Those are workspace Application endpoints. Portal administration endpoints are a different surface and are not part of the developer contract.
Keep the public and workspace origins visibly separate in scripts and prompts:
```text
PUBLIC_DOCS_BASE=https://moltaro.com
WORKSPACE_API_BASE_URL=https://customer-workspace-api.example.com
```
`PUBLIC_DOCS_BASE` is for documentation only. Every `/api/workspace/...` and installation-local `/openapi/...` path resolves against `WORKSPACE_API_BASE_URL`. The Web Application used by people may have another origin; never derive the API hostname from it.
## Preflight every workspace
[Section titled “Preflight every workspace”](#preflight-every-workspace)
Before changing anything:
```text
GET ${WORKSPACE_API_BASE_URL}/api/workspace/context
GET ${WORKSPACE_API_BASE_URL}/api/workspace/auth/me
GET ${WORKSPACE_API_BASE_URL}/openapi/moltaro-public-v1.json
GET ${WORKSPACE_API_BASE_URL}/openapi/moltaro-config-v1.json
GET ${WORKSPACE_API_BASE_URL}/api/workspace/admin/net-operation-project/developer-surface
```
After this handoff, the authenticated Application endpoints below may also be used to refresh the installation manifest or generated guide:
```text
GET ${WORKSPACE_API_BASE_URL}/api/workspace/admin/agent-integration
GET ${WORKSPACE_API_BASE_URL}/api/workspace/admin/agent-integration/agents-md
```
They do not discover an unknown workspace; they require the API base URL and authorized key that the user already supplied.
Read the workspace locale and time zone, the caller’s roles and module states, the installed API contract, and the supported C# surface. Treat the installation-local OpenAPI documents and `developer-surface` response as authoritative for that workspace. Do not infer an endpoint, enum value, injectable service, or module availability from a different release or from a public CLR type.
## Safe operating loop
[Section titled “Safe operating loop”](#safe-operating-loop)
Use the same sequence for schema, records, C# logic, Boards, Entitlement Operations, and custom pages:
1. **Discover** the installed contract, entity schema, module state, current revision, and permissions.
2. **Read before writing.** Capture ids, stable keys, row versions, source checksums, and file hashes from current responses.
3. **Preview where supported.** Use schema-change plans, source-template preview, page-generator preview, validation, and language diagnostics before creating a revision or changing data.
4. **Make one bounded change.** Keep a stable external correlation or idempotency key when the operation supports one.
5. **Verify the saved state.** Read the entity, revision, source index, board item, entitlement history, or generated artifact back from the API.
6. **Check before publishing.** Net Operation Project Build activates on success; Workspace UI Build does not. Use their exact lifecycles below.
7. **Verify the runtime result and history.** Check the Function Catalog or active artifact, invoke only when requested, and read the relevant audit or ledger surface.
Do not delete schema, purge artifacts, deactivate pages, rotate credentials, or activate code merely because an endpoint exists. Those actions require an explicit task. For a destructive schema change, inspect the server-provided change plan and surface its data-loss warnings before proceeding.
## Concurrency and HTTP 409
[Section titled “Concurrency and HTTP 409”](#concurrency-and-http-409)
Moltaro rejects stale writes instead of silently overwriting newer work:
* record updates use the last observed `RowVersion`;
* Net Operation Project manual edits use `BaseRevisionId`, `BaseSourceChecksum`, and `ExpectedContentHash` for every changed existing file;
* Workspace UI source edits use the same base revision/checksum pattern and file hashes where the request schema requires them;
* artifact activation, deactivation, rollback, and purge use expected project and artifact row versions.
On HTTP `409`, do not resend the old body. Re-read the object or source tree, compare the intervening change with the intended change, merge when safe, and submit a new request using the fresh concurrency values. If the correct merge is ambiguous, stop and ask the user.
## Retries, idempotency, and stable keys
[Section titled “Retries, idempotency, and stable keys”](#retries-idempotency-and-stable-keys)
* Retry reads after a transient transport failure.
* Do not blindly retry a mutation after an unknown outcome. First read the target state or use the operation’s documented idempotency key.
* Boards automation commands mirror Runtime API semantics and do not have a separate durable receipt. After an unknown add outcome, query the open item; after an unknown mutation outcome, read the item and compare its current state and `RowVersion` before deciding whether another command is needed.
* Reuse the same durable idempotency key when retrying the same logical Entitlement grant, renewal, lifecycle, consume, reverse, adjustment, or renewal-operation command. Do not generate a new key for each attempt. The Entitlement facade reports a native durable replay as `Replayed = true`.
* A source template’s `StableKey` identifies the published function or data source across revisions. Read the template catalog to learn whether it is required; do not derive it from a display name.
* A command `CorrelationId` connects one external operation with its run and downstream history. It is observability context, not a substitute for an operation-specific idempotency key.
* Validation, permission, not-found, and concurrency responses require a corrected request or fresh state, not automatic retry.
## Polling checks, builds, and jobs
[Section titled “Polling checks, builds, and jobs”](#polling-checks-builds-and-jobs)
Queue a build once, retain its returned id, and poll that exact resource with a bounded delay and overall client deadline. Do not queue duplicate builds because a build remains `Queued` or `Running` longer than expected.
First establish that the operation is actually queued. A C# `Task`, an `Async` method name, or HTTP `200` is not an execution-mode signal: Moltaro enqueue endpoints return a normal success envelope containing a queued resource. Use Function Catalog contract, binding source, API publication kind, schedule metadata, and the response schema. The complete discovery table, CRON flow, job/run distinction, and polling algorithm are in [Asynchronous operations and polling](/docs/developer/async-operations/).
Both project build status enums use `Queued = 0`, `Running = 1`, `Succeeded = 2`, `Failed = 3`, and `Cancelled = 4`. `2`, `3`, and `4` are terminal. On failure, read the stored diagnostics rather than immediately submitting an identical build.
The build kinds and activation behavior are different:
| Project | Check | Build | Activation |
| --------------------- | ----: | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Net Operation Project | `1` | `0` | A successful Build automatically replaces the active C# artifact. A failed Build leaves the previous artifact active. |
| Workspace UI Project | `0` | `1` | Build creates an inactive artifact. Activation is a separate, row-version-protected operation; rollback can restore a compatible prior artifact. |
Function jobs use `Queued = 0`, `Leased = 1`, `Completed = 2`, `Failed = 3`, and `Cancelled = 4`. Poll the job id returned by enqueue; do not enqueue the same business operation again simply because it is still queued or leased.
For recurring work, create one Function Schedule through the Configuration API instead of running a client-side timer that repeatedly calls a manual enqueue endpoint. The schedule processor leases each due occurrence and the function queue suppresses a new scheduled job while the target function has queued or running work. That due occurrence remains visible as a diagnostic `Skipped` run. Do not add another singleton or deduplication layer and do not submit a compensating manual job for `Skipped`; observe the active job and let the next CRON occurrence proceed normally.
## Generated entity contract after schema changes
[Section titled “Generated entity contract after schema changes”](#generated-entity-contract-after-schema-changes)
Moltaro regenerates `GeneratedWorkspaceContract` on the server. Each C# language request and Check/Build receives the current generated entity source; there is no contract download or refresh endpoint in the API-first workflow.
After creating, changing, or deleting an entity field:
1. read the entity definition again and use its current keys and ids;
2. call completions, hover, signature help, or diagnostics again for the current source buffer;
3. fix any renamed or removed generated members;
4. if the source changed, create a new immutable source revision; run Check against the exact revision before Build.
Do not continue from completion results captured before the schema change.
## Net Operation Project source limits
[Section titled “Net Operation Project source limits”](#net-operation-project-source-limits)
The current source contract accepts at most 500 files, 512 KiB of UTF-8 text per file, 5 MiB of UTF-8 source in total, and 500 changes in one manual-edit request. Paths are relative, use `/`, and are at most 512 characters. Supported stored paths are:
* C# files below `src/`;
* Markdown at the project root or below `docs/`;
* root `package-references.json`, with at most 50 package references.
Generated, build, dependency, and repository directories such as `GeneratedWorkspaceContract`, `bin`, `obj`, `lib`, `node_modules`, `.git`, and `.moltaro` cannot be edited through the source API. Language requests use the same 512 KiB active-buffer and 5 MiB snapshot limits.
Successful diagnostics and manual-edit responses include `Limits`, which is the machine-readable contract for source size, manual-edit count, diagnostics timeout, manual-edit timeout, and diagnostics concurrency. A size rejection includes safe `MaxBytes` and `ActualBytes` metadata without source text. File-count and manual-edit-count rejections likewise include exact maximum and actual counts. Diagnostics that exceed the server deadline return HTTP `503` with `netOperationProject.language.analysis.timeout`. A manual edit that exceeds its deadline or cannot acquire the project write lock in time returns HTTP `503` with `netOperationProject.source.manualEdit.timeout`; it creates no revision. After either response, read current status and retry with current concurrency values. Client cancellation may leave analysis completing safely in the background, but it does not retain the project write lock or block status reads. An `ExpectedContentHash` mismatch is a distinct HTTP `409` response.
## Data and error conventions
[Section titled “Data and error conventions”](#data-and-error-conventions)
* JSON properties are PascalCase and public enums are numeric. Read `x-enum-varnames` from the installed OpenAPI document instead of guessing a number.
* Check the response envelope’s `Success` before using `Data`. Branch on the stable error `Code` and optional `Field`, never on localized `Message`.
* Read locale and time zone from `/api/workspace/context`. Send dates, times, decimals, and currency values in the exact JSON shape declared by OpenAPI; never apply locale-specific display formatting to an API value.
* A `403` can mean the account lacks a role or resource-context permission. The agent asks the user or workspace administrator to review access; it does not attempt to grant itself permissions.
## Recovery guide
[Section titled “Recovery guide”](#recovery-guide)
| Symptom | Safe response |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` | Confirm the request used `WORKSPACE_API_BASE_URL` and the provided bearer key. If the key is missing, expired, or revoked, ask the user to update it in the Portal or administrative interface. |
| `403` | Read the operation’s permission requirements and ask the administrator for the narrowest missing permission. Do not switch to a personal login or change roles yourself. |
| `404` | First verify the API origin. Then re-discover the entity, function, board, module resource, or artifact; ids and visibility are workspace-specific. |
| `409` | Re-read, compare, merge, and retry with fresh row versions, revision ids, checksums, and hashes. Never overwrite blindly. |
| Net Operation Project source processing returns `503` | For `netOperationProject.language.analysis.timeout` or `netOperationProject.source.manualEdit.timeout`, read current status, respect the returned retry metadata and `Limits`, then retry from fresh revision, checksum, and file-hash values. A timed-out manual edit creates no revision. |
| Diagnostics pass but Check fails | Diagnostics replace one unsaved buffer inside a revision snapshot. Save every intended file, then inspect Check diagnostics for the exact immutable revision and installed dependencies. |
| Check passes but Build fails | Read the stored Build stage and nullable structured `Failure`. Branch on `ReasonCode`, not the localized summary. The previous Net Operation Project artifact remains active. For `storage_capacity_exhausted`, ask the operator to free or expand storage before queueing a new Build. For `publication_commit_unconfirmed`, re-read the Build and active artifact before retrying; do not repair catalog rows manually. Workspace UI never activates a failed build. |
| Workspace UI Build succeeds but users see no change | Find the artifact by `BuildId` and `RevisionId`, activate it with current row versions, then perform the required full WebApp refresh. |
| Module is disabled or an injectable service is absent | Read workspace module state and `developer-surface`. Ask the administrator to enable/configure the supported module, or remove the dependency. Do not inject an unlisted internal service. |
| `moltaro.automation.executionPhase.unsupported` | Move the module operation out of Validation/BeforeSaveMutation and into an Action, TriggerHandler, Command, Job, or HttpEndpoint. Prefer an after-commit trigger when it depends on a saved record. |
| Polling reaches the client deadline | Keep the build or job id, report its latest state, and let the user decide whether to continue polling or request cancellation. Do not enqueue a duplicate. |
## Audit responsibility
[Section titled “Audit responsibility”](#audit-responsibility)
| Change path | History behavior |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Runtime record create/update/archive through Moltaro APIs and product surfaces | The platform writes field, table, assignment, and system-field history when audit is enabled for the entity definition. |
| Net Operation Project direct-DB save | Automatic field-level audit is not added. Emit a display-safe `AddBusinessEventAsync` event for each meaningful custom state change. |
| Boards application-automation operation | Stored actor is `moltaro-system-automation`; join Function operations to board history/resource events by correlation and origin metadata. |
| Entitlement application-automation operation | Stored actor is `moltaro-system-automation`; treat the append-only ledger and entitlement history as truth and join them to the function run by correlation/origin metadata. |
| Net Operation Project or Workspace UI publication | Project build and publication governance is separate from one record’s change feed. Verify the active artifact through the project APIs. |
See [Record history and audit](/docs/developer/audit-trail/) for the record change feed and business-event recipe, and [Errors and responses](/docs/developer/errors/) for the common response shape.
# Asynchronous operations and polling
> Determine whether Moltaro work is request-blocking or queued, then track schedules, function jobs, actions, triggers, and project builds to a terminal state.
Moltaro has both request-blocking operations and durable background work. A human developer or coding agent must identify the execution model before invoking an operation: a successful enqueue response means that work was accepted, not that its business effect has completed.
Do not infer execution from C# or HTTP syntax
An `async` C# method or a `Task` return type can still execute while the HTTP caller waits. Conversely, Moltaro enqueue endpoints currently return an HTTP `200` envelope containing a queued resource. Neither the word `async` nor the HTTP status alone determines the lifecycle. Use the installed OpenAPI schema, Function Catalog metadata, binding or publication kind, and the returned `Status` plus polling URL.
Every route below resolves against the configured `WORKSPACE_API_BASE_URL`, not `moltaro.com`. The function catalog, schedules, and operations routes are supported administrative APIs of the configured Moltaro Application. They are not Portal administration APIs.
## Execution-model decision table
[Section titled “Execution-model decision table”](#execution-model-decision-table)
| Surface | Machine-readable signal | Execution model | Completion evidence |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Entity validation or before-save mutation | Function Catalog `CurrentPublishedContract` is `Validation` or `BeforeSaveMutation`; binding `Source` is `EntitySave` | Request-blocking inside the record save | The original record API response |
| Command function | API publication `Kind` is `Command`; catalog contract is `Command` | Request-blocking | Command response contains the terminal run result |
| HTTP endpoint function | Catalog contract is `HttpEndpoint` | Request-blocking for that inbound HTTP request | The endpoint’s HTTP response; any job it explicitly enqueues is separate work |
| Global job through API | API publication `Kind` is `Enqueue`; catalog contract is `Job` | Queued worker execution | Poll the returned `JobId` |
| Function schedule | Schedule targets a published global `Job` function | Each due CRON occurrence is enqueued for a worker unless the target function already has queued or running work; an overlap is recorded as `Skipped` | Query jobs and runs by `ScheduleId` |
| Entity trigger | Binding `Source` is `EntityTrigger`; catalog contract is `TriggerHandler` when the function is trigger-only | The record commits first, then a worker handles the trigger | Query jobs and runs by `TriggerBindingId` or correlation id |
| Entity or global UI action | Catalog contract is `Action`; runtime route ends in `/enqueue` | Validation is request-blocking; execution is queued | Observe the active action job and Function operations |
| Boards or Entitlement C# application automation call | `developer-surface` entry has `ServiceKind = ApplicationAutomation`; enclosing function contract must be Action, TriggerHandler, Command, Job, or HttpEndpoint | The facade call waits for its own separate main application transaction | Successful `MoltaroRuntimeResult`; poll only ids actually returned in `FollowUpOperationIds` |
| Net Operation Project Check or Build | `POST .../net-operation-project/builds` returns a build record with `Id` and `Status` | Queued build worker | Poll `GET .../builds/{buildId}` |
| Workspace UI Check or Build | `POST .../ui-project/builds` returns a build record with `Id` and `Status` | Queued build worker | Poll `GET .../builds/{buildId}` |
Read enum names from `x-enum-varnames` in the installation-local OpenAPI document instead of guessing numeric values. For the current contract, `BusinessFunctionContract` includes `Validation = 0`, `BeforeSaveMutation = 1`, `Action = 2`, `Job = 3`, `TriggerHandler = 5`, `HttpEndpoint = 6`, and `Command = 7`. `BusinessFunctionApiPublicationKind` is `Command = 0`, `Enqueue = 1`; `BusinessFunctionBindingSource` is `EntitySave = 0`, `EntityTrigger = 1`.
## Discover the function before invoking it
[Section titled “Discover the function before invoking it”](#discover-the-function-before-invoking-it)
For an authoring or administrative client, use the Configuration API rather than class names or UI labels as the source of truth:
```text
GET /api/workspace/admin/function-catalog
GET /api/workspace/admin/function-catalog/{functionId}
GET /api/workspace/admin/function-catalog/binding-summaries
GET /api/workspace/admin/api-functions
GET /api/workspace/admin/function-schedules
```
Join these responses by `BusinessFunctionId`:
1. `CurrentPublishedContract` says which runtime contract is active now.
2. A binding `Source` distinguishes an in-save binding from an after-commit trigger.
3. An API publication `Kind` distinguishes synchronous command invocation from asynchronous enqueue publication. `IsEnabled` and `IsRetired` say whether the published key is callable.
4. A schedule identifies its target `BusinessFunctionId`, `Status`, `NextRunAt`, `LastStatus`, and target health. An enabled schedule enqueues only future due occurrences; creating or updating it does not run the function immediately.
Source-language inspection helps while authoring, but the catalog after a successful Build is the runtime truth. Preserve the stable function key across revisions.
## CRON schedule: exact asynchronous flow
[Section titled “CRON schedule: exact asynchronous flow”](#cron-schedule-exact-asynchronous-flow)
A Function Schedule contains configuration, not executable code. Its target must be a published global function with the `Job` contract. At a due time:
```text
enabled schedule becomes due
-> scheduler enqueues one job with Source = Schedule and ScheduleId
-> worker leases that job
-> worker creates and durably completes a function run
-> job reaches Completed only after terminal run success is confirmed
-> scheduler keeps the next planned occurrence
```
The record or API call that originally caused the schedule to be created is not held open. The scheduled run uses the system automation actor, and there may be no original user. Active-work deduplication skips a new occurrence while the previous job for the same schedule is still `Queued` or `Leased`.
To verify one schedule, read it and then filter the operations APIs by its id:
```text
GET /api/workspace/admin/function-schedules/{scheduleId}
GET /api/workspace/admin/function-operations/jobs?Source=Schedule&ScheduleId={scheduleId}&Limit=50
GET /api/workspace/admin/function-operations/runs?Source=Schedule&ScheduleId={scheduleId}&Limit=50
```
`NextRunAt` shows planning, a job row proves enqueue, and a terminal run proves execution outcome. `LastStatus` on the schedule is a summary, not a substitute for the job or run detail when diagnosing failure. Moltaro does not publish `Completed` for an occurrence while its linked run is `Running` or otherwise unconfirmed. After a restart between the two metadata writes, recovery uses the persisted run outcome without re-executing the business function: it completes an already successful run, preserves an accepted cancellation as `Cancelled`, or restores an ordinary terminal failure to `Queued` with its configured retry delay until the attempt limit is reached. Incomplete, ambiguous, protected, and exhausted terminal outcomes fail closed instead of being redelivered.
## Function job lifecycle
[Section titled “Function job lifecycle”](#function-job-lifecycle)
An API-enqueued function returns a status model containing `JobId`. Retain that id and poll:
```text
POST /api/workspace/functions/{functionKey}/enqueue
GET /api/workspace/functions/jobs/{jobId}
POST /api/workspace/functions/jobs/{jobId}/cancel
```
When the publication declares `MoltaroApiEnqueue.TimeoutSeconds`, the enqueue response and later status reads expose that captured value. It belongs to the job, not to one attempt: retries keep it even if a later Build changes the publication or an administrator changes the runtime Job default. A null value means the runtime default remains in effect.
Job status values are:
| Status | Value | Meaning | Terminal |
| ----------- | ----: | --------------------------------------- | -------- |
| `Queued` | `0` | Waiting for a worker or for `NextRunAt` | No |
| `Leased` | `1` | Claimed by a worker | No |
| `Completed` | `2` | Worker execution completed successfully | Yes |
| `Failed` | `3` | Execution ended in error | Yes |
| `Cancelled` | `4` | Work was cancelled | Yes |
`Completed` is published only after the linked run is durably `Success` or `Skipped` with `CompletedAt`, `DurationMs`, and a non-empty `ResultSummary`. On `Completed`, inspect `Run.Status`, `Run.ResultData`, and `Run.ResultSummary`; on `Failed`, inspect `LastError` and `Run.ErrorSummary`. A successful status read containing `Failed` is not a successful business operation. Call the public cancel endpoint only while its response says `CanCancel: true`; for API-enqueued jobs this is the queued state.
Administrators use Function operations to stop an entire retry chain:
```text
POST /api/workspace/admin/function-operations/jobs/{jobId}/stop-retries
{ "ExpectedModifiedAt": "" }
```
The job projection exposes `CurrentAttempt`, `EffectiveMaxAttempts`, `RemainingAttempts`, `RetryDelaySeconds`, `NextRunAt`, `WillRetry`, `CanStopRetries`, `RetryStopState`, and stable reason codes. A queued or expired leased job is finalized immediately. A live leased attempt is cancelled cooperatively and cannot create a later attempt; code that ignores cancellation may still finish or time out. A fully durable success for that exact current attempt wins and produces `Completed`. Repeating an already accepted command is idempotent; a stale version for a different active state returns a conflict. `CancellationReasonCode: "job.retry.stopRequested"` is the stable canonical reason for an accepted administrative retry-chain stop. It is persisted by the stop command; projections also normalize upgraded active jobs whose legacy cancellation intent did not originally store a code. Disabling or deleting a schedule prevents new occurrences but does not silently cancel an already-created job. Schedule responses keep its active job and retry-control state visible. Use the explicit `Stop retries` action when that job must receive cooperative cancellation and be prevented from starting a later attempt; it remains visible in Function operations afterward.
Use `DeduplicationKey` for repeat submissions of the same active operation. It suppresses duplicates only while an existing job with that key is `Queued` or `Leased`; it is not permanent business idempotency. Keep the returned job id across transport failures and client restarts instead of enqueueing a replacement because polling is slow.
## Polling algorithm
[Section titled “Polling algorithm”](#polling-algorithm)
Use the same bounded pattern for function jobs and project builds:
1. Submit once and persist the returned job or build id.
2. Poll only that resource, using a modest delay and an overall client deadline. Honor server retry guidance if a response provides it.
3. Continue while the status is non-terminal. Function jobs use `Queued` and `Leased`; project builds use `Queued` and `Running`.
4. Treat `Completed` or `Succeeded` as terminal success and inspect the result or artifact. Treat `Failed` and `Cancelled` as terminal non-success and read stored diagnostics.
5. If the client deadline expires, report the id and latest server state. Do not submit a duplicate automatically.
Both project build status enums currently use `Queued = 0`, `Running = 1`, `Succeeded = 2`, `Failed = 3`, and `Cancelled = 4`. Build-kind values are different: Net Operation Project uses `Build = 0`, `Check = 1`, while Workspace UI uses `Check = 0`, `Build = 1`.
Net Operation Project Build responses have an additive nullable `Failure` object. When present, branch on its stable `ReasonCode`, not on the localized summary. It also carries category, retryability, a validated PostgreSQL `SqlState` when available, a safe logical target, and a stable remediation code. The Build id is the correlation id used by Studio and Function Operations. `storage_capacity_exhausted` (SQLSTATE `53100` when raised by PostgreSQL) means an operator must free or expand storage before submitting a new Build. `publication_commit_unconfirmed` means the server could not prove the exact Build/artifact/project commit and did not replace the previous active artifact. An exact replay of a confirmed published Build is write-free.
For a module automation facade, the returned `Task` covers the facade’s main transaction; it does not imply that durable outbox delivery has finished. `FollowUpOperationIds` contains only follow-up work that was actually queued. Poll a returned function job id at `GET /api/workspace/functions/jobs/{jobId}`. If the list is empty, do not invent a job or enqueue the operation again; verify the synchronous result and the related audit/resource activity instead.
## Run history is different from queue state
[Section titled “Run history is different from queue state”](#run-history-is-different-from-queue-state)
A **job** describes delivery and worker state. A **run** describes one actual function execution and its business outcome. A retry can update the job’s attempt count and create another execution record, so agents must not collapse the two concepts.
Use the runtime job endpoint for a job created through public API enqueue. Use the Configuration API’s Function operations routes for administrative diagnostics across schedules, triggers, actions, test invocations, commands, and endpoint runs. Filter by `FunctionId`, `Source`, `ScheduleId`, `TriggerBindingId`, `ActionId`, entity ids, user ids, correlation id through search, time range, and status as declared by the installed OpenAPI document.
## Authoring checklist
[Section titled “Authoring checklist”](#authoring-checklist)
Before handing off asynchronous logic, verify all of the following:
* the active Function Catalog contract and binding/publication kind match the intended execution model;
* a scheduled target is a global `Job` function and the schedule is enabled, healthy, and has the expected `NextRunAt` in the effective time zone;
* the caller stores job/build ids, uses bounded polling, and distinguishes terminal failure from transport success;
* retries reuse the supported deduplication or business idempotency key;
* cancellation follows server-provided `CanCancel` and does not assume that stopping polling stops server work;
* operations and business audit/ledger history are checked after execution.
See [Function schedules](/docs/developer/business-logic/schedules/), [Commands and API functions](/docs/developer/business-logic/commands-and-api-functions/), [Operations and diagnostics](/docs/developer/business-logic/operations/), and [Reliable API automation](/docs/developer/reliable-api-automation/) for the surface-specific contracts.
# Integration quickstart
> The discovery flow developers and AI agents use to work with any Moltaro installation through the public API.
This page shows the shortest reliable path from “I have a Moltaro URL and a user” to reading and changing records through the public API. It is written for developers and for AI agents: every Moltaro workspace describes itself through the same endpoints, so one flow works against any installation — discover the schema first, then act on it.
All examples below were executed against a real Moltaro installation; response bodies are real, trimmed for length. The complete contract is in the [API reference](/docs/developer/api-reference/) and in the raw OpenAPI document ([`moltaro-public-v1.json`](/docs/api/openapi/moltaro-public-v1.json), also served by every installation at `/openapi/moltaro-public-v1.json`).
Use the workspace API host
The public page you are reading is on `moltaro.com`, but the requests below are not. Send every `/api/workspace/...` and installation-local `/openapi/...` request to the target `WORKSPACE_API_BASE_URL`. If that URL and an API key have not been handed to you yet, follow [Connect to a workspace API](/docs/developer/workspace-api-connection/) first.
## Conventions
[Section titled “Conventions”](#conventions)
* `WORKSPACE_API_BASE_URL` is the API host of the deployed installation, for example `https://ops.example.com`; all endpoints live under its `/api/workspace/` path. It is not `https://moltaro.com`.
* Authentication is a bearer token: `Authorization: Bearer `. Tokens come from the login endpoint or from an administrator-issued API key (see [Service accounts and API keys](#service-accounts-and-api-keys)).
* JSON responses use an envelope: `{ "Data": …, "Errors": [], "Warnings": [], "Success": true }`. Check `Success`, then read `Data`. The exceptions are file downloads (raw file content) and low-level protocol failures such as malformed JSON or an unknown route, which return a plain HTTP problem response instead of the envelope.
* JSON property names are PascalCase. Enums are numeric in JSON; the OpenAPI schema carries the member names as `x-enum-varnames` (and, where provided, member descriptions as `x-enum-descriptions`).
* User docs say *record type* and *record*; the API says **entity definition** and **entity instance**. See the [terminology bridge](/docs/user/data-structure/).
## Service accounts and API keys
[Section titled “Service accounts and API keys”](#service-accounts-and-api-keys)
Unattended integrations and AI agents should not run on a person’s login. The credential handoff is a one-time administrator action in the Moltaro Portal or administrative Web Application interface. Installing or registering the Web Application and provisioning access are not part of an agent’s API workflow.
For a trusted AI agent, the administrator opens **Administration > Agent integration**, selects **Create service user for agent**, and confirms the high-privilege handoff. Moltaro creates a new Service user with the Admin and Configurator roles, issues a 90-day API key, and downloads a ZIP containing `AGENTS.md`, `CLAUDE.md`, `.moltaro/credentials.env`, and a nested `.moltaro/.gitignore`. The key appears only in that download.
Extract the kit into the intended project, delete the ZIP, and keep `.moltaro/credentials.env` local. Never commit, upload, paste, echo, or print the credentials file or `MOLTARO_API_KEY`. Other unattended integrations can still use Administration > Users to create a narrowly scoped Service user and API key manually.
An agent receives the prepared guide, URL, and token. If they are missing, it asks the user to complete Agent integration in the Portal or administrative interface. It does not create its own service user, issue a key, change roles, or use administrative Agent integration endpoints to discover an unknown workspace.
A service key is gated by roles only — exactly like a human user. With the runtime roles a key covers the record-centric integration surface; with the **Admin** or **Configurator** role it can also use the administrative configuration API, including creating and changing entity definitions. Grant the narrowest role set that covers the integration’s task. The one exception is account self-service (profile, password): service accounts have nothing to manage there, and those endpoints answer them with an explicit error.
The curated administrative surface is documented in the [Configuration API reference](/docs/developer/configuration-api-reference/), starting with entity definition and schema field management.
The generated Markdown files contain the installation’s real base URL, the discovery flow, secret-handling rules, and links to the installation-local OpenAPI documents. This is a completed administrative handoff, not an agent task. The `agent-integration` routes are administrative endpoints of the configured Moltaro Application, not the Portal. An authorized agent may call the read endpoints after it already knows `WORKSPACE_API_BASE_URL` and has a key; it must not call the access-kit endpoint to provision or elevate itself.
For retry, concurrency, polling, recovery, and audit behavior, apply [Reliable API automation](/docs/developer/reliable-api-automation/).
## Step 1 — Read the workspace context
[Section titled “Step 1 — Read the workspace context”](#step-1--read-the-workspace-context)
`GET /api/workspace/context` requires no authentication and returns the installation’s locale, time zone, and authoring mode:
```bash
curl -s https://ops.example.com/api/workspace/context
```
```json
{
"Data": {
"DisplayName": "Acme Operations",
"Locale": "en",
"TimeZone": "Etc/UTC",
"RegionCode": "us",
"EnabledFeatures": [],
"GoogleMapsApiKey": null,
"BusinessLogicAuthoringMode": "NetOperationProject"
},
"Errors": [],
"Warnings": [],
"Success": true
}
```
## Step 2 — Authenticate
[Section titled “Step 2 — Authenticate”](#step-2--authenticate)
`POST /api/workspace/auth/login` exchanges credentials for an access token. (For unattended integrations and agents, prefer a [service-account API key](#service-accounts-and-api-keys) and skip this step — API keys are sent the same way, as bearer tokens.)
```bash
curl -s -X POST https://ops.example.com/api/workspace/auth/login \
-H "Content-Type: application/json" \
-d '{ "UserNameOrEmail": "owner@acme.test", "Password": "" }'
```
```json
{
"Data": {
"AccessToken": "",
"UserId": "f4ac8709-c8ac-4404-a30a-7ddbcfcc8115",
"UserName": "owner",
"WorkspaceName": "default",
"Locale": "en",
"TimeZone": "Etc/UTC",
"Roles": ["Admin", "Everyone"],
"Modules": { "Boards": { "Enabled": false } }
},
"Success": true
}
```
Send the token on every following request: `-H "Authorization: Bearer "`. `GET /api/workspace/auth/me` returns the same profile for an existing token.
## Step 3 — List entity definitions
[Section titled “Step 3 — List entity definitions”](#step-3--list-entity-definitions)
[`GET /api/workspace/entity-definitions`](/docs/developer/api-reference/operations/entity-definitions-list/) returns every record type the caller may see — the workspace’s data model:
```json
{
"Data": [
{
"Id": "I0ub2IyjFcx4",
"Name": "SupportTicket",
"DisplayNameSingular": "Support ticket",
"DisplayNamePlural": "Support tickets",
"Description": "Demo record type for the integration quickstart.",
"CommentsEnabled": true,
"AttachmentsEnabled": true,
"TagsEnabled": true,
"SearchTargetCount": 0,
"AuditTrailEnabled": true,
"IsReadOnly": false,
"DisabledOperations": 0,
"UsageType": 0
}
],
"Success": true
}
```
`Name` is the stable internal name. Instance endpoints accept either the `Id` or this name as `{entityIdOrKey}`.
Entity Definition discovery does not imply record access. Every record query and direct read is constrained by the definition’s current Security configuration. Missing, invalid, disabled, or stale root `View` Permission Assignments fail closed.
## Step 4 — Read one definition to learn its schema
[Section titled “Step 4 — Read one definition to learn its schema”](#step-4--read-one-definition-to-learn-its-schema)
[`GET /api/workspace/entity-definitions/{entityDefinitionId}`](/docs/developer/api-reference/operations/entity-definitions-get/) returns the full definition, including tables and fields. This is what tells an agent which field keys exist, their types, and their rules:
```json
{
"Data": {
"Id": "I0ub2IyjFcx4",
"Name": "SupportTicket",
"Tables": [
{
"Id": "oGp2IzJVF8Sn",
"IsPrimary": true,
"Fields": [
{ "Key": "Title", "FieldType": 10, "RequiredWhen": "TRUE", "StringMaxLength": 200 },
{ "Key": "DueDate", "FieldType": 1 },
{ "Key": "Priority", "FieldType": 9,
"Options": [ { "Key": "low", "Value": "Low" }, { "Key": "medium", "Value": "Medium" }, { "Key": "high", "Value": "High" } ] },
{ "Key": "EstimatedHours", "FieldType": 6, "DecimalPrecision": 6, "DecimalScale": 2 }
]
}
]
},
"Success": true
}
```
`FieldType` is the numeric `EntityFieldTypeEnum` (10 = String, 1 = DateOnly, 2 = TimeOnly, 9 = Select, 6 = Decimal, 13 = Reference, 17 = DateTimeOffset, …). The full list with semantics is in [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/). `RequiredWhen: "TRUE"` means always required; other values are conditional expressions.
`DateTimeOffset` values are ISO 8601 timestamps with an explicit `Z` or numeric offset. For example, `"2026-07-27T10:15:42.123456+02:00"` is returned as the same instant in UTC: `"2026-07-27T08:15:42.123456Z"`. The API and database preserve microseconds, but not the sender’s original offset. Never send a local timestamp without an offset.
## Step 5 — Create a record
[Section titled “Step 5 — Create a record”](#step-5--create-a-record)
`POST /api/workspace/entity/{entityIdOrKey}/instances` takes a `Fields` map keyed by field key:
```bash
curl -s -X POST https://ops.example.com/api/workspace/entity/SupportTicket/instances \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "Fields": { "Title": "Printer in hall B is jammed", "DueDate": "2026-07-25",
"Priority": "high", "EstimatedHours": 1.5 } }'
```
```json
{
"Data": {
"InstanceId": "c24878e1ff334648aeb4022e19f6cd96",
"RowVersion": "53cedb7e-dad6-425c-86c1-edd23dacfd59",
"ReadableAfterMutation": true,
"Instance": {
"Id": "c24878e1ff334648aeb4022e19f6cd96",
"Number": "1",
"DisplayName": "1",
"Fields": {
"Title": { "State": 0, "Value": "Printer in hall B is jammed" },
"DueDate": { "State": 0, "Value": "2026-07-25" },
"Priority": { "State": 0, "Value": "high" },
"EstimatedHours": { "State": 0, "Value": 1.5 }
},
"Permissions": { "CanRead": true, "CanUpdate": true, "CanArchive": true, "CanDelete": false }
}
},
"Success": true
}
```
Note the read shape: each field comes back as an envelope with a `State` (0 = value present, 1 = restricted — the caller may not read this field) and the `Value`. `Permissions` are server-computed for the calling user — trust them instead of guessing.
## Step 6 — Query records
[Section titled “Step 6 — Query records”](#step-6--query-records)
`POST /api/workspace/entity/{entityIdOrKey}/instances/query` supports paging, a nested filter tree, sorting, and reference includes:
For filters and sorts that traverse references, use `Target.Path` instead of the direct `Field` property. The [Entity Instance Query guide](/docs/developer/entity-instance-query/) gives complete payloads, limits, reference-ID operand rules, access semantics, and saved/shared-view behavior.
```json
{
"Page": 1,
"PageSize": 25,
"Filter": {
"Operator": 0,
"Conditions": [
{ "Field": { "FieldKey": "Priority" }, "Operator": 0, "Values": [ { "Value": "high" } ] }
]
},
"Sort": [ { "Field": { "FieldKey": "DueDate" }, "Direction": 0 } ]
}
```
The response is a page: `{ "Items": [...], "TotalCount": 1, "Page": 1, "PageSize": 25, "Permissions": { "CanCreate": true, … } }`. Condition `Operator` is numeric (`0 = Eq`, `7 = In`, `9 = Contains`, `13 = IsNull`, …); the full operator list is in the [query operation](/docs/developer/api-reference/operations/entity-instances-query-list/) schema. A single record is read with `GET .../instances/{instanceId}`.
## Step 7 — Update with optimistic concurrency
[Section titled “Step 7 — Update with optimistic concurrency”](#step-7--update-with-optimistic-concurrency)
`PATCH /api/workspace/entity/{entityIdOrKey}/instances/{instanceId}` sends only the fields to change plus the `RowVersion` you last saw. Omitted keys stay unchanged; an explicit `null` clears a value:
```bash
curl -s -X PATCH https://ops.example.com/api/workspace/entity/SupportTicket/instances/c24878e1ff334648aeb4022e19f6cd96 \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "Fields": { "Priority": "medium" }, "RowVersion": "53cedb7e-dad6-425c-86c1-edd23dacfd59" }'
```
A successful mutation returns the new `RowVersion` (`"75ee761a-0dd1-44f5-ab36-33089bd37a6e"` here) — keep it for the next write. A stale `RowVersion` fails the request instead of silently overwriting someone else’s change.
## Error handling
[Section titled “Error handling”](#error-handling)
Failures return the same envelope with `Success: false` and structured errors. This 400 came from creating a record without the required `Title`:
```json
{
"Data": null,
"Errors": [
{ "Code": "moltaro.instances.validation.requiredFields",
"Message": "Fill in all required fields and try again.", "Field": null, "Type": 1, "Severity": 2 },
{ "Code": "moltaro.instances.field.required",
"Message": "This field is required.", "Field": "Title", "Type": 1, "Severity": 2 }
],
"Success": false
}
```
`Code` is stable and machine-readable; `Field` names the offending field key or PascalCase request property; `Message` is localized for display. Agents should branch on `Code` and `Field`, not on `Message`. The error shape is documented in [Errors and responses](/docs/developer/errors/), and a curated catalog of core entity and record-write codes is in the [error code reference](/docs/developer/error-codes/).
## Field value shapes when writing
[Section titled “Field value shapes when writing”](#field-value-shapes-when-writing)
| Field type | Write value |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Yes/No | `true` / `false` |
| Date / Time | `"2026-07-25"` / `"14:30:00"` |
| String / Text | JSON string |
| Whole number | JSON number |
| Decimal | JSON number or invariant decimal string on write; canonical invariant string padded to configured scale in Entity Instance field envelopes and form-evaluation patches |
| Select | option key string; array of keys when the field allows multiple |
| Classifier | Catalog node id string from the Classifier Catalog bound to the field |
| User / Role / File / Reference | id string of the target |
| Inverse reference | array of source record ids |
| Money | `{ "Amount": 10.5, "CurrencyCode": "EUR" }` — amounts come back as strings |
| Address | object with `FullAddress`, street/locality/region/postal/country parts, optional `Latitude`/`Longitude` |
| Table | managed through table-row payloads, not a scalar value |
Details per type: [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/).
## Invoke business logic
[Section titled “Invoke business logic”](#invoke-business-logic)
Workspaces can publish their own C# business logic as callable functions. Which keys exist is workspace-specific — publications are part of the workspace configuration, so discover them per installation. Accounts with the **Admin** role can list every function and its publications through the Function Catalog in the [Configuration API](/docs/developer/configuration-api-reference/) (`GET /api/workspace/admin/function-catalog`); other integrations receive their callable keys from the workspace administrator.
A **command** runs synchronously and returns its result in the response. `POST /api/workspace/commands/{functionKey}` takes optional `Args` (JSON matching the command’s argument type) and an optional `CorrelationId`; an empty body is allowed for commands without arguments:
```bash
curl -s -X POST https://ops.example.com/api/workspace/commands/close-overdue-tickets \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "Args": { "GraceDays": 3 }, "CorrelationId": "req-8842" }'
```
```json
{
"Data": {
"RunId": "b6f3e0d8a3c14f7f9a1de2c07f6b4a51",
"Message": "Closed 4 overdue tickets.",
"Result": { "ClosedCount": 4 },
"DurationMs": 118,
"CorrelationId": "req-8842"
},
"Success": true
}
```
Each publication either allows any authenticated user or requires a specific permission; callers without it get `403`. Unknown, disabled, and retired keys all return the same `404` (`moltaro.functionCommands.notFound`), so command keys cannot be probed.
Long-running logic is published for asynchronous execution instead. `POST /api/workspace/functions/{functionKey}/enqueue` accepts optional `Args`, a `DeduplicationKey` (reuses an active queued job), `RunAfter` (earliest execution time), and `CorrelationId`, and returns the queued job:
```json
{
"Data": {
"JobId": "0d2f6c1e58b747a2a9c3f1b4e6d80a97",
"FunctionKey": "rebuild-reporting-snapshots",
"Status": 0,
"CreatedAt": "2026-07-22T09:14:03+00:00",
"NextRunAt": null,
"AttemptCount": 0,
"DeduplicationKey": null,
"CorrelationId": null,
"LastError": null,
"CanCancel": true,
"Run": null
},
"Success": true
}
```
Poll `GET /api/workspace/functions/jobs/{jobId}` until `Status` reaches a terminal value (`0 = Queued`, `1 = Leased`, `2 = Completed`, `3 = Failed`, `4 = Cancelled`). A finished job carries its latest execution under `Run`, including the structured `ResultData` payload and a bounded `ErrorSummary` on failure. While `CanCancel` is `true`, `POST /api/workspace/functions/jobs/{jobId}/cancel` cancels the queued job. Job visibility matches invocability: the initiator and every holder of the publication permission can read and cancel the job.
The full contract — publication and permission model, request options, job lifecycle, and error codes — is in [Commands and API functions](/docs/developer/business-logic/commands-and-api-functions/).
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Entity UI](/docs/developer/api-reference/operations/tags/entity-ui/) returns the configured list, details, drawer, card, and form payloads when an integration should render records the way the product does.
* [Entity Data Transfer](/docs/developer/api-reference/operations/tags/entity-data-transfer/) moves record data in bulk through admin-defined profiles.
* The [Configuration quickstart](/docs/developer/configuration-quickstart/) creates the entity definition this page discovers — schema authoring through the [Configuration API](/docs/developer/configuration-api-reference/) (raw contract: [`moltaro-config-v1.json`](/docs/api/openapi/moltaro-config-v1.json)).
* The Boards walkthroughs cover process automation end to end: [board configuration](/docs/developer/boards-configuration/) and [items and moves](/docs/developer/boards-runtime/).
* The [Entitlement Operations walkthrough](/docs/developer/entitlement-operations/) goes from module setup to grant, consume, and the ledger.
* [Record history and audit](/docs/developer/audit-trail/) reads the change feed this page’s create and update produced.
* AI agents can start from [`/llms.txt`](https://moltaro.com/llms.txt) for a machine-oriented map of this documentation, or fetch the complete documentation as one Markdown file at [`/docs/llms-full.txt`](https://moltaro.com/docs/llms-full.txt) (abridged variant: [`/docs/llms-small.txt`](https://moltaro.com/docs/llms-small.txt)).
# Configuration quickstart
> Create an entity definition, add schema fields, configure search and its user-facing table, and create the first record through the Configuration API — for developers and AI agents.
The [Integration quickstart](/docs/developer/integration-quickstart/) starts from a workspace that already has a record type and shows how to discover and use it. This page is the other half of the pair: it starts from an empty workspace and creates that same `SupportTicket` record type through the Configuration API — definition first, then schema fields, explicit search targets, and a usable user-facing table, then the first record. Together the two pages cover the full loop an AI agent needs: shape the data model and its query surface, then operate on it.
All examples below were executed against a real Moltaro installation; response bodies are real, trimmed for length. The complete administrative contract is in the [Configuration API reference](/docs/developer/configuration-api-reference/) and in the raw OpenAPI document ([`moltaro-config-v1.json`](/docs/api/openapi/moltaro-config-v1.json), also served by every installation at `/openapi/moltaro-config-v1.json`).
Use the workspace API host
All `/api/workspace/...` routes on this page are relative to the configured workspace’s `WORKSPACE_API_BASE_URL`, not to `https://moltaro.com`. The public site provides documentation and reference copies only. See [Connect to a workspace API](/docs/developer/workspace-api-connection/) for the administrator-to-developer handoff.
For an agent, that handoff must already contain the guide, API base URL, key, and required role. If it does not, ask the user to complete Agent integration in the Portal or administrative interface; do not create credentials or grant permissions through the API. Apply the conflict and destructive-change rules from [Reliable API automation](/docs/developer/reliable-api-automation/).
After completing the flat vertical slice here, use [Configuring Parent Tree View](/docs/configuration/hierarchies/configuring-parent-tree-view/) for the self-Reference, index, default or explicitly selected Table Surface, Entity List selector, Lookup picker, and read-back workflow.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* The caller needs the **Admin** or **Configurator** role. For unattended work use a [service-account API key](/docs/developer/integration-quickstart/#service-accounts-and-api-keys) whose service user carries one of those roles; the examples below send it as a normal bearer token.
* Everything else from the [Integration quickstart conventions](/docs/developer/integration-quickstart/#conventions) applies: the `/api/workspace/` base path, the `{ "Data": …, "Errors": [], "Warnings": [], "Success": true }` envelope, PascalCase property names, and numeric enums with `x-enum-varnames` in the OpenAPI schema.
* The business schema is provisioned immediately, but a new Entity Definition is intentionally locked until its Security pages contain a valid enabled Security Statement assigned to the root `View` Permission. Destructive schema edits (field type changes, deletions) go through explicit `schema-change-plan` preview endpoints instead of applying silently.
## Step 1 — Create the entity definition
[Section titled “Step 1 — Create the entity definition”](#step-1--create-the-entity-definition)
`POST /api/workspace/admin/entity-definitions` ([operation](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-create/)) creates the record type. `Name` is the stable internal name that instance endpoints later accept as `{entityIdOrKey}`; the display names are what users see:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/entity-definitions \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "Name": "SupportTicket", "DisplayNameSingular": "Support ticket",
"DisplayNamePlural": "Support tickets",
"Description": "Demo record type for the integration quickstart." }'
```
```json
{
"Data": {
"Id": "pYZqCLZWb0K1",
"Name": "SupportTicket",
"DisplayNameSingular": "Support ticket",
"DisplayNamePlural": "Support tickets",
"CommentsEnabled": true,
"AttachmentsEnabled": true,
"TagsEnabled": true,
"SearchTargetCount": 0,
"SearchTargets": [],
"AuditTrailEnabled": true,
"AuditTrailRetentionMode": 1,
"Tables": [
{
"Id": "yBxr7wo1xsNo",
"SchemaName": "op",
"PhysicalTableName": "SupportTicket",
"IsPrimary": true,
"Fields": []
}
],
"RowVersion": "a244a235-1dc6-4e8b-81cf-2db6b972cd55"
},
"Success": true
}
```
The response is the full definition detail, including the tables that were provisioned with it. Keep the id of the table with `"IsPrimary": true` — that is where schema fields are created in the next step. Note what the platform turned on by default: comments, attachments, tags, and the audit trail with unlimited retention (see [Record history and audit](/docs/developer/audit-trail/)). Responsibilities and assignment authority are authored in Entity Security rather than created as Entity relations. Search becomes available after explicit target paths are saved on the definition’s Search tab.
## Step 2 — Add schema fields
[Section titled “Step 2 — Add schema fields”](#step-2--add-schema-fields)
`POST /api/workspace/admin/entity-definition-tables/{entityDefinitionTableId}/fields` ([operation](/docs/developer/configuration-api-reference/operations/admin-entity-fields-create/)) adds one field per call. `Key`, `DisplayName`, and `FieldType` are required; everything else is per-type configuration. The `Select` field is the most involved shape, so here it is in full:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/entity-definition-tables/yBxr7wo1xsNo/fields \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "Key": "Priority", "DisplayName": "Priority", "FieldType": 9,
"RequiredWhen": "TRUE",
"Options": [
{ "Key": "low", "Value": "Low" },
{ "Key": "medium", "Value": "Medium" },
{ "Key": "high", "Value": "High" }
] }'
```
```json
{
"Data": {
"Id": "l1eqb13B03NG",
"EntityDefinitionId": "pYZqCLZWb0K1",
"EntityDefinitionTableId": "yBxr7wo1xsNo",
"Key": "Priority",
"DisplayName": "Priority",
"FieldType": 9,
"RequiredWhen": "TRUE",
"Options": [
{ "Id": "3pflNS8LtMQ0", "Key": "low", "Value": "Low", "SortOrder": 0 },
{ "Id": "1CK3fc4xlwuK", "Key": "medium", "Value": "Medium", "SortOrder": 1 },
{ "Id": "6ZZoc0FtzRWA", "Key": "high", "Value": "High", "SortOrder": 2 }
],
"RowVersion": "9fe783cb-b821-4252-800d-63245850b04d"
},
"Success": true
}
```
The remaining three quickstart fields are one call each with the same shape:
| Field | Request body |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Title (required string, max 200) | `{ "Key": "Title", "DisplayName": "Title", "FieldType": 10, "RequiredWhen": "TRUE", "StringMaxLength": 200 }` |
| DueDate (date) | `{ "Key": "DueDate", "DisplayName": "Due date", "FieldType": 1 }` |
| EstimatedHours (decimal 6,2) | `{ "Key": "EstimatedHours", "DisplayName": "Estimated hours", "FieldType": 6, "DecimalPrecision": 6, "DecimalScale": 2 }` |
| ScheduledAt (exact instant) | `{ "Key": "ScheduledAt", "DisplayName": "Scheduled at", "FieldType": 17 }` |
### Relational and typed fields
[Section titled “Relational and typed fields”](#relational-and-typed-fields)
Reference, Money, and Classifier fields each carry one extra key that points at an existing target — read the target definition or catalog id from the API first, never guess it:
| Field | Request body |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Customer (single Reference to another definition) | `{ "Key": "Customer", "DisplayName": "Customer", "FieldType": 13, "ReferenceToEntityDefinitionId": "", "ReferenceDeleteBehavior": 2 }` |
| Budget (Money in a base currency) | `{ "Key": "Budget", "DisplayName": "Budget", "FieldType": 15, "BaseCurrencyCode": "EUR" }` |
| Category (Classifier from a catalog) | `{ "Key": "Category", "DisplayName": "Category", "FieldType": 16, "ClassifierCatalogDefinitionId": "" }` |
To expose the inverse side of a Reference on the target record (its “referenced-by” list), create an Inverse reference field (`FieldType` 14) on the target definition’s primary table. Both relation identifiers are required: `ReferenceToEntityDefinitionId` is the id of the source (or child) definition that owns the forward Reference field, and `PairedReferenceFieldId` is the id of that forward field.
For example, if `SupportTicket.Customer` is a Reference to `Customer`, add the inverse field to the `Customer` primary table with this complete request:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/entity-definition-tables//fields \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "Key": "SupportTickets", "DisplayName": "Support tickets", "FieldType": 14,
"ReferenceToEntityDefinitionId": "",
"PairedReferenceFieldId": "" }'
```
Do not set `ReferenceToEntityDefinitionId` to the `Customer` definition where the inverse field is being created. It points back to `SupportTicket`, the definition that contains the forward Reference. Omitting it returns `moltaro.fields.reference.target.required`; omitting `PairedReferenceFieldId` returns `moltaro.fields.inverseReference.pairedField.required`.
`ReferenceDeleteBehavior` is `0` No action, `1` Cascade, or `2` Restrict. Omitting it while creating a Reference, or while changing another field to Reference, defaults to Restrict. Omitting it from an ordinary field update preserves the stored value. Restrict applies to primary- and child-table References and returns HTTP 409 with `moltaro.instances.delete.referenced` when a permanent delete is blocked. Cascade is available only on a primary-table Reference. Archive behavior is a separate setting.
YAML import follows the same omission rules: an omitted delete behavior on a new Reference becomes Restrict, while omission on a matched existing Reference preserves its stored value. YAML export writes the effective value explicitly. Blueprint-created and Package SDK Reference fields also default to Restrict; package-authored child-table references cannot use Cascade.
`FieldType` is the numeric `EntityFieldTypeEnum` — the same numbers the read endpoints return: `10` String, `11` Text, `5` Integer, `6` Decimal, `0` Yes/No, `1` Date, `2` Time, `9` Select, `13` Reference, `14` Inverse reference, `3` User, `15` Money, `16` Classifier, `17` Date and time, `8` Address. A Date and time value must be an ISO 8601 timestamp with an explicit `Z` or numeric offset, for example `"2026-07-27T10:15:42.123456+02:00"`. The API returns the equivalent UTC instant; it does not preserve the submitted offset. The full list with per-type semantics and write shapes is in [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/). `RequiredWhen: "TRUE"` means always required; any other value is a conditional [expression](/docs/dsl/).
### Typed defaults for new records
[Section titled “Typed defaults for new records”](#typed-defaults-for-new-records)
`DefaultValue` is schema metadata, not a DSL expression. It is applied only when the field key is absent from an Entity Instance create request. For example:
```json
{ "Key": "Active", "DisplayName": "Active", "FieldType": 0,
"DefaultValue": { "Kind": 0, "Value": true } }
```
The `Kind` values are `0` Constant, `1` CurrentDate, and `2` CurrentDateTime. CurrentDate is valid only for Date; CurrentDateTime is valid only for Date and time. For a dynamic default, `Value` must be omitted or `null`; every non-null value is invalid.
On a create form, `CurrentDate` is presented as an editable workspace-local preview. The preview does not change the API contract: an untouched field is still omitted and is resolved authoritatively when create is processed. A user change or clear is sent explicitly. `CurrentDateTime` has no editable preview because the exact instant remains server-resolved at create time.
Constant defaults support Yes/No, Whole number, Decimal, String, Text, Money, Select (one configured option key, or a non-empty array of unique configured keys for multi-select), scalar Reference, Date, Time, and Date and time. A Reference constant uses one target entity-instance ID string:
```json
"DefaultValue": { "Kind": 0, "Value": "" }
```
The ID must identify an active actor-readable record in the field’s configured target definition. Number and Display Name are UI-only resolved data. Runtime create/new-child application rechecks current access, active state, Reference Integrity, and Reference Eligibility and fails without partial writes when the target is unavailable. Money uses the ordinary `{ "Amount": "12.34", "CurrencyCode": "EUR" }` shape. Its currency must be active in the workspace; when `CurrencyCode` is omitted, `null`, or blank, the field’s active `BaseCurrencyCode` is used. Other non-string values are invalid. String and Text constants are trimmed like ordinary runtime values and cannot be empty or whitespace-only. Decimal accepts a JSON number or an invariant decimal string; JavaScript clients should use the string form for values that exceed safe numeric precision. The Entity Instance create/patch API accepts the same invariant string form, so the browser can submit a high-precision Decimal without rounding it through a JavaScript number. Entity Instance field envelopes and form-evaluation patches return Decimal as a canonical invariant string padded to the field’s configured scale. Date and time constants require an explicit offset and are canonicalized to UTC. Calculated and Line number fields cannot have defaults.
Omitting a field key during record creation asks the server to use the default. Sending the key with `null`, `false`, `0`, an empty string, or an empty array is explicit caller input and suppresses it. Defaults also apply to missing fields on newly created child rows, including rows added by a patch request. It never defaults primary fields during patch/update and never changes existing child rows or existing data.
Field updates are full configuration writes. Read and round-trip the current `DefaultValue` when changing another property; send `"DefaultValue": null` to remove it. Definition YAML carries the same shape as `DefaultValue`, using enum names such as `Constant` or `CurrentDate`. A Reference constant remains workspace-local: YAML and Package apply preserve its exact ID and fail before mutation when the record is missing, archived, or belongs to another target definition. Interactive import also honors the current actor’s access; Number and Display Name are never portable. See [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/#default-values) for the configurator behavior.
## Step 3 — Read the definition back
[Section titled “Step 3 — Read the definition back”](#step-3--read-the-definition-back)
The definition is immediately visible through the public API — this is the exact read that the [Integration quickstart](/docs/developer/integration-quickstart/#step-4--read-one-definition-to-learn-its-schema) uses for discovery:
```bash
curl -s https://ops.example.com/api/workspace/entity-definitions/pYZqCLZWb0K1 \
-H "Authorization: Bearer "
```
```json
{
"Data": {
"Id": "pYZqCLZWb0K1",
"Name": "SupportTicket",
"Tables": [
{
"Id": "yBxr7wo1xsNo",
"IsPrimary": true,
"Fields": [
{ "Key": "Priority", "FieldType": 9, "RequiredWhen": "TRUE",
"Options": [ { "Key": "low", "Value": "Low" }, { "Key": "medium", "Value": "Medium" }, { "Key": "high", "Value": "High" } ] },
{ "Key": "Title", "FieldType": 10, "RequiredWhen": "TRUE", "StringMaxLength": 200 },
{ "Key": "DueDate", "FieldType": 1 },
{ "Key": "EstimatedHours", "FieldType": 6, "DecimalPrecision": 6, "DecimalScale": 2 }
]
}
]
},
"Success": true
}
```
## Step 4 — Give records a display name
[Section titled “Step 4 — Give records a display name”](#step-4--give-records-a-display-name)
A field named `Title` does not automatically become the record label. Until a display-name presentation rule is set, every record shows as its `Number` — note the `"DisplayName": "1"` in the responses here. Configure the label with a presentation rule through the same Configuration API:
`PUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/display-settings`
The request carries `PresentationRules` (the display-name expression — for example one that returns the `Title` field), an optional `NumberPrefix`, `DefaultSort`, and the current `RowVersion`. See [Presentation rules](/docs/dsl/presentation-rules/) for the expression grammar and [Display fields](/docs/user/data-structure/entity-definitions/display-fields/) for the concept.
## Step 5 — Configure record search
[Section titled “Step 5 — Configure record search”](#step-5--configure-record-search)
Fields do not become searchable merely because they contain text. Read the latest administrative Entity Definition detail, keep its `RowVersion`, and replace the complete set of search targets:
```bash
curl -s -X PUT https://ops.example.com/api/workspace/admin/entity-definitions/pYZqCLZWb0K1/search-targets \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "SearchTargets": [
{ "Path": [ { "FieldKey": "Title" } ] },
{ "Path": [ { "SystemTarget": 0 } ] }
],
"RowVersion": "" }'
```
This makes the runtime search box match the Title field or computed display name. The request replaces every existing target, and an empty list disables search. Use [Entity search](/docs/developer/entity-search/) for relation and Table paths, system-target values, exact runtime `SearchTerm` payloads, concurrency, strict Entity YAML v8, and the contracts that replaced the old full-text flags and rebuild operations.
## Step 6 — Configure the user-facing table query surface
[Section titled “Step 6 — Configure the user-facing table query surface”](#step-6--configure-the-user-facing-table-query-surface)
Creating the schema does not complete a user-facing record type. Before writing the table configuration, read the installed release’s supported targets:
```text
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/table-surfaces/configuration/options
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/table-surfaces/{surfaceKey}/configuration
```
See the generated operations for [configuration options](/docs/developer/configuration-api-reference/operations/admin-entity-ui-get-table-surface-configuration-options/) and the [current table configuration](/docs/developer/configuration-api-reference/operations/admin-entity-ui-get-table-surface-configuration/). Choose the ordered `FilterItems`, `SortItems`, and `DefaultSort` from the typed targets returned by the options response, then replace the configuration with [PUT table configuration](/docs/developer/configuration-api-reference/operations/admin-entity-ui-put-table-surface-configuration/). Do not guess field paths or infer query behavior from the column renderer. For complete related-path examples and the distinction between query targets and lookup breadcrumbs, read [Table filters and sorting](/docs/configuration/table-filters-and-sorting/).
Every visible business dimension that users reasonably need to segment or order by—especially a status or another reference—should normally be present in both `FilterItems` and `SortItems`. A root reference target such as `Status` is distinct from its displayed value. Use `Status/Name` when the intended behavior is label ordering. Use a business target such as `Status/SortOrder` only when the referenced records actually contain meaningful values. Every selected path must come from the options response.
After the PUT, GET the configuration again and verify localized labels, visibility, `VisibleByDefault`, item ordering, typed sort paths, and `DefaultSort`. Then apply at least one representative filter and one representative sort in the Web Application. Check the returned total, every visible row value, and visible ordering; appearing in the Filters or Sorting drawer alone is not sufficient. Clear the temporary filter and restore the intended default sort after testing.
Filters and sorts do not grant access. Repeat the applied-query check as an ordinary reader when possible, and confirm that server-side entity and record Security Statements still protect inaccessible fields, records, and result counts. Keep technical keys and paths in English, localize workspace-authored labels, and do not put credentials, secrets, or copied customer values in examples.
## Step 7 — Configure Entity Security
[Section titled “Step 7 — Configure Entity Security”](#step-7--configure-entity-security)
Before any runtime record request, an Owner, Admin, or Configurator opens the Entity Definition and configures its three Security pages:
1. create the required Security Statements;
2. assign root `View`, actions, and field Permissions to those Statements;
3. create Responsibility Definitions and assignment rules when the record type uses responsibilities.
Each successful save is atomic and immediately effective. There is no separate publish step. Owner/Admin/Configurator may edit this configuration but do not receive Entity Instance access unless the configured Statements grant it.
## Step 8 — Create the first record
[Section titled “Step 8 — Create the first record”](#step-8--create-the-first-record)
Record creation now uses the replay-safe command contract. Send a new UUID as `OperationKey`, the definition key, the latest definition `RowVersion`, the caller-supplied values, and any StageOnCreate responsibility candidates:
```bash
curl -s -X POST https://ops.example.com/api/workspace/entity/SupportTicket/instances \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "OperationKey": "154e7dc2-9f61-4ba4-a4ab-e1bd94183dfa",
"EntityDefinitionKey": "SupportTicket",
"ExpectedDefinitionRowVersion": "a244a235-1dc6-4e8b-81cf-2db6b972cd55",
"Values": { "Title": "Printer in hall B is jammed", "DueDate": "2026-07-25",
"Priority": "high", "EstimatedHours": 1.5 },
"ResponsibilityCandidates": [] }'
```
All five properties are required:
* `OperationKey` is a non-empty caller-generated UUID for one logical create. Preserve it only for an exact retry after an unknown outcome. Reusing it with different input returns an idempotency conflict.
* `EntityDefinitionKey` must identify the same definition as the route, by id or normalized key.
* `Values` is the field-key-to-JSON-value map. Send `{}` when the record has no caller-supplied values; field defaults and governed mutation rules remain server-owned.
* `ResponsibilityCandidates` is the complete caller proposal for StageOnCreate assignment. Send `[]` when no candidates are proposed. It is required and must not be omitted or `null`.
* `ExpectedDefinitionRowVersion` is the latest Entity Definition `RowVersion`. It protects the create from running against a schema that changed after the caller read it. On a conflict, re-read the definition and rebuild the request.
Clients written for the pre-0.0.20 payload must regenerate from the current Runtime OpenAPI document. Replace `Fields` with `Values`, and replace the arbitrary string `MutationIdempotencyKey` with the required UUID `OperationKey`. Missing or null required members return HTTP 400 with a stable error `Code` and the exact PascalCase property in `Field`; branch on those values rather than localized messages.
```json
{
"Data": {
"Kind": "AuthorizedEntityDetail",
"Data": {
"EntityDefinitionId": "pYZqCLZWb0K1",
"EntityInstanceId": "98520ecb203d4188abf10298a5a638e2",
"RowVersion": "daefc569-83f4-4ca2-aa5d-071e7cfa3942",
"AuthorizedFields": {
"Priority": "high",
"Title": "Printer in hall B is jammed",
"DueDate": "2026-07-25",
"EstimatedHours": 1.50
}
},
"CanViewResult": true
},
"Success": true
}
```
From here the [Integration quickstart](/docs/developer/integration-quickstart/) covers querying, updating with optimistic concurrency, and error handling.
## Configure the workspace file-type policy
[Section titled “Configure the workspace file-type policy”](#configure-the-workspace-file-type-policy)
Workspace Owners and Administrators can read and update the same FileSystem settings used by the Web Application through the Configuration API. A service user with the Administrator role has the same access; other roles receive `403 Forbidden`.
```bash
curl -s https://ops.example.com/api/workspace/filesystem/settings \
-H "Authorization: Bearer "
```
The authoritative `FileTypeRules` pair extensions with declared MIME types. A file is accepted only when its longest matching extension and normalized declared MIME appear in the same rule. Keep the `RowVersion` from GET and send it back on PUT. PUT replaces the complete policy, so start with the rules from GET, make the intended addition or edit, and send the full resulting array. The shortened array below illustrates the request shape and would deliberately restrict the workspace to only these three rules:
```bash
curl -s -X PUT https://ops.example.com/api/workspace/filesystem/settings \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{
"FileTypeRules": [
{ "Extensions": [".pdf"], "MimeTypes": ["application/pdf"] },
{ "Extensions": [".dwg"], "MimeTypes": ["application/octet-stream", "application/acad", "application/x-dwg"] },
{ "Extensions": [".fbd"], "MimeTypes": ["application/octet-stream"] }
],
"TrashRetentionTimeHours": 720,
"RowVersion": ""
}'
```
The policy cannot be empty. Administrators may deliberately allow any extension, including executable formats, and own that configuration decision. This mechanism checks only the requested file name and declared MIME type. It is not antivirus or malware inspection and cannot detect executable content renamed to an allowed ambiguous format. `AllowedMimeTypes` in responses is a deprecated compatibility summary and is not authoritative for new clients. An upload rejected with `filesystem.file.type.notAllowed` exposes only the normalized, bounded `Extension` and `DeclaredMimeType` plus the effective `FileTypePolicyVersion` and `FileTypePolicyHash` in error metadata. The same version/hash are returned by settings and upload preflight so operators can correlate a decision across API and Worker processes. This metadata does not expose file content or storage-provider details.
## When configuration fails
[Section titled “When configuration fails”](#when-configuration-fails)
Configuration errors come back in the same envelope with stable machine-readable codes. Creating a second definition with the same name:
```json
{
"Data": null,
"Errors": [
{ "Code": "moltaro.definitions.name.exists",
"Message": "An entity definition with this name already exists.",
"Field": "Name", "Type": 1, "Severity": 2 }
],
"Success": false
}
```
Branch on `Code` and `Field`, not on `Message`. The configuration error catalog is part of the [error code reference](/docs/developer/error-codes/).
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Boards configuration walkthrough](/docs/developer/boards-configuration/) — put the new record type on a governed process board.
* [Record history and audit](/docs/developer/audit-trail/) — audit settings and the record change feed.
* [Validation rules](/docs/developer/configuration-api-reference/operations/tags/entity-validation-rules/) and the [expression language](/docs/dsl/) — declarative guards on top of the schema.
* Entity Security Statements — the definition-scoped [Security Administration operations](/docs/developer/configuration-api-reference/) control `View`, actions, fields, responsibilities, and Assignment Rules.
* [Data transfer](/docs/developer/api-reference/operations/tags/entity-data-transfer/) — bulk import/export once the schema is in place.
# Entity search
> Configure entity search targets and execute PostgreSQL-backed SearchTerm queries through the Configuration and Runtime APIs.
Entity search is an explicit per-definition contract. The Configuration API stores a set of field and system-target paths; the Runtime API applies one literal substring term to those paths inside the normal PostgreSQL entity query.
Use the workspace API host
All `/api/workspace/...` routes on this page are relative to the target installation’s `WORKSPACE_API_BASE_URL`, never to `https://moltaro.com`. Fetch that installation’s `moltaro-config-v1.json` and `moltaro-public-v1.json` before constructing requests.
## Discover the current configuration
[Section titled “Discover the current configuration”](#discover-the-current-configuration)
Read the administrative Entity Definition detail:
```http
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}
```
The response contains:
* `SearchTargets` — resolved paths with stable field IDs, keys, display labels, field types, and optional system terminals;
* `SearchMaxPathDepth` — the effective transition limit for this installation;
* `SearchMaxTargetsPerDefinition` — the effective number of paths accepted for one definition;
* `RowVersion` — the concurrency token required by the replace operation.
Entity Definition summaries expose `SearchTargetCount`. Runtime query options include the special `SearchTerm` target only when at least one search target is configured. Do not infer availability from a field type or UI label.
## Replace all search targets
[Section titled “Replace all search targets”](#replace-all-search-targets)
Use the [`search-targets` operation](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-update-search-targets/):
```http
PUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/search-targets
```
The request is replace-all. It must include every target that should remain and the latest Entity Definition `RowVersion`:
```json
{
"SearchTargets": [
{
"Path": [
{ "FieldId": "" }
]
},
{
"Path": [
{ "FieldId": "" },
{ "SystemTarget": 0 }
]
},
{
"Path": [
{ "FieldId": "" },
{ "FieldId": "" }
]
},
{
"Path": [
{ "SystemTarget": 2 }
]
}
],
"RowVersion": ""
}
```
Use field IDs returned by the current definition. `FieldKey` is also accepted and is resolved relative to the root, related entity, or child table reached by the preceding segment. Never send a display label as a key. If both `FieldId` and `FieldKey` are present, they must identify the same field.
`SystemTarget` is numeric in JSON:
| Value | Target |
| ----- | ------------ |
| `0` | Display name |
| `1` | Number |
| `2` | Comments |
| `3` | Attachments |
Comments and Attachments are valid only when that feature is enabled on the reached entity. Reference, Inverse reference, and Table fields are intermediate segments. Valid field terminals are String, Text, Address, and File. Address searches `FullAddress`; File searches the active file name and description.
Each intermediate Reference, Inverse reference, or Table counts as one transition. The terminal does not count. Read `SearchMaxPathDepth` rather than hard-coding the normal value of three.
The replacement must not contain more than `SearchMaxTargetsPerDefinition` entries. The normal installation default is 64. Oversized API, YAML, and package-owned configurations are rejected with `moltaro.definitions.searchTargets.limit.exceeded`.
An empty replacement disables search:
```json
{
"SearchTargets": [],
"RowVersion": ""
}
```
The response is the refreshed Entity Definition detail with a new `RowVersion`. On HTTP 409, read the definition again, merge the intended complete set, and retry with the new token. Never resend a stale replace-all payload blindly.
## Execute a runtime search
[Section titled “Execute a runtime search”](#execute-a-runtime-search)
First read:
```http
GET /api/workspace/entity/{entityIdOrKey}/instances/query/options
```
When search is available, the options contain a special target whose `Reference.Special` is `0` (`SearchTerm`) and whose only operator is `9` (`Contains`). Copy the installed option rather than guessing enum values.
Send the condition through the ordinary list query:
```http
POST /api/workspace/entity/{entityIdOrKey}/instances/query
```
```json
{
"Page": 1,
"PageSize": 25,
"ArchiveMode": 0,
"Filter": {
"Operator": 0,
"Conditions": [
{
"Field": { "Special": 0 },
"Operator": 9,
"Values": [
{
"Kind": 0,
"Value": "acme 100%"
}
]
}
]
},
"Sort": null,
"Include": null
}
```
The same query model is accepted by table bootstrap endpoints. The Web Application’s `search` URL state is translated to this `SearchTerm` filter; it is not a separate search service.
If an advanced filter is also present, keep the standalone search as an outer `AND`: `SearchTerm AND (advanced A OR advanced B)`. This prevents an `OR` inside the advanced filter from broadening the search term.
## Exact semantics
[Section titled “Exact semantics”](#exact-semantics)
* Normal input is one case-insensitive literal substring containing 3 to 256 trimmed characters.
* Targets are combined with `OR`.
* `%`, `_`, and `\` are escaped and remain literal characters.
* `#number` matches only the exact root-record `Number`, while `#{record-id}` matches only the exact root-record `Id`; they may be shorter than three characters but remain capped at 256 characters.
* Every related entity hop applies its own record access and archive policy, independently from the root query’s archive mode. Active related records require ordinary view access; archived related records require read-archive access.
* Field-access rules are applied before text is allowed to match.
* A related comment or attachment matches only when its Entity owner passes the same lifecycle check plus `Comments:View` or `Attachments:View` in the captured query snapshot. Content owned by another provider is excluded.
* Changes are visible immediately after commit.
Search is not tokenized, stemmed, ranked, or eventually consistent. There is no external index, candidate threshold, rebuild endpoint, queue, or search-storage health state.
When no target is configured, query options omit `SearchTerm`, table metadata returns `FullTextSearchAvailable: false`, and a submitted SearchTerm condition fails with `moltaro.search.notAvailable`. The property name `FullTextSearchAvailable` remains for API compatibility; it does not indicate that a separate full-text engine exists.
## YAML portability
[Section titled “YAML portability”](#yaml-portability)
Current exports use strict Entity Definition YAML version 8. The mandatory `Security` member embeds the Entity’s complete current Security configuration:
```yaml
Version: 8
Security:
Format: MoltaroSecurityConfiguration
Version: 8
Statements: []
PermissionAssignments: []
Responsibilities: []
InitialAssignmentRules: []
AssignmentRules: []
SearchTargets:
- Path:
- FieldKey: Title
- Path:
- FieldKey: Customer
- SystemTarget: DisplayName
```
Portable YAML uses stable field keys; import resolves them in the reached entity or child-table scope. Versions 1 through 7 are rejected after the destructive Entity security cutover. There is no compatibility parser and legacy access modes, field rules, assignments, and inheritance are never converted. Re-export the definition as v8 and recreate its Security Statements, Permission Assignments, and Responsibilities explicitly.
Version 8 also carries structured Hierarchy Selector metadata, record-matching profiles, the Entity Manual Order capability, and typed `Special: ManualOrder` sort targets. The platform-managed backing rank storage is not exported. See [Record matching](/docs/developer/record-matching/) for profile semantics.
## Removed contracts
[Section titled “Removed contracts”](#removed-contracts)
Do not generate code or automation that uses:
* `FullTextSearchEnabled`;
* `ExcludeFromFullTextSearch`;
* full-text rebuild or reindex endpoints;
* index-storage paths, readiness checks, or polling for search consistency.
To change search, read the current Entity Definition, replace `SearchTargets`, then verify runtime query options. For configurator-facing setup, see [Record search](/docs/configuration/entity-search/); for the complete filter tree, see [Entity Instance Query](/docs/developer/entity-instance-query/).
# Entity Instance Query
> Exact Runtime API contracts for grouped filters, related paths, sorting, views, permissions, nulls, and archived references.
Use `POST /api/workspace/entity/{entityIdOrKey}/instances/query` for server-side paging, filters, sorting, and reference includes. Read the installation’s OpenAPI and query options before constructing a request. Never guess field IDs, field keys, system-field IDs, operator values, or supported targets.
This endpoint returns a flat result set. A configured [Parent Tree View runtime](/docs/configuration/hierarchies/developer-guide/) reuses the same filter and sort language but bootstraps roots and loads direct-child sibling pages through context-preserving Entity UI routes.
## Configured record search
[Section titled “Configured record search”](#configured-record-search)
The list search box uses this same query endpoint. When the Entity Definition has explicit search targets, query options expose the special `SearchTerm` field; send it as a `Contains` condition in the ordinary filter tree. The Web Application’s `search` URL state is client-side state that is translated into that condition, not a separate Runtime API route or query-string contract.
Search-target configuration, the exact condition payload, literal-substring semantics, relation and Table paths, concurrency, and removed full-text contracts are documented in [Entity search](/docs/developer/entity-search/). Normal substring terms contain 3 to 256 trimmed characters. Exact `#number` matches only `Number`, while `#{record-id}` matches only `Id`; both shortcuts may be shorter and remain capped at 256 characters.
## Related filter and sort payload
[Section titled “Related filter and sort payload”](#related-filter-and-sort-payload)
This query filters `AidRecord` through `Community -> District -> Region` and sorts by the Region display name:
```json
{
"Page": 1,
"PageSize": 25,
"ArchiveMode": 0,
"Filter": {
"Operator": 0,
"Conditions": [
{
"Target": {
"Path": [
{ "FieldId": "field-community", "FieldKey": "Community" },
{ "FieldId": "field-district", "FieldKey": "District" },
{ "FieldId": "field-region", "FieldKey": "Region" }
]
},
"Operator": 7,
"Values": [
{ "Kind": 0, "Value": "region-record-id-1" },
{ "Kind": 0, "Value": "region-record-id-2" }
]
}
],
"Groups": null
},
"Sort": [
{
"Target": {
"Path": [
{ "FieldId": "field-community", "FieldKey": "Community" },
{ "FieldId": "field-district", "FieldKey": "District" },
{ "FieldId": "field-region", "FieldKey": "Region" }
]
},
"Direction": 0
}
]
}
```
`Operator: 0` is `Eq`; `Operator: 7` is `In`. A terminal `Reference` compares record IDs, so the operands above are Region record IDs, not Community IDs and not display names. Sorting a terminal `Reference` uses its Display Name.
For a system field of a related entity, make the synthetic system field the last path segment:
```json
{
"Target": {
"Path": [
{ "FieldKey": "Community" },
{ "FieldKey": "District" },
{ "FieldId": "$system:ModifiedAt" }
]
},
"Direction": 1
}
```
Copy synthetic system IDs from installation-provided options; do not construct them from this example.
## ID and key resolution
[Section titled “ID and key resolution”](#id-and-key-resolution)
Each path segment may use `FieldId`, `FieldKey`, or both. Resolution starts on the queried entity definition. After a `Reference`, the next segment is resolved on that reference’s target definition.
If both ID and key are present, they must identify the same field. A stale ID paired with a current key is an error, not a fallback. Display labels are never field keys.
Use `Field` only for a direct, one-segment condition or sort. Use `Target.Path` for related paths. Configuration-provided runtime items expose the exact `FilterTarget` or `SortTarget` to copy.
## Groups and saved/shared views
[Section titled “Groups and saved/shared views”](#groups-and-savedshared-views)
`Filter.Operator` is the group operator (`0 = And`, `1 = Or`). `Conditions[]` and nested `Groups[]` can be combined recursively. Table saved views and shared views preserve this query structure, including every complete target path. A configured item is matched to a restored condition by the full canonical path, not by its first segment.
## Limits and supported paths
[Section titled “Limits and supported paths”](#limits-and-supported-paths)
* Filter `Target.Path`: 1 to 6 segments.
* Sort `Target.Path`: 1 to 4 segments.
* Applied `Sort`: at most 3 items.
* Intermediate segments: readable primary-table `Reference` fields only.
* Terminals: field and system-field targets supported by the query options.
* `Table` and `InverseReference` are not deep terminals.
* `RowsFilter` is a separate direct-`Table` mechanism and cannot be used as a reference-path hop.
* Statements and object-context facts use their root condition contracts; they are not transported through references.
The terminal option controls operators and operand shape. Use the returned `FilterOperators`, `FieldType`, `AllowMultiple`, reference target definition, classifier metadata, and row-filter options instead of inferring them.
### Select and enum operands use option keys
[Section titled “Select and enum operands use option keys”](#select-and-enum-operands-use-option-keys)
For a Select field, including a generated C# enum projection, send the stable string option key. Do not send the numeric enum ordinal or the display label:
```json
{
"Target": { "Path": [{ "FieldKey": "Status" }] },
"Operator": 0,
"Values": [{ "Kind": 0, "Value": "Parsed" }]
}
```
Here `"Parsed"` is the configured option key. A numeric value such as `0` is invalid even if it happens to be the enum’s current ordinal. Invalid operands return HTTP 400 with `moltaro.instances.query.operand.invalid`; provider exception details are not part of the response.
For a String or Text field whose `SemanticRole` is Markdown, the raw field `Value` remains exact Markdown source while compact read contracts may include a separate `PlainTextPreview`. Treat `CanSort=false` as authoritative: Markdown is not sortable. Use only the field option’s returned closed `FilterOperators` and never strip Markdown or render source as trusted HTML in the client.
## DateTimeOffset precision
[Section titled “DateTimeOffset precision”](#datetimeoffset-precision)
Direct Runtime API `DateTimeOffset` operands are exact instants. Send ISO 8601 with an explicit `Z` or numeric offset; equality compares the exact normalized timestamp, including seconds and microseconds.
The Web Application intentionally exposes a minute-precision filter. It keeps the readable condition in saved and shared views, but expands the executable request into exact half-open UTC ranges. For example, UI equality at `10:00` executes as `>= 10:00:00 AND < 10:01:00`, and a UI `Between` includes the complete first and last selected minutes. This is a Web Application presentation contract, not a change to direct API operator semantics. An API integration that needs minute-level behavior must construct the equivalent grouped range explicitly.
## Null, archived, and permission semantics
[Section titled “Null, archived, and permission semantics”](#null-archived-and-permission-semantics)
* If an intermediate reference is null, a deep condition does not match.
* During sorting, a broken reference chain produces a null sort value. Stable pagination still uses the server’s deterministic tie-breaker.
* Related records are evaluated independently from the root archive mode: active related records require ordinary view access, while archived related records require applicable read-archive access.
* Every hop is checked for the current caller. A filter or sort cannot grant definition, field, record, or archive access.
* Restricted related records must not be disclosed through matches, totals, ordering, lookup labels, or unavailable-target diagnostics.
Execution remains server-side. Related filters compile to the existing correlated/`EXISTS` plans and related sorts to the existing join plans. Clients must not fetch all rows for local filtering, and they do not need an N+1 read loop.
## Common errors
[Section titled “Common errors”](#common-errors)
| Symptom | Check |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Empty or over-depth path | Copy a complete target from options and check the limits above. |
| ID/key mismatch | Refresh schema/options and replace the stale segment pair. |
| Unknown segment | Resolve each segment relative to the preceding reference target. |
| Unsupported intermediate | Confirm it is a primary-table `Reference`, not a scalar, `Table`, or `InverseReference`. |
| Operator rejected | Use the terminal option’s `FilterOperators`. |
| Reference values never match | Send terminal record IDs, not labels or IDs from the root reference entity. |
| Configured target is unavailable at runtime | Test with the actual user; inspect definition, field, record, and archive permissions at every hop. |
| Duplicate Table Surface filter | Compare the complete canonical `Target.Path`; only an exact path duplicate conflicts. |
For authoring a Table Surface, start with [Table filters and sorting](/docs/configuration/table-filters-and-sorting/). For generated schemas and exact enum values, use the [Runtime API reference](/docs/developer/api-reference/) and the current installation’s OpenAPI.
# Record matching API and configuration
> Configure exact and fuzzy duplicate detection, run bounded or durable scans, and round-trip profiles through portable Entity Definition YAML.
Record matching is an entity-definition-scoped duplicate-detection contract. It supports exact normalized groups and deterministic fuzzy pairs. This page is for API clients, package authors, and configuration automation. For the user workflow, see [Duplicate detection](/docs/user/data-structure/duplicate-detection/).
## Profile contract
[Section titled “Profile contract”](#profile-contract)
A profile targets one entity definition, selects Table and/or Details launch surfaces, defines score/confidence thresholds, and contains enabled rules. Fields inside one rule use `AND`; enabled rules use `OR`.
Existing exact clients remain compatible. When `ComparisonMode` and `FuzzySettings` are omitted from a rule-field request, the field is `Exact` and the persisted profile behaves exactly as before.
Relevant enum values are:
| Contract | Values |
| --------------------------------------- | -------------------------- |
| `RecordMatchingMatchModeEnum` | `ExactNormalized`, `Fuzzy` |
| `RecordMatchingFieldComparisonModeEnum` | `Exact`, `Fuzzy` |
| `RecordMatchingFuzzyAlgorithmEnum` | `DamerauLevenshtein` |
| `RecordMatchingDiacriticModeEnum` | `Preserve`, `Ignore` |
| `RecordMatchingTokenOrderModeEnum` | `Ordered`, `AnyOrder` |
| `RecordMatchingGroupKindEnum` | `ExactSet`, `FuzzyPair` |
`FuzzySettings` has `Algorithm`, `MaxDistance`, `MinimumLength`, `Weight`, `DiacriticMode`, and `TokenOrderMode`. Fuzzy fields are limited to String or Text fields using `TextKey`. `MaxDistance` is 1–3, `Weight` is 1–100, and `MinimumLength` must be at least `2 * MaxDistance + 1` and no more than 512 Unicode runes. A fuzzy rule must contain at least one fuzzy field; exact guard fields are allowed. Exact fields reject fuzzy settings.
The compatible-field responses expose the available normalizers, `SupportsFuzzy`, and supported fuzzy algorithms so clients do not infer support from field names. Fuzzy profiles are bounded to eight enabled rules. Complete full scans additionally allow at most four fields per rule and sixteen distinct matching fields. A legacy exact profile outside the full-scan rule bound keeps its bounded and anchored behavior but cannot enqueue a complete scan.
## Normalization, distance, and score
[Section titled “Normalization, distance, and score”](#normalization-distance-and-score)
Fuzzy TextKey normalization applies Unicode FormKC, invariant rune lower-case, Unicode whitespace trim/collapse, optional canonical mark removal, and optional ordinal token sorting with duplicates preserved. Punctuation remains significant. There is no transliteration, nickname, phonetic, semantic, vector, or LLM similarity.
Distance is unrestricted metric Damerau-Levenshtein over Unicode runes. Each fuzzy field must independently pass its distance and minimum length. Field similarity is returned in basis points. A fuzzy rule’s weighted similarity is the rounded weighted mean of its field similarities; its effective score is the rounded configured score multiplied by that similarity. Exact rules keep their configured score and 10,000 similarity basis points. A group’s score is the maximum effective score among its matched rules.
Exact rules create `ExactSet` groups with two or more candidates. Fuzzy rules create `FuzzyPair` groups with exactly two candidates. Pairs are not joined into connected components. Multiple rules for the same canonical pair are returned under one group. Matched rule DTOs expose configured/effective score and similarity. Field reasons expose safe algorithm/distance/threshold/similarity/ weight/settings evidence and never expose raw or normalized matching values.
## Runtime modes
[Section titled “Runtime modes”](#runtime-modes)
### Bounded Table and Constructor run
[Section titled “Bounded Table and Constructor run”](#bounded-table-and-constructor-run)
The transient target-wide endpoint scans at most 250 actor-visible records and may return `ScanLimitReached=true`. `RecordMatchingRunRequest.Limit` continues to bound this compatibility mode. Constructor uses the same bounded contract. Fuzzy evaluation checks at most 31,125 pairs after applying exact guards. Mixed/fuzzy runs materialize at most 2,000 groups. A broader result returns HTTP 409 with `recordMatching.fuzzy.memoryBudgetExceeded`, fixed limit metadata, and no partial groups or candidate identifiers.
### Complete anchored Details run
[Section titled “Complete anchored Details run”](#complete-anchored-details-run)
The anchored endpoint ignores `RecordMatchingRunRequest.Limit`, owns a repeatable-read transaction, counts the complete actor-visible archive scope, and returns `TotalScannedRecords=TotalVisibleRecords` with `ScanLimitReached=false`. It reads at most 251 distinct matching candidate ids: up to 250 are materialized, while the 251st returns HTTP 409 with `recordMatching.anchor.tooBroad` and only `MaxCandidateCount=250` metadata.
The complete request has a five-second deadline. HTTP 503 uses `recordMatching.anchor.executionBudgetExceeded` or `recordMatching.fuzzy.executionBudgetExceeded`. Unsupported/unsafe compilation, readable over-length fuzzy values, and comparison-cap failures return HTTP 409 without groups. Caller-owned EF transactions and ambient `TransactionScope` are rejected before profile or data SQL. Client cancellation cancels the query and never returns a partial response.
### Durable complete target-wide scan
[Section titled “Durable complete target-wide scan”](#durable-complete-target-wide-scan)
`POST .../record-matching/full-scans` returns HTTP 202 and an actor-owned job. The existing Worker Host executes the scan; there is no separate scheduler or generic operation-center framework. Poll the job or latest-result endpoints, cancel queued/processing work, retry a terminal eligible job, and page groups or group candidates only after success.
The durable job pins an immutable profile/rule/field snapshot, entity/archive scope, actor proof, and idempotency fingerprint. It rehydrates authorization before execution, at publication, and on every result read. Profile, schema, security, assignment, candidate RowVersion, or archive drift invalidates the result instead of returning stale or partially authorized candidates.
The Worker uses one repeatable-read snapshot, bounded BK-trees for fuzzy rules, the existing lease/heartbeat/retry/cancel/attempt-fence lifecycle, one deadline across EF execution-strategy replay, and atomic unpublished-result publication. Hard fuzzy attempt caps are 5,000,000 distance evaluations, 250,000 intermediate pairs, 256 MiB estimated working-set delta, 512 normalized runes, and 2,048 raw characters projected per readable value. Retained-result caps are 10,000 groups, 100,000 distinct candidates, and 250,000 memberships. Exceeding a hard cap is terminal and publishes no partial result.
Terminal fuzzy job errors include:
* `recordMatching.fuzzy.valueTooLong`;
* `recordMatching.fuzzy.comparisonBudgetExceeded`;
* `recordMatching.fuzzy.memoryBudgetExceeded`;
* `recordMatching.fuzzy.executionBudgetExceeded`.
Error metadata contains only fixed server limits, never actual values, ids, or hidden counts. Transient database/network failures retain durable retry and backoff.
## Security and SQL boundary
[Section titled “Security and SQL boundary”](#security-and-sql-boundary)
Count, candidate stream, materialization, and result reads apply the requested archive scope, ordinary row access, and each matching field’s read predicate. The SQL projection uses a nested authorization-gated `CASE` before length or value work, so an unreadable large Text field is not length-inspected, detoasted, or transferred. Readable over-limit values return only an oversized flag.
Identifiers are resolved from validated runtime metadata and quoted as PostgreSQL identifiers. Values and limits are parameters. Missing, archived, and restricted records remain externally indistinguishable. Normalized values are never persisted in a job result or written to API/Worker telemetry.
## Portable Entity Definition YAML and packages
[Section titled “Portable Entity Definition YAML and packages”](#portable-entity-definition-yaml-and-packages)
Entity Definition YAML version 8 adds optional `RecordMatchingProfiles`:
* absent section preserves the definition’s existing profiles;
* present section is desired state for the profile collection;
* an explicit empty section plans removal of all profiles for that definition;
* profile, rule, and rule-field ids round-trip;
* field bindings use portable field identity and validate field key/type;
* duplicate ids/keys or unsupported fuzzy settings fail planning;
* plan selection controls profile create/update/delete operations;
* apply is transactional and idempotent, including package EntityDefinition resources.
A version 8 document without the section preserves existing profiles. Versions v1-v7 are rejected. Existing Blueprint profiles remain exact and are never silently converted to fuzzy.
Use the PascalCase v8 shape below for a mixed rule with one exact guard and one fuzzy field:
```yaml
Version: 8
RecordMatchingProfiles:
- Id: profile:contact-duplicates
Name: Contact duplicates
Key: contact-duplicates
Enabled: true
ShowOnTablePage: true
ShowOnDetailsPage: true
ActionMode: ReviewOnly
MinimumScore: 70
MediumConfidenceThreshold: 80
HighConfidenceThreshold: 95
Rules:
- Id: rule:email-name
Name: Email and similar name
Key: email-name
Enabled: true
MatchMode: Fuzzy
Score: 100
SortOrder: 0
Fields:
- Id: rule-field:email
FieldId: field:email
FieldKey: Email
Normalizer: Email
SortOrder: 0
ComparisonMode: Exact
- Id: rule-field:name
FieldId: field:name
FieldKey: Name
Normalizer: TextKey
SortOrder: 1
ComparisonMode: Fuzzy
FuzzySettings:
Algorithm: DamerauLevenshtein
MaxDistance: 1
MinimumLength: 3
Weight: 100
DiacriticMode: Preserve
TokenOrderMode: Ordered
```
`FieldId` and `FieldKey` must resolve to the same field. Exact bindings omit `FuzzySettings`; fuzzy bindings require every shown fuzzy setting.
## Generated references
[Section titled “Generated references”](#generated-references)
Use the [Runtime API reference](/docs/developer/api-reference/) for launch, anchored run, full-scan lifecycle, result paging, cancel, and retry operations. Use the [Configuration API reference](/docs/developer/configuration-api-reference/) for profile CRUD, compatible fields, and Constructor preview. Stable localized codes are listed in the [error code reference](/docs/developer/error-codes/).
# Moltaro release notes
> Versioned summaries and detailed change records for shipped Moltaro releases.
Use this index to identify the installed product version, then open that version’s detail page before changing a workspace or integration. The [documentation manifest](/docs/llms-manifest.json) identifies the exact published product version and source revision.
## Release-note policy
[Section titled “Release-note policy”](#release-note-policy)
* Every production publication has one versioned detail page, whether the Managed Apps receive a full runtime replacement or a delta update.
* Notes are prepared from the complete Git history since the previously deployed documentation source and separate user-visible behavior, database and upgrade impact, deployment actions, compatibility, and breaking changes.
* Exact migration identifiers and technical verification remain internal release evidence and are not published on version detail pages.
* Release notes explain the supported current behavior; they do not preserve obsolete execution paths or replace installation-local OpenAPI.
## Releases
[Section titled “Releases”](#releases)
### [Moltaro 0.0.32-beta](/docs/developer/release-notes/0.0.32-beta/)
[Section titled “Moltaro 0.0.32-beta”](#moltaro-0032-beta)
**Released:** 2026-08-29
This release introduces the administrable File Protection module foundation, unifies Entity UI surface management around dependency-aware diagnostics, and prevents terminal batch-upload failures from retaining unusable storage reservations.
### [Moltaro 0.0.31-beta](/docs/developer/release-notes/0.0.31-beta/)
[Section titled “Moltaro 0.0.31-beta”](#moltaro-0031-beta)
**Released:** 2026-08-28
This release makes function failures consistently diagnosable, prevents interrupted managed-file uploads from becoming stuck or ambiguously reusable, and gives Work Schedule administrators a complete actionable readiness list.
### [Moltaro 0.0.30-beta](/docs/developer/release-notes/0.0.30-beta/)
[Section titled “Moltaro 0.0.30-beta”](#moltaro-0030-beta)
**Released:** 2026-08-28
This release makes new Entity Definitions immediately usable by administrators, simplifies Work Schedule operational setup around current Entity fields, restores several Board and Activity Feed paths, and removes avoidable RabbitMQ probe load from shared runtime hosts.
### [Moltaro 0.0.29-beta](/docs/developer/release-notes/0.0.29-beta/)
[Section titled “Moltaro 0.0.29-beta”](#moltaro-0029-beta)
**Released:** 2026-08-27
This release makes high-volume Board governance predictable, restores core User integrations inside package runtimes, and streamlines Worker association and Entity Security administration in the WebApp.
### [Moltaro 0.0.28-beta](/docs/developer/release-notes/0.0.28-beta/)
[Section titled “Moltaro 0.0.28-beta”](#moltaro-0028-beta)
**Released:** 2026-08-26
This release introduces governed Work Schedule calendars, assignments, exceptions, and Worker-to-User associations, while giving managed Runtime VMs a safe external staging-volume option for backups and deployment work.
### [Moltaro 0.0.27-beta](/docs/developer/release-notes/0.0.27-beta/)
[Section titled “Moltaro 0.0.27-beta”](#moltaro-0027-beta)
**Released:** 2026-08-26
This release prevents overlapping scheduled function work, adds guided and atomic Security administration for Entity Definitions and Boards, corrects draft-specific Reference Eligibility, and increases trusted attachment batch capacity without raising ordinary user upload limits.
### [Moltaro 0.0.26-beta](/docs/developer/release-notes/0.0.26-beta/)
[Section titled “Moltaro 0.0.26-beta”](#moltaro-0026-beta)
**Released:** 2026-08-25
This release gives API-enqueued business jobs durable per-publication timeouts, makes Reference Eligibility depend on one authoritative server-evaluated Form context, and improves drawer navigation and actions across operational details.
### [Moltaro 0.0.25-beta](/docs/developer/release-notes/0.0.25-beta/)
[Section titled “Moltaro 0.0.25-beta”](#moltaro-0025-beta)
**Released:** 2026-08-24
This release improves high-volume attachment migration, makes related-create Reference Eligibility work with server-derived values, and gives administrators a clearer master-detail workspace for users, roles and groups. It also prevents stale Data Explorer reloads from corrupting a newly selected table projection.
### [Moltaro 0.0.24-beta](/docs/developer/release-notes/0.0.24-beta/)
[Section titled “Moltaro 0.0.24-beta”](#moltaro-0024-beta)
**Released:** 2026-08-24
This release adds role-aware runtime navigation, clearer fail-closed Boards states, improved role and group membership administration, consistent Card layouts, and safer repeatable historical Board imports.
### [Moltaro 0.0.23-beta](/docs/developer/release-notes/0.0.23-beta/)
[Section titled “Moltaro 0.0.23-beta”](#moltaro-0023-beta)
**Released:** 2026-08-23
This release replaces legacy Boards authorization with immediately effective Security Statements and SQL-only persisted decisions, improves user and role administration, and hardens several Entity, package and file-runtime paths.
### [Moltaro 0.0.22-beta](/docs/developer/release-notes/0.0.22-beta/)
[Section titled “Moltaro 0.0.22-beta”](#moltaro-0022-beta)
**Released:** 2026-08-21
This release makes Entity access consistently PostgreSQL-authorized across interactive and background work, adds independently persisted Related Tables to Entity forms, and improves retry safety for automation, managed uploads, record lifecycle operations, comments, and concurrent mutations.
### [Moltaro 0.0.21-beta](/docs/developer/release-notes/0.0.21-beta/)
[Section titled “Moltaro 0.0.21-beta”](#moltaro-0021-beta)
**Released:** 2026-08-20
This release restores the hottest governed Entity create, update, automation and managed-upload paths after the Unified Security cutover, makes Board item admission safely retryable, and adds configurable responsive widths to Entity Card items. It is intended to unblock everyday customer work while retaining fail-closed authorization and idempotency boundaries.
### [Moltaro 0.0.20-beta](/docs/developer/release-notes/0.0.20-beta/)
[Section titled “Moltaro 0.0.20-beta”](#moltaro-0020-beta)
**Released:** 2026-08-18
This release replaces legacy Entity authorization with Unified Security Statements, hardens collectible .NET package runtimes against memory retention, adds durable unload diagnostics, improves persisted Entity list behavior, and reduces idle RabbitMQ health-check load. The Entity security cutover requires a customer administrator action immediately after each runtime replacement.
### [Moltaro 0.0.19-beta](/docs/developer/release-notes/0.0.19-beta/)
[Section titled “Moltaro 0.0.19-beta”](#moltaro-0019-beta)
**Released:** 2026-08-14
This release adds actor-safe Reference defaults, contextual Entity UI routing, durable Net Operation Project publication diagnostics, extension-aware file policies, and more predictable responsive Card layouts for workspace users, administrators, operators, and package developers.
### [Moltaro 0.0.18-beta](/docs/developer/release-notes/0.0.18-beta/)
[Section titled “Moltaro 0.0.18-beta”](#moltaro-0018-beta)
**Released:** 2026-08-13
This release adds governed Manual Order, deterministic fuzzy duplicate detection, conditional mutation effects, safer long-running file and function operations, and more useful date and action-input defaults across Moltaro’s runtime, administration, and developer surfaces.
### [Moltaro 0.0.17-beta](/docs/developer/release-notes/0.0.17-beta/)
[Section titled “Moltaro 0.0.17-beta”](#moltaro-0017-beta)
**Released:** 2026-08-11
This release restores scheduled business-function execution after a Managed App container replacement leaves stale run history linked to an already terminal queue job. It lets blocked schedules retry safely without bypassing the active job guard or duplicating work on another live worker.
### [Moltaro 0.0.16-beta](/docs/developer/release-notes/0.0.16-beta/)
[Section titled “Moltaro 0.0.16-beta”](#moltaro-0016-beta)
**Released:** 2026-08-11
This release unblocks scheduled Package SDK attachment imports into managed object storage, adds complete and durable duplicate-detection workflows, fixes Data Explorer preview ownership and overflow behavior, and makes Runtime VM agent publication an enforced part of the production release transaction.
### [Moltaro 0.0.15-beta](/docs/developer/release-notes/0.0.15-beta/)
[Section titled “Moltaro 0.0.15-beta”](#moltaro-0015-beta)
**Released:** 2026-08-10
This release makes production Portal and Managed App hosts self-maintaining: disk-consuming release artifacts and logs are bounded, low-space operations fail before mutating a system, RabbitMQ recovers under a stable identity, and backup snapshot reconciliation is deterministic and observable. It also reorganizes public hierarchy and business-logic documentation around the tasks operators, developers, and AI agents actually perform.
### [Moltaro 0.0.14-beta](/docs/developer/release-notes/0.0.14-beta/)
[Section titled “Moltaro 0.0.14-beta”](#moltaro-0014-beta)
**Released:** 2026-08-10
This release makes declarative Entity mutations deterministic across preview, save, reload, Package SDK writes, and concurrent authorization changes; enables trusted scheduled attachment imports; fixes generated workspace File and Markdown contracts; and localizes grid-selection accessibility in every supported language.
### [Moltaro 0.0.13-beta](/docs/developer/release-notes/0.0.13-beta/)
[Section titled “Moltaro 0.0.13-beta”](#moltaro-0013-beta)
**Released:** 2026-08-09
This release strengthens governed Board workflows and historical reconciliation, makes grid and picker working state durable, improves Entity drawers and assignment visibility, exposes safer managed-upload contracts, and bounds Net Operation Project source processing with explicit limits and retryable errors.
### [Moltaro 0.0.12-beta](/docs/developer/release-notes/0.0.12-beta/)
[Section titled “Moltaro 0.0.12-beta”](#moltaro-0012-beta)
**Released:** 2026-08-08
Moltaro `0.0.12-beta` completes the managed multipart-upload path across the product, strengthens governed record matching and generated-context writes, and ships a more compact and predictable object-detail experience.
### [Moltaro 0.0.11-beta](/docs/developer/release-notes/0.0.11-beta/)
[Section titled “Moltaro 0.0.11-beta”](#moltaro-0011-beta)
**Released:** 2026-08-06
Moltaro `0.0.11-beta` makes managed S3 file-body storage the complete default for Portal-created Managed Apps, adds a resumable database-to-S3 migration workflow, and hardens Portal storage-signer recovery after a host restart.
### [Moltaro 0.0.10-beta](/docs/developer/release-notes/0.0.10-beta/)
[Section titled “Moltaro 0.0.10-beta”](#moltaro-0010-beta)
**Released:** 2026-08-06
Moltaro `0.0.10-beta` introduces managed Object Storage for large file bodies, governed Board constraints and statements, and the Operational Location foundation. It also removes ClamAV from the baseline runtime while preserving mandatory streaming integrity and content verification.
### [Moltaro 0.0.9-beta](/docs/developer/release-notes/0.0.9-beta/)
[Section titled “Moltaro 0.0.9-beta”](#moltaro-009-beta)
**Released:** 2026-08-02
Moltaro `0.0.9-beta` adds governed reference eligibility and hierarchy-aware reference selection and corrects Board Data field presentation and semantics.
### [Moltaro 0.0.8-beta](/docs/developer/release-notes/0.0.8-beta/)
[Section titled “Moltaro 0.0.8-beta”](#moltaro-008-beta)
**Released:** 2026-08-01
Moltaro `0.0.8-beta` introduces governed Markdown content, secure inline image handling, a substantially improved Data Explorer experience, and a safer interactive production release workflow.
### [Moltaro 0.0.6-beta](/docs/developer/release-notes/0.0.6-beta/)
[Section titled “Moltaro 0.0.6-beta”](#moltaro-006-beta)
**Released:** 2026-07-30
Moltaro `0.0.6-beta` improves presentation templates for Select fields and hardens C# business-logic activation, diagnostics, and persisted runtime contracts.
### [Moltaro 0.0.5-beta](/docs/developer/release-notes/0.0.5-beta/)
[Section titled “Moltaro 0.0.5-beta”](#moltaro-005-beta)
**Released:** 2026-07-29
Moltaro `0.0.5-beta` expands governed data modeling and runtime navigation, replaces the legacy Lucene search subsystem with PostgreSQL-native entity search, and improves day-to-day administration.
# Card item layout
> Configure responsive widths for fields, text blocks, and alerts on Card Surfaces.
A Card Surface can arrange its items in one, two, or three columns depending on the space available inside its current container. **Column span** lets a configurator say how many of those columns an item should use without creating separate layouts for a Drawer and a Details page.
Use a wider span for content such as a description, Markdown narrative, address, text block, or alert that needs reading space. Moltaro does not widen an item automatically because of its field type or renderer; the saved Card configuration is the source of that presentation decision.
## Configure a Card item
[Section titled “Configure a Card item”](#configure-a-card-item)
1. Open **Constructor > Entity Explorer** and select the Entity Definition.
2. Open **Surface library**, then select or create the Card Surface.
3. In **Card items**, edit a Field, Text block, or Alert item.
4. Choose **Column span** and save the item.
5. Save the Card Surface, then verify it in every Runtime screen that selects that Card, especially Drawer and Details.
Card items do not have a fixed **Column** setting. They keep their row-major order from `SortOrder` and flow into the available columns. A wider item may use the remaining space in a row or cause the next item to wrap to a new row. Form layout is different: Forms keep their own fixed Column and Column span settings against an authored Form column count.
## How the responsive span works
[Section titled “How the responsive span works”](#how-the-responsive-span-works)
The accepted authored values are `1` through `3`. A configuration with no authored value (`null` in the API) behaves as `1`; the visual editor therefore shows the effective value **1** for existing null items.
At runtime Moltaro uses the smaller of the authored span and the number of columns that currently fit the Card:
```text
effective span = min(ColumnSpan ?? 1, current Card column count)
```
| Column span | Narrow Card (1 column) | Medium Card (2 columns) | Wide Card (3 columns) |
| ------------ | ---------------------: | ----------------------: | --------------------: |
| unset or `1` | 1 | 1 | 1 |
| `2` | 1 | 2 | 2 |
| `3` | 1 | 2 | 3 |
The Card’s container width, not only the browser or device width, determines the current column count. The same saved value can therefore appear as one column in a narrow Drawer and as several columns on a full Details page.
Practical choices:
* use `1` for compact facts that can share a row;
* use `2` when an item should fill a two-column Card but may share a wider row;
* use `3` when an item should fill the row in every current Card layout.
A valid value above the current responsive column count is clamped to the available width. Values `0`, negative values, and values above `3` are invalid.
Upgrade behavior
When an installation adopts this three-column contract, persisted Card values above `3` are normalized to `3`, which preserves their current full-row rendering. Form item spans are not changed. Package and YAML sources must use `1`, `2`, or `3` before they are applied again.
## Drawer and Details behavior
[Section titled “Drawer and Details behavior”](#drawer-and-details-behavior)
The span belongs to the reusable Card Surface, not to its host. Drawer, Details, and other screens that render the selected ordinary Card use the same clamp rule. They may still show different results if they select different Card Surfaces or provide different container widths.
This setting does not reconfigure the generated Boards **Board data** section. Change an ordinary Card Surface only when its content and hosts are the target.
## Configuration API workflow for developers and agents
[Section titled “Configuration API workflow for developers and agents”](#configuration-api-workflow-for-developers-and-agents)
All routes below are relative to the installation’s `WORKSPACE_API_BASE_URL`, never to `https://moltaro.com`. The caller needs the Admin or Configurator role. Public JSON property names are PascalCase.
Read the options response before presenting or sending a value:
```text
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/card-surfaces/configuration/options
```
Its `Data.SupportedColumnSpans` value is the server-authoritative list:
```json
{
"Data": {
"SupportedColumnSpans": [1, 2, 3]
},
"Errors": [],
"Warnings": [],
"Success": true
}
```
Then use a read-modify-write-read workflow:
```text
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/card-surfaces/{surfaceKey}/configuration
PUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/card-surfaces/{surfaceKey}/configuration
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/card-surfaces/{surfaceKey}/configuration
```
PUT replaces the complete Card configuration
Preserve the current `Tabs`, every `Items` entry, and `LabelRules`; change only the intended item’s `ColumnSpan`. Sending only the abbreviated fragment below would remove the omitted configuration.
The following JSON is an item fragment for explanation, not a complete request:
```json
{
"Items": [
{
"Id": "description",
"SortOrder": 30,
"ColumnSpan": 3
}
]
}
```
After PUT, GET the configuration and verify the exact authored value. Entity Definition YAML export/import also preserves `ColumnSpan` exactly. Do not translate Card span into a fixed Column, infer width from the field type, or send a breakpoint-specific map.
Generated operation references:
* [Get Card configuration options](/docs/developer/configuration-api-reference/operations/admin-entity-ui-get-card-surface-configuration-options/)
* [Get Card configuration](/docs/developer/configuration-api-reference/operations/admin-entity-ui-get-card-surface-configuration/)
* [Replace Card configuration](/docs/developer/configuration-api-reference/operations/admin-entity-ui-put-card-surface-configuration/)
An invalid value returns the stable code `moltaro.ui.card.item.columnSpan.invalid`. See [Errors and responses](/docs/developer/errors/) for the standard error envelope and recovery rules.
## Related documentation
[Section titled “Related documentation”](#related-documentation)
* [UI surface library](/docs/user/data-structure/entity-definitions/ui-surface-library/) explains reusable Table, Card, and Form Surfaces.
* [Runtime screens](/docs/user/data-structure/entity-definitions/runtime-screens/) explains how Drawer and Details select a Card Surface.
* [Display fields](/docs/user/data-structure/entity-definitions/display-fields/) explains calculated read-time values that can be shown on Cards.
# Record search
> Configure which direct and related record content the list search box searches.
Record search is configured per entity definition. It is not a single on/off feature flag: the definition is searchable when it has at least one explicit search target, and an empty target list makes search unavailable.
In the Constructor, open **Entity Explorer**, select the entity definition, and choose **Search** in the Data model group. Each row on this tab is one path from the record being listed to the text that may produce a match.
## What a search target can contain
[Section titled “What a search target can contain”](#what-a-search-target-can-contain)
A path ends in one of these field targets:
| Target | Text that is searched |
| -------------- | ------------------------------------- |
| String or Text | The complete stored text. |
| Address | The address’s `FullAddress` value. |
| File | The active file name and description. |
The following system targets are also available:
* **Display name** — the reached record’s computed display name;
* **Number** — the reached record’s generated number;
* **Comments** — active comment text;
* **Attachments** — active attachment file names and descriptions.
**Reference**, **Inverse reference**, and **Table** are intermediate path segments rather than text targets. A Table path must continue to a concrete field in its child row. Comments and Attachments can be selected on the root record or on a record reached through Reference or Inverse reference; they are not properties of an individual Table child row.
For example:
* `Title`;
* `Customer → Display name`;
* `Orders → Comments`, where Orders is an Inverse reference;
* `Lines → Product → Category → Display name`;
* `Lines → Supporting file`.
Every Reference, Inverse reference, or Table transition counts toward the installation’s path-depth limit. The normal limit is three transitions; the terminal field does not count. The Entity Definition detail returned by the Configuration API exposes the effective value as `SearchMaxPathDepth`.
## Match behavior
[Section titled “Match behavior”](#match-behavior)
Normal input is one literal, case-insensitive substring containing 3 to 256 trimmed characters. All configured targets are tried, and a record matches when any one target contains the complete input. `%`, `_`, and `\` have no wildcard meaning and are matched literally. Search is not tokenized, stemmed, ranked, or language-specific.
Changes to records, comments, attachments, and file metadata are visible in search immediately after the successful change. There is no search-index rebuild and no synchronization delay.
The `#number` shortcut finds the root record by its exact generated number. The `#{record-id}` shortcut finds it by its exact technical ID. The shortcuts do not cross-match the other identity. They may be shorter than three characters but remain capped at 256 characters. A definition still needs at least one configured target before its runtime search control is available.
## Access and archives
[Section titled “Access and archives”](#access-and-archives)
Search never expands access:
* the caller must be allowed to read every field and related entity used by the path;
* record access is applied again at every entity transition;
* related records are evaluated independently from the root archive mode: active targets require ordinary view access and archived targets require read-archive access;
* inaccessible related text cannot be inferred from matches or totals.
This means two users can legitimately receive different results for the same term.
## Safe configuration changes
[Section titled “Safe configuration changes”](#safe-configuration-changes)
The server rejects duplicate, incomplete, unsupported, or over-depth paths. Remove a dependent search target before deleting a participating field, changing its path-shaping type or relation, disabling Comments or Attachments on an entity used by that target, or deleting an association whose generated relation field a search path travels through.
Configured paths are listed in a fixed order derived from the schema: paths follow the field order of the entity each segment belongs to, paths through the same relation stay together, and record content comes last. The order is not editable on its own; change the field order of the entity to change it. The same order is used by the Entity Definition YAML export.
Entity Definition YAML export uses version 8 and includes `SearchTargets`. Versions 1 through 7 are rejected. Definitions upgraded from an older installation start with no targets until a configurator chooses them explicitly.
For API payloads, concurrency rules, and runtime query examples, continue with [Entity search for developers and agents](/docs/developer/entity-search/).
# Table filters and sorting
> Configure direct and related-field filters and sorting on Entity Instance tables.
Table surfaces can filter and sort by a field reached through a chain of ordinary references. A record type does not need separate `Community`, `District`, and `Region` fields to expose all three controls.
For an `AidRecord` that stores only `Community`, a table may expose:
* `Community`
* `Community / District`
* `Community / District / Region`
The complete query path is the target identity. These three targets may coexist because their paths differ, even though they share the same first field. Only an exact duplicate of the complete path is rejected.
When the Table Surface uses [Parent Tree View](/docs/configuration/hierarchies/configuring-parent-tree-view/), each root or child sibling set is sorted independently by the server. Search and filters retain visible ancestor context; changing the query reloads roots and branches.
## Read supported targets first
[Section titled “Read supported targets first”](#read-supported-targets-first)
Read the installation-provided options before writing configuration:
```text
GET /api/workspace/admin/entity-definitions/{aidRecordDefinitionId}/ui/table-surfaces/configuration/options
```
`FilterTargets[]` and `SortTargets[]` contain human-readable labels, stable technical keys, field metadata, operators, and the complete typed path. Match the desired labels and copy the returned `Target` or `SortTarget` object. Do not guess record IDs, field IDs, field keys, or enum values.
A field segment may contain both `EntityFieldDefinitionId` and `EntityFieldKey`. When both are supplied, they must identify the same field. Each segment after the first is resolved relative to the entity definition targeted by the preceding reference.
## Configure three filters with one root
[Section titled “Configure three filters with one root”](#configure-three-filters-with-one-root)
The following abbreviated request shows the relevant table properties. The IDs are examples; copy the real targets from your options response.
```json
{
"FilterItems": [
{
"Target": {
"TargetKind": 0,
"Path": [
{ "EntityFieldDefinitionId": "field-community", "EntityFieldKey": "Community" }
]
},
"Label": "Community",
"Visible": true,
"VisibleByDefault": true,
"ReferenceInputType": 2,
"ReferenceRendererType": 0,
"ReferencePath": [],
"SortOrder": 10
},
{
"Target": {
"TargetKind": 0,
"Path": [
{ "EntityFieldDefinitionId": "field-community", "EntityFieldKey": "Community" },
{ "EntityFieldDefinitionId": "field-district", "EntityFieldKey": "District" }
]
},
"Label": "District",
"Visible": true,
"VisibleByDefault": true,
"ReferenceInputType": 2,
"ReferenceRendererType": 0,
"ReferencePath": [],
"SortOrder": 20
},
{
"Target": {
"TargetKind": 0,
"Path": [
{ "EntityFieldDefinitionId": "field-community", "EntityFieldKey": "Community" },
{ "EntityFieldDefinitionId": "field-district", "EntityFieldKey": "District" },
{ "EntityFieldDefinitionId": "field-region", "EntityFieldKey": "Region" }
]
},
"Label": "Region",
"Visible": true,
"VisibleByDefault": true,
"ReferenceInputType": 2,
"ReferenceRendererType": 0,
"ReferencePath": [],
"SortOrder": 30
}
],
"SortItems": [
{
"Target": {
"Path": [
{ "EntityFieldDefinitionId": "field-community", "EntityFieldKey": "Community" },
{ "EntityFieldDefinitionId": "field-district", "EntityFieldKey": "District" },
{ "EntityFieldDefinitionId": "field-region", "EntityFieldKey": "Region" }
]
},
"Label": "Region",
"Visible": true,
"SortOrder": 10
}
],
"DefaultSort": [
{
"Target": {
"Path": [
{ "EntityFieldDefinitionId": "field-community", "EntityFieldKey": "Community" },
{ "EntityFieldDefinitionId": "field-district", "EntityFieldKey": "District" },
{ "EntityFieldDefinitionId": "field-region", "EntityFieldKey": "Region" }
]
},
"Direction": 0,
"SortOrder": 10
}
]
}
```
Use the other required properties from the current table configuration when sending the replace request.
## `Target.Path` is not `ReferencePath`
[Section titled “Target.Path is not ReferencePath”](#targetpath-is-not-referencepath)
These properties solve different problems:
* `Target.Path` identifies the terminal field used by the server-side query. In the Region filter above, the picker values are Region record IDs.
* `ReferencePath` controls breadcrumb rendering inside a reference value picker. It never changes which field is filtered.
Changing `ReferencePath` cannot turn a Community filter into a Region filter. For a related target, copy the full `Target.Path` from `FilterTargets[]`, then choose `ReferencePath` independently from that target’s `ReferencePathOptions[]`.
## Validation and limits
[Section titled “Validation and limits”](#validation-and-limits)
* Filter paths contain 1 to 6 segments.
* Sort paths contain 1 to 4 segments.
* A request may apply at most 3 sort items.
* Every intermediate segment must be a readable, primary-table `Reference`.
* `Table` and `InverseReference` are not valid path terminals here.
* Statement and object-context-fact targets are root-only.
* A related system field is terminal, for example three reference segments followed by `{ "SystemField": 1 }` for the related record’s Display Name.
The terminal field determines the available operators and value editor. Reference `Eq` and `In` operands are IDs of records in the terminal entity definition.
## Read back and verify
[Section titled “Read back and verify”](#read-back-and-verify)
After `PUT`, read the table configuration again and confirm all paths:
```text
GET /api/workspace/admin/entity-definitions/{aidRecordDefinitionId}/ui/table-surfaces/{surfaceKey}/configuration
```
Then open the table as an ordinary user:
1. Apply District and Region filters and verify rows and total count.
2. Sort by Region and verify visible order and pagination.
3. Confirm restricted related fields or records do not become visible through target labels, matches, or counts.
4. Clear temporary filters and restore the intended default view.
For exact Runtime API payloads, see [Entity Instance Query](/docs/developer/entity-instance-query/).
# .NET reference for workspace C#
> Curated machine-readable XML reference and injectable service catalog for the Net Operation Project.
Moltaro publishes a curated .NET reference for the C# surface supported inside the workspace Net Operation Project. It is documentation, not an assembly download and not the broader NetPackage surface.
The exact contract of an installation is available from:
```text
GET ${WORKSPACE_API_BASE_URL}/api/workspace/admin/net-operation-project/developer-surface
```
`WORKSPACE_API_BASE_URL` is the API host recorded in the downloaded workspace-specific agent guide. This endpoint is not served by `moltaro.com`; see [Connect to a workspace API](/docs/developer/workspace-api-connection/).
That response lists supported assembly versions and every supported constructor-injected type, including its purpose, lifetime, module key, `ServiceKind`, preferred status, supported function contracts, transaction semantics, and documentation URL. It also confirms that the current generated workspace contract is automatically present in language-service and build snapshots.
## Machine-readable files
[Section titled “Machine-readable files”](#machine-readable-files)
* [Reference manifest](/docs/api/dotnet/reference-manifest.json) — assembly version, SHA-256 checksum, member count, and included CLR types.
* [Core SDK XML](/docs/api/dotnet/Moltaro.Package.NET.xml)
* [Boards XML](/docs/api/dotnet/Moltaro.Package.NET.Boards.xml)
* [Currency Rates XML](/docs/api/dotnet/Moltaro.Package.NET.CurrencyRates.xml)
* [Entitlement Operations XML](/docs/api/dotnet/Moltaro.Package.NET.EntitlementOperations.xml)
* [File Protection XML](/docs/api/dotnet/Moltaro.Package.NET.FileProtection.xml)
* [Work Schedule XML](/docs/api/dotnet/Moltaro.Package.NET.WorkSchedule.xml)
The XML files use the standard C# compiler documentation format. Agents can join a `` entry to a symbol returned by completions, hover, or signature help. The manifest checksum lets tooling detect a changed reference without downloading all XML files.
## Deliberate boundary
[Section titled “Deliberate boundary”](#deliberate-boundary)
The reference includes function and data-source contracts, supported core services, dynamic-entity runtime services, preferred Boards, Entitlement, and Work Schedule application-automation services, their advanced direct-DB counterparts, Currency Rates cached lookup, and the request/result/enum types needed by those contracts. Application automation is the recommended Net Operation Project surface and is marked `ApplicationAutomation`; lower-level package services are marked `TrustedDirectDb`. The FP-01 File Protection status facade is read-only and is available to API-hosted `Action`, `Command`, and `HttpEndpoint` functions; Worker-hosted `Job` and `TriggerHandler` functions are not advertised until File Protection is registered in Worker.
It excludes package identity, package-owned schema authoring, model-building, manifests, migrations, install lifecycle, and host-only orchestration services. Public CLR visibility is not a support promise; use `developer-surface` as the authoritative allow-list.
# Boards configuration walkthrough
> Enable Boards, create a board, bind a record type target, define statuses and transitions, and activate — through the Configuration API.
This walkthrough builds a working process board over the `SupportTicket` record type from the [Configuration quickstart](/docs/developer/configuration-quickstart/): a triage board where new tickets land in **Triage**, move to **In progress**, and finish in **Resolved**. Everything happens through the Configuration API, so the same flow works for an AI agent with a service-account key.
All examples were executed against a real Moltaro installation; response bodies are real, trimmed for length. For what boards are and how users work with them, see the [Boards user documentation](/docs/user/boards/); for the complete contract, see the [Configuration API reference](/docs/developer/configuration-api-reference/).
Workspace URL
Send the `/api/workspace/...` requests below to the target installation’s `WORKSPACE_API_BASE_URL`, not to `moltaro.com`. If the workspace URL and API key have not been provided, start with [Connect to a workspace API](/docs/developer/workspace-api-connection/).
An agent starts only with the prepared workspace guide, URL, key, and roles. If access or module administration is missing, ask the user to complete it in the Portal or administrative interface. Do not provision or elevate the agent through these APIs. Follow [Reliable API automation](/docs/developer/reliable-api-automation/) for idempotent discovery, conflicts, and safe mutation rules.
One convention to know up front: every board administration mutation returns the **full board administration model** — the created status, transition, or target definition appears inside its `Statuses`, `Transitions`, or `TargetDefinitions` array rather than as the response root. Read the ids from there.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* **Admin** or **Configurator** role for enabling the module and creating boards. Per-board changes are also open to subjects granted the board-scoped **Manage configuration** permission.
* An existing entity definition to bind — this page uses `SupportTicket` (id `pYZqCLZWb0K1`) from the [Configuration quickstart](/docs/developer/configuration-quickstart/).
## Step 1 — Enable the Boards module
[Section titled “Step 1 — Enable the Boards module”](#step-1--enable-the-boards-module)
Boards ship disabled. `POST /api/workspace/admin/boards/enable` ([operation](/docs/developer/configuration-api-reference/operations/admin-boards-administration-enable/)) turns the module on for the workspace; the body is optional:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/boards/enable \
-H "Authorization: Bearer " -H "Content-Type: application/json" -d '{}'
```
`GET /api/workspace/admin/boards/setup` ([operation](/docs/developer/configuration-api-reference/operations/admin-boards-administration-setup/)) reports the module state at any time — useful as the idempotent first call of an automation.
## Step 2 — Create the board
[Section titled “Step 2 — Create the board”](#step-2--create-the-board)
`POST /api/workspace/admin/boards/boards` ([operation](/docs/developer/configuration-api-reference/operations/admin-boards-administration-create-board/)). The `Key` must start with a letter and may contain only ASCII letters, numbers, and underscores (`support-triage` would be rejected with `moltaroBoards.validation.boardKeyInvalid`):
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/boards/boards \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "Key": "support_triage", "DisplayName": "Support triage",
"Description": "Incoming ticket triage and resolution.",
"BoardType": 0, "CreateDefaultStatuses": false }'
```
```json
{
"Data": {
"Id": "zuq5JQx4kLF1",
"Key": "support_triage",
"DataEntityDefinitionId": "wJ8wB2plyd7F",
"DisplayName": "Support triage",
"IsActive": false,
"BoardType": 0,
"AllowedActions": { "CanUpdate": true, "CanActivate": false, "CanManageConfiguration": true },
"TargetDefinitions": [],
"Statuses": [],
"Transitions": []
},
"Success": true
}
```
`BoardType: 0` is a Kanban board (`1` adds planned operating cycles and then requires `BoardCycleActivationMode`). `CreateDefaultStatuses: false` starts with an empty status set so this walkthrough can build the process explicitly; with `true` the board is created with a ready Initial/Active/Terminal status trio instead.
All starter display metadata is created in the workspace locale: status labels, the Board Data entity and its Subject and Description fields, system link types, and initial-mapping destination labels. Their technical keys remain stable (`init`, `active`, `terminal`, `child`, `related`, `Subject`, and `Description`).
The response shows what a board brings along: it is **inactive** (invisible to end users until activated — which is what makes the configure-then-activate flow safe against a live workspace), it owns a board data model (`DataEntityDefinitionId` — process-specific fields that live on the board item rather than the record, see [board data fields](#board-data-fields) below). Board Security starts empty: configure Responsibilities, Access Policies, and Permissions explicitly.
## Step 3 — Activating too early fails
[Section titled “Step 3 — Activating too early fails”](#step-3--activating-too-early-fails)
Activation validates the configuration, so an agent can probe readiness honestly. Activating right after creation:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/boards/boards/zuq5JQx4kLF1/activate \
-H "Authorization: Bearer " -H "Content-Type: application/json" -d '{}'
```
```json
{
"Data": null,
"Errors": [
{ "Code": "moltaroBoards.domain.initialStatusRequired",
"Message": "An active board must have exactly one active, non-deleted initial status.",
"Field": "Initial", "Type": 1, "Severity": 2 },
{ "Code": "moltaroBoards.domain.terminalStatusRequired",
"Message": "An active board must have at least one active, non-deleted terminal status.",
"Field": "Terminal", "Type": 1, "Severity": 2 }
],
"Success": false
}
```
Activation requires exactly one initial status and at least one terminal status. Transitions are not an activation requirement — but without them no status change is allowed at runtime, so a useful board defines both.
## Step 4 — Bind the record type target
[Section titled “Step 4 — Bind the record type target”](#step-4--bind-the-record-type-target)
A **target definition** connects the board to the records it manages. `POST /api/workspace/admin/boards/boards/{boardId}/target-definitions` ([operation](/docs/developer/configuration-api-reference/operations/admin-boards-administration-create-target-definition/)):
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/boards/boards/zuq5JQx4kLF1/target-definitions \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "TargetKind": 0, "TargetModelId": "pYZqCLZWb0K1" }'
```
```json
{
"Data": {
"Id": "zuq5JQx4kLF1",
"TargetDefinitions": [
{ "Id": "ojmxCpYFwfCY", "TargetKind": 0, "TargetModelId": "pYZqCLZWb0K1",
"IsActive": true, "RepeatPolicy": 0,
"RowVersion": "1c0b2008-f9da-46ff-96ea-8a85c683843c" }
]
},
"Success": true
}
```
`TargetKind: 0` binds an entity definition; `TargetModelId` is the entity definition id. The created target definition id (`ojmxCpYFwfCY` here) comes back inside `TargetDefinitions` — the runtime item APIs will need it. One board can carry several target definitions, so records of different types share one process.
## Step 5 — Create the statuses
[Section titled “Step 5 — Create the statuses”](#step-5--create-the-statuses)
`POST /api/workspace/admin/boards/boards/{boardId}/statuses` ([operation](/docs/developer/configuration-api-reference/operations/admin-boards-administration-create-status/)), one call per status. `MetaType` gives each status its role in the process: `0` Initial (new items land here), `1` Active, `2` Terminal:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/boards/boards/zuq5JQx4kLF1/statuses \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "DisplayName": "Triage", "MetaType": 0, "SortOrder": 1 }'
```
| Status | Request body | Created id |
| ----------- | ----------------------------------------------------------------- | -------------- |
| Triage | `{ "DisplayName": "Triage", "MetaType": 0, "SortOrder": 1 }` | `2FEzzRX4vMNg` |
| In progress | `{ "DisplayName": "In progress", "MetaType": 1, "SortOrder": 2 }` | `x2lpSYpWQ3q6` |
| Resolved | `{ "DisplayName": "Resolved", "MetaType": 2, "SortOrder": 3 }` | `gc3NgI61wO4D` |
As with every board mutation, each response is the full board model — the new status id appears in `Data.Statuses`.
## Step 6 — Create the transitions
[Section titled “Step 6 — Create the transitions”](#step-6--create-the-transitions)
Transitions define which status moves are allowed. `POST /api/workspace/admin/boards/boards/{boardId}/transitions` ([operation](/docs/developer/configuration-api-reference/operations/admin-boards-administration-create-transition/)) takes the two status ids from the previous step:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/boards/boards/zuq5JQx4kLF1/transitions \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "FromBoardStatusId": "2FEzzRX4vMNg", "ToBoardStatusId": "x2lpSYpWQ3q6" }'
```
Create one per allowed move — here `Triage → In progress` and `In progress → Resolved`. Moves without a matching transition are rejected at runtime with `moltaroBoards.runtime.transitionNotFound`, and the runtime API exposes the allowed set per item through `move-options`, so an agent never has to guess.
### Require links before a transition
[Section titled “Require links before a transition”](#require-links-before-a-transition)
`LinkPrerequisites` can require active item links before a transition is allowed. A prerequisite fixes the link system kind (`0` Child, `1` Related), its direction relative to the item being moved (`0` outgoing, `1` incoming), the target Board and exact target definition, and a minimum/optional maximum count. For example, this transition requires one incoming Child link from an item on the specified target definition:
```json
{
"FromBoardStatusId": "2FEzzRX4vMNg",
"ToBoardStatusId": "x2lpSYpWQ3q6",
"LinkPrerequisites": [
{
"LinkType": 0,
"Direction": 1,
"TargetBoardId": "zuq5JQx4kLF1",
"TargetBoardTargetDefinitionId": "ojmxCpYFwfCY",
"MinimumCount": 1,
"MaximumCount": 1
}
]
}
```
The target Board and target definition must exist, be active, and match each other. Duplicate scopes and invalid count ranges are rejected. On update, omit `LinkPrerequisites` to preserve them or send an empty array to clear them.
## Step 7 — Configure Board Security
[Section titled “Step 7 — Configure Board Security”](#step-7--configure-board-security)
Board Security is empty on a new board and therefore fails closed. Before the runtime walkthrough, create an enabled `BoardItem` Security Statement and assign the permissions used by that walkthrough to it. Start by reading the shared concurrency token and the server-owned permission catalog:
```bash
curl -s https://ops.example.com/api/workspace/admin/boards/boards/zuq5JQx4kLF1/security \
-H "Authorization: Bearer "
curl -s https://ops.example.com/api/workspace/admin/boards/boards/zuq5JQx4kLF1/security/profiles/BoardItem/permissions \
-H "Authorization: Bearer "
```
Create a Statement for the actors that will operate this example. The source below deliberately limits the walkthrough to the workspace `ADMIN` role; use the exact role design required by your workspace instead:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/boards/boards/zuq5JQx4kLF1/security/profiles/BoardItem/statements \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d "{ \"OperationKey\": \"ae8fa130-25eb-4570-ab1c-ed372eef8380\",
\"RowVersion\": \"00000000-0000-0000-0000-000000000000\",
\"Key\": \"RuntimeAdmin\", \"Source\": \"HAS_ROLE('ADMIN')\", \"Enabled\": true }"
```
Then create one Permission Assignment per required catalog entry. Use each entry’s returned `PermissionId`, a unique assignment `Key` and `OperationKey`, and the latest `RowVersion` returned by the previous mutation:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/boards/boards/zuq5JQx4kLF1/security/profiles/BoardItem/permission-assignments \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "OperationKey": "760eb25c-8f36-4b3c-9c0f-da1945750c8a",
"RowVersion": "",
"Key": "RuntimeAdminView", "PermissionId": "Resource|View|",
"StatementKey": "RuntimeAdmin" }'
```
Repeat that assignment for `AddItem`, `MoveItem`, `EditBoardData`, `ViewAuditTrail`, `ManageItemLinks`, `Target:View`, and the exact `UseTransition` entries returned for the two transitions. These entries match the action flags and link-candidate call shown in the runtime walkthrough. If you extend the example to change custom Board Data fields, also assign `Field:Write` for each changed field. The target Entity Definition must separately grant the same actor its own `Create`, `View`, and required field permissions, as described in [Configure Entity Security](/docs/developer/configuration-quickstart/#step-7--configure-entity-security). Responsibilities are optional: omit them when this board has no named responsibility assignments.
## Step 8 — Activate
[Section titled “Step 8 — Activate”](#step-8--activate)
With statuses, process wiring, and explicit Security in place, activation succeeds and the same board model comes back with `IsActive: true`:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/boards/boards/zuq5JQx4kLF1/activate \
-H "Authorization: Bearer " -H "Content-Type: application/json" -d '{}'
```
```json
{
"Data": {
"Id": "zuq5JQx4kLF1",
"Key": "support_triage",
"IsActive": true,
"AllowedActions": { "CanActivate": false, "CanDeactivate": true }
},
"Success": true
}
```
The board is now live for actors allowed by the configured Statements and Permissions. Continue with the [Boards runtime walkthrough](/docs/developer/boards-runtime/) to create and move items through the public API.
## Board data fields
[Section titled “Board data fields”](#board-data-fields)
Process-specific fields — an escalation flag, a triage note — belong to the board item, not the record. They live on the board-owned data model (`DataEntityDefinitionId` above) and are managed with `POST /api/workspace/admin/boards/boards/{boardId}/data-model/fields` ([operation](/docs/developer/configuration-api-reference/operations/admin-boards-administration-create-board-data-field/)), which reuses the exact field request shape from the [Configuration quickstart](/docs/developer/configuration-quickstart/#step-2--add-schema-fields). The record keeps its own schema; the board carries the process context.
## Board Statements
[Section titled “Board Statements”](#board-statements)
Board Statements are reusable Board-owned boolean expressions. Configure them through `/api/workspace/admin/boards/boards/{boardId}/statements`; the server-generated `GET .../authoring` response is the authoritative set of roots, typed paths, operators, functions, variables, examples, and diagnostic codes for the chosen optional target definition.
Use `POST .../validate` before create or update, and use `POST .../preview` with an explicit proposed values/collections payload when an integration needs a deterministic authoring check. Create and update persist source, normalized expression, dependency metadata, eligibility, fingerprints, and `RowVersion`. Installation package export persists portable source and recompiles it against the destination workspace during apply.
See the complete [Board Statement DSL](/docs/dsl/board-statements/) reference. Statements are not runtime gates by themselves.
## Board Constraints and Bindings
[Section titled “Board Constraints and Bindings”](#board-constraints-and-bindings)
Board Constraint configuration is available through the Board administration screen and the Configuration API. A Constraint owns its safe failure message; each ordered Binding selects `Transition`, `EnterStatus`, `ExitStatus`, or `StatusInvariant`, and combines a non-empty ordered Statement set with `And` or `Or`.
```text
GET /api/workspace/admin/boards/boards/{boardId}/constraints
POST /api/workspace/admin/boards/boards/{boardId}/constraints
PUT /api/workspace/admin/boards/boards/{boardId}/constraints/{constraintId}
DELETE /api/workspace/admin/boards/boards/{boardId}/constraints/{constraintId}
POST /api/workspace/admin/boards/boards/{boardId}/constraints/reorder
POST /api/workspace/admin/boards/boards/{boardId}/constraints/bindings/validate
POST /api/workspace/admin/boards/boards/{boardId}/constraints/{constraintId}/bindings
PUT /api/workspace/admin/boards/boards/{boardId}/constraints/{constraintId}/bindings/{bindingId}
DELETE /api/workspace/admin/boards/boards/{boardId}/constraints/{constraintId}/bindings/{bindingId}
POST /api/workspace/admin/boards/boards/{boardId}/constraints/{constraintId}/bindings/reorder
```
Validation returns configuration health separately from runtime availability. Healthy `Transition`, `EnterStatus`, and `ExitStatus` Bindings report runtime availability and can be enabled. A Constraint can be enabled only after it has at least one enabled, healthy Binding; disabling or deleting its last enforcing Binding is rejected. `IsEnforcing` requires an enabled Constraint, an enabled Binding, valid configuration, an available trigger runtime, and an active Board.
`StatusInvariant` Bindings can be enabled when every referenced Statement is invariant-safe. Moltaro enforces them against the complete proposed Target or Board Data aggregate before supported Entity API, NetOperationProject, and Package SDK writes commit. Package YAML applies the same health and activation checks and never silently downgrades an enabled definition.
The Board resource keeps portable Statement source and Binding references. A minimal fragment is shown below; use stable ids within the resource and let the installer recompile the expression for the destination workspace.
```yaml
Statements:
- Id: owner-present
Name: OwnerPresent
DisplayName: Owner is present
BoardTargetDefinitionId: null
Expression: BoardData.Owner IS NOT NULL
SortOrder: 0
IsEnabled: true
Constraints:
- Id: ready-requires-owner
Name: ReadyRequiresOwner
DisplayName: Ready requires an owner
FailureMessage: Select an owner before moving this item to Ready.
SortOrder: 0
IsEnabled: true
Bindings:
- Id: enter-ready
TriggerType: EnterStatus
BoardStatusId: ready
EvaluatorType: Statements
LogicalOperator: And
SortOrder: 0
IsEnabled: true
BoardStatementIds:
- owner-present
```
Package validation, planning, and apply reject unknown Statement ids, invalid trigger shapes, missing target coverage, unhealthy expressions, and active configuration that cannot enforce. Export writes current portable source; it does not serialize a compiled plan or retain a destination-specific schema fingerprint as executable state.
See [Board constraints](/docs/user/boards/constraints/) for the product model and the event/status-invariant enforcement boundaries.
## Board Security
[Section titled “Board Security”](#board-security)
Board Security uses the same three administration concepts as Entity Security:
* **Responsibilities** define optional named assignments for Board Items or Board Statuses and the rules that may change them;
* **Access Policies** project one reusable Security Statement condition plus all exact Permissions assigned to it and use atomic Plan/Apply;
* **Permissions** expose inherited field access, explicit field refinements and exact transition coverage. Multiple Statements assigned to a Permission use OR semantics.
Raw Security Statement and Permission Assignment operations remain available through the Configuration API as an immediate Advanced surface.
An empty configuration fails closed for Board Item and direct Board Status access.
Board Item collections, search, saved filters, link candidates and cycle item counts evaluate `BoardItem.View` before count, ordering and pagination. Direct reads use the same policy. Owner, Admin and Configurator can manage Board configuration, but those roles do not bypass Board Item data authorization.
Runtime operations compose `View` with one exact Permission: for example, `MoveItem` and `UseTransition:` for a configured transition, or `EditBoardData` plus each changed `Field:Write` Permission for Board Data. Responsibility changes are different: they require the resource profile’s `View` and an active Assignment Rule for the exact responsibility key and action. Board cycle administration is Owner/Admin or `ManageCycles`; Configurator has no implicit cycle authority.
Target display and Security conditions are separate. Cards project the target selected by `BoardTargetDefinition` and require `BoardItem.View`, the separate `Target:View` Permission, and that target resource’s own access. Inside a Security Statement, `target(, '').` selects one exact target type; `target.` selects the first configured Target Definition by `SortOrder`, then `Id`. Predicate traversal does not require or grant `Target:View`.
Board status columns remain part of the process shell. A Board Item requires `BoardItem.View`; it does not additionally require `BoardStatus.View`. `BoardStatus.View` governs direct status responsibility and history access.
Workspace C# automation and background Board work enter the same captured authorization scope as HTTP operations. A confirmed trusted system invocation may use the reserved system actor identity, but it receives no automatic Permission and must satisfy the configured Statements. A package host without the Moltaro Boards evaluator fails closed.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Boards runtime walkthrough](/docs/developer/boards-runtime/) — items, moves, and process collaboration through the public API.
* Board Security — configure Responsibilities, Access Policies, and Permissions through the [Configuration API reference](/docs/developer/configuration-api-reference/).
* Due-date and attention-signal policies — per-board operational policies in the same administration group.
# Boards runtime walkthrough
> Discover a board, read the create context, create an item, then validate and execute a move through the public API.
This walkthrough operates the **Support triage** board built in the [Boards configuration walkthrough](/docs/developer/boards-configuration/): discover the board, create a ticket directly onto it, and move the item through the process. Everything uses the public integration API, so it works with any authenticated caller the board permissions allow — including a service-account key.
All examples were executed against a real Moltaro installation; response bodies are real, trimmed for length. The complete contract is under the [Boards runtime tag](/docs/developer/api-reference/operations/tags/boards-runtime/) of the API reference.
Workspace URL
The public documentation host is not the runtime API host. Resolve every `/api/workspace/...` route below against the target installation’s `WORKSPACE_API_BASE_URL`; see [Connect to a workspace API](/docs/developer/workspace-api-connection/).
The workspace owner supplies the guide, URL, key, and permissions through the Portal or administrative handoff. An agent does not create or elevate its own account. Apply the row-version, retry, and verification rules in [Reliable API automation](/docs/developer/reliable-api-automation/).
## Runtime authorization
[Section titled “Runtime authorization”](#runtime-authorization)
Board access has two independent gates. `Boards:ACCESS` admits the caller to the module and Board shell. Every returned Board Item must also satisfy that Board’s `BoardItem.View` Security Permission. Moltaro applies the View predicate before count, sorting, cursor or offset, and page limit, so totals and cursors describe only items the caller may read. Status columns remain visible process structure; `BoardStatus.View` is not an additional Board Item predicate.
An ordinary item mutation requires pre-change `BoardItem.View` plus its exact Permission. A move also requires `MoveItem`; when it uses a configured transition it additionally requires `UseTransition:`. Board Data updates require `EditBoardData` and `Field:Write` for every changed field. Adding an item evaluates `AddItem` against the proposed status, target, Board Data and due date, applies Initial Assignment Rules, and requires final `BoardItem.View` before commit.
Target values are not disclosed by a successful Board Item predicate alone. Projection requires `BoardItem.View`, Board `Target:View`, the target resource’s own `View`, and that resource’s field-safe projection. Typed `target(...)` paths inside a Security Statement are predicate facts and do not themselves grant target access.
## Calling Boards from workspace C# automation
[Section titled “Calling Boards from workspace C# automation”](#calling-boards-from-workspace-c-automation)
For an Action, TriggerHandler, Command, Job, or HttpEndpoint in the Net Operation Project, prefer `IBoardAutomationCommandService` and `IBoardAutomationQueryService` from `Moltaro.Package.NET.Boards.Automation`. They are application-level facades: `AddItemAsync` creates Board Data internally and performs target/repeat, Security and transition rules, audit/business events, resource events/outbox, and attention projections. The caller supplies a `BoardRuntimeTargetRef` and a caller-owned UUID `OperationKey`, not `BoardRuntimeActor` or `DataRecordId`.
Ordinary calls authorize and audit the original actor captured by the host. Only a runtime-confirmed trusted system invocation may use `moltaro-system-automation`; naming that actor in package code does not create trusted provenance. Both actor forms evaluate the same Board Security Statements and exact Permissions, and the system actor receives no implicit access. Calls execute in a separate application scope/transaction, and a successful return means that main transaction committed. Ordinary facade mutations deliberately have the same add/repeat semantics as the Runtime API and do not create an automation receipt. After an unknown ordinary add outcome, query the open item before deciding whether to start a new logical admission. Retry the exact same add with its original `OperationKey`; an exact replay returns the original item without a duplicate, while changed-payload reuse returns an idempotency conflict. The replay scope includes the captured actor, board, and target definition and does not bypass authorization. When a failed facade result should fail the containing function, call `result.ThrowIfFailed()`. This raises a bounded SDK exception that preserves the first platform error code and its safe message in Function Operations; wrapping `Errors` in a generic exception discards that reason code.
Clients built from the earlier add-item contract must regenerate from the current OpenAPI document and replace `MutationIdempotencyKey` with the required UUID `OperationKey`. Arbitrary string keys are no longer accepted. Package authors must also rebuild against the current Boards Package SDK and pass the UUID as the first `AddBoardAutomationItemRequest` constructor argument. Call a Board command before staging or saving Entity or owned-table changes in the same Net Operation Project transaction. Otherwise the facade fails before opening its separate scope with `moltaroBoards.runtime.entityMutationBeforeBoardCommandUnsupported`; start a new transaction rather than retrying from the Entity-first transaction. Enabled Board event Constraints are evaluated inside that transaction. A rejection preserves every ordered error’s code, workspace-authored message, target, and safe metadata; it commits no partial Board Item, Board Data, Target, history, audit, resource event, notification, or outbox effect. Historical import and reconciliation are different: they do not replay transition, entry, or exit Constraints against today’s actor or clock. They do enforce the resulting status invariant before committing the imported or reconciled state. The complete lifecycle-to-trigger matrix, including same-status reorder, remove, terminal reopen, new pass, administrative relocation, and Board activation behavior, is documented in [Board constraints: Which bindings run for each lifecycle operation](/docs/user/boards/constraints/#which-bindings-run-for-each-lifecycle-operation). Use the returned `RowVersion` for the next mutation, and poll any returned function job id at `GET /api/workspace/functions/jobs/{jobId}`. Validation and BeforeSaveMutation phases reject the facade with `moltaro.automation.executionPhase.unsupported`.
### Tag ownership and feature flags
[Section titled “Tag ownership and feature flags”](#tag-ownership-and-feature-flags)
`EntityDefinition.TagsEnabled` and `Board.TagsEnabled` are independent. The first controls tags whose owner is an EntityInstance; the second controls tags whose owner is a BoardItem. An Entity-backed Board does not inherit the Entity Definition setting, and enabling one does not enable the other.
Ordinary tag mutation accepts an authorized open BoardItem. A terminal item returns a correlated safe Problem Details response with code `moltaro.tags.boardItem.terminalRequiresHistoricalOperation` and metadata that names `ReconcileHistoricalItemTagsAsync`. Disabled Entity and Board owners use `moltaro.tags.entity.disabled` and `moltaro.tags.board.disabled`; permission and unknown-owner failures use `moltaro.tags.access.denied` and `moltaro.tags.resource.notFound`. Consult the installation-local OpenAPI for the exact HTTP schema.
### Historical Board import
[Section titled “Historical Board import”](#historical-board-import)
Use `ImportHistoricalItemsAsync` for an initial or corrective import whose source provides a stable per-item identity. One call accepts 1–500 ordered items; each item commits in its own short transaction, so the batch can return both successes and stable item errors. `SourceKey` is case-sensitive, at most 128 characters, and cannot contain whitespace or control characters. Moltaro stores one durable identity per Board and source key. Ordinary historical import does not adopt a Board item that already exists without that identity. The item’s `DataRecordId` is fixed when the item is created; reconciliation updates fields in that same Board Data record and rejects attempts to replace the record identity.
Create requests may start directly in a terminal status. `IsOpen` comes only from the configured status type; a terminal item defaults `CompletedAtUtc` to its effective entered time and rejects completion before that time, while a non-terminal item rejects a completed timestamp. Reconciliation applies the same chronology rule after preserving omitted timestamps. Null content and due values mean “absent” on create and “preserve” on reconcile. A JSON null inside a supplied `Fields` patch clears that one field.
An exact replay returns `Unchanged` without requiring a row version and writes no new Board Data, item, history, audit, or resource event. A changed identity requires its current `ExpectedRowVersion`; missing or stale tokens return a conflict. After cancellation or an unknown response, call `GetHistoricalImportAsync(boardIdOrKey, sourceKey)` or safely replay the whole batch. Keep source keys stable across retries and inspect every ordered item result, including `FollowUpOperationIds`.
### Reconcile Board Data on terminal historical items
[Section titled “Reconcile Board Data on terminal historical items”](#reconcile-board-data-on-terminal-historical-items)
Use `ReconcileHistoricalItemDataAsync` when an already imported historical item is terminal and only its Board-owned data is wrong. This is a trusted Net Operation Project migration operation; it does not relax `UpdateDataAsync` or the Runtime API rule that ordinary callers cannot edit a closed item.
One request accepts 1-500 ordered patches and commits each item independently. Each patch must provide exactly one locator: either `BoardItemId` or the case-sensitive historical `SourceKey`. The item must still belong to that Board, have a durable `BoardHistoricalImportIdentity`, be non-removed, and be closed in a terminal status. `ExpectedRowVersion` is mandatory for every patch. A changed patch with a stale token is rolled back; an exact replay is recognized before the token check, returns `Unchanged` with the current row version, and remains write-free.
Omit `Subject` or `Description` to preserve it. Supply `new BoardHistoricalTextPatch(null)` to clear it, or wrap a string to replace it. `Fields` accepts only fields from the Board-owned Part definition; JSON null clears one supplied Part field. The normal contained-Entity path still enforces field schema, rules, audit, presentation/cache maintenance, and the Board `UpdateData` business invariant.
A successful change keeps the same Board item and Board Data record and changes only Board Data plus the Board item’s `RowVersion`/ordinary modified stamp. Status, open/closed state, rank, run, entered/completed/due timestamps, target, responsibilities, move history, transition history, and Board lifecycle audit are not rewritten. The Board Data audit diff records `moltaro-system-automation` and preserves the original function run, function version, original user, and correlation metadata. Inspect every ordered item result; after an unknown outcome, safely replay the same desired patch.
### Reconcile tags on terminal historical items
[Section titled “Reconcile tags on terminal historical items”](#reconcile-tags-on-terminal-historical-items)
Use `ReconcileHistoricalItemTagsAsync` only from trusted Net Operation Project automation when a terminal BoardItem itself must own provenance or classification tags. It does not relax the ordinary Tags API. One request accepts 1-500 ordered items and commits each owner independently; inspect `SucceededCount`, `FailedCount`, and every item error because there is no cross-owner transaction.
Each item provides exactly one `BoardItemId` or case-sensitive historical `SourceKey`, plus `ExpectedRowVersion`. `Add` creates only missing names and leaves an existing same-name tag unchanged. `ReplaceManagedSet` requires the complete `ManagedTagNames` allow-list owned by that synchronization and can update or remove only those names; an empty desired `Tags` list removes all currently stored names in that managed set. All tags outside the managed set are preserved, including manual and unrelated automation tags. Exact replay returns `Unchanged` without writes even after the supplied token became stale; a real change with a stale token fails.
The operation requires an active, tag-enabled Board and a non-removed item in a terminal status. It writes tags through the governed tag service as `moltaro-system-automation`; tag events and Board audit preserve function, run, version, original-user, and correlation metadata. It never reopens or moves the item and does not change Board Data, status, `CompletedAtUtc`, rank, run, history, or Board item `RowVersion`. Read tags back with `ITagService` or the ordinary resource-list Tags API.
### Recover identity for a pre-existing Board item
[Section titled “Recover identity for a pre-existing Board item”](#recover-identity-for-a-pre-existing-board-item)
Use the separate recovery contract only for an Entity Definition target that was already tracked before its historical `SourceKey` was available:
1. Call `GetHistoricalRecoveryStateAsync` with the exact Board, case-sensitive source key, and target.
2. Use exactly the returned recommendation: `AdoptExistingItem` for one non-removed unbound pass, or `CreateAfterRemovedItem` for one removed unbound pass.
3. Pass the returned `BoardItemId` and `RowVersion` unchanged to `RecoverHistoricalItemAsync`, together with the desired `ImportHistoricalBoardItemRequest`.
4. Handle `Adopted`, `Created`, `Updated`, or `Unchanged`.
5. After an unknown result, retry the same guarded request or call `GetHistoricalImportAsync` for the source key.
Adoption preserves the existing Board item and Board Data ids, comments, attachments, responsibilities, links, history, audit, and origin metadata. Recovery after removal leaves the removed pass untouched and creates `RunNumber = removed.RunNumber + 1` with `PreviousBoardItemId = null`. Multiple prior passes are ambiguous. Self-contained and Entitlement targets are not supported by this recovery contract.
The older `IBoardRuntimeCommandService` and `IBoardRuntimeDefinitionProvider` are advanced `TrustedDirectDb` interfaces; they do not promise the complete application/WebApp orchestration above. See the exact [C# Boards recipes](/docs/developer/business-logic/csharp-business-logic/recipes/#boolean-transition-add-an-entity-to-a-board) and inspect the installation’s `developer-surface` response before injecting a service. A direct package host that enables event Board Constraints must register an `IBoardConstraintRuntimeEvaluator` through `AddMoltaroBoardsConstraintRuntimeEvaluator()`; the package default fails closed when an enabled Binding applies and no production evaluator is available. That public custom-evaluator contract receives Board event plans; proposed-state status-invariant evaluation is supplied by the Moltaro application host to generated Package and Net Operation Project contexts. A standalone package-only host fails closed for applicable status invariants. Authoritative mutations invoke `EvaluateAuthoritativeBatchAsync` after taking their governance locks. A cache-backed custom evaluator must override that method and reload its applicable configuration and compiled plans inside the call; delegating to `EvaluateBatchAsync` is safe only for uncached evaluation sources.
## Step 1 — Discover boards
[Section titled “Step 1 — Discover boards”](#step-1--discover-boards)
`GET /api/workspace/boards` ([operation](/docs/developer/api-reference/operations/boards-runtime-list/)) returns the active boards the caller may see:
```bash
curl -s https://ops.example.com/api/workspace/boards \
-H "Authorization: Bearer "
```
```json
{
"Data": [
{
"Id": "zuq5JQx4kLF1",
"Key": "support_triage",
"DisplayName": "Support triage",
"Description": "Incoming ticket triage and resolution.",
"TagsEnabled": false,
"DueDateEnabled": false
}
],
"Success": true
}
```
Item endpoints accept either the board `Id` or the `Key` as `{boardIdOrKey}`.
## Step 2 — Read the create context
[Section titled “Step 2 — Read the create context”](#step-2--read-the-create-context)
`GET /api/workspace/boards/{boardIdOrKey}/items/create-context` ([operation](/docs/developer/api-reference/operations/boards-runtime-create-context/)) is the discovery step before creating anything: it lists the board’s target definitions (with the full create-form model per target), the statuses, and what the caller may do:
```bash
curl -s https://ops.example.com/api/workspace/boards/support_triage/items/create-context \
-H "Authorization: Bearer "
```
```json
{
"Data": {
"Board": { "Id": "zuq5JQx4kLF1", "Key": "support_triage", "DisplayName": "Support triage" },
"Statuses": [
{ "Id": "2FEzzRX4vMNg", "DisplayName": "Triage", "MetaType": 0, "SortOrder": 1 },
{ "Id": "x2lpSYpWQ3q6", "DisplayName": "In progress", "MetaType": 1, "SortOrder": 2 },
{ "Id": "gc3NgI61wO4D", "DisplayName": "Resolved", "MetaType": 2, "SortOrder": 3 }
],
"TargetOptions": [
{
"TargetDefinition": {
"Id": "ojmxCpYFwfCY",
"TargetKind": 0,
"TargetModelId": "pYZqCLZWb0K1",
"DisplayNameSingular": "Support ticket",
"CanAdd": true,
"CanMove": true
}
}
]
},
"Success": true
}
```
An agent should treat this response the way it treats an entity schema: read it first, then act on what it declares.
## Step 3 — Create an item
[Section titled “Step 3 — Create an item”](#step-3--create-an-item)
`POST /api/workspace/boards/{boardIdOrKey}/items/create` ([operation](/docs/developer/api-reference/operations/boards-runtime-create-item/)) creates the record **and** its board item in one atomic call: `TargetCreate` carries the record fields (same write shapes as [record creation](/docs/developer/integration-quickstart/#step-5--create-a-record)), `BoardData` carries the process context:
```bash
curl -s -X POST https://ops.example.com/api/workspace/boards/support_triage/items/create \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "BoardTargetDefinitionId": "ojmxCpYFwfCY",
"TargetCreate": { "Fields": {
"Title": "VPN drops every hour", "Priority": "high" } },
"BoardData": { "Subject": "VPN drops every hour" } }'
```
```json
{
"Data": {
"Id": "1WtfVd0cpu9x",
"BoardId": "zuq5JQx4kLF1",
"Target": {
"BoardTargetDefinitionId": "ojmxCpYFwfCY",
"TargetKind": 0,
"TargetModelId": "pYZqCLZWb0K1",
"TargetObjectId": "89aed6c628e54e2ab4a782ae30fb156d"
},
"BoardStatusId": "2FEzzRX4vMNg",
"IsOpen": true,
"RowVersion": "6f2acd60-ac51-4adb-ac19-49784f243436",
"Actions": { "CanMove": true, "CanReadHistory": true, "CanEditBoardData": true, "CanViewAuditTrail": true },
"Card": { "Number": 1, "Subject": "VPN drops every hour", "BoardStatusDisplayName": "Triage" }
},
"Success": true
}
```
The new record exists as a normal `SupportTicket` (`Target.TargetObjectId`), and the item landed in the board’s initial status. To put an **existing** record on the board instead, use [`add-context`](/docs/developer/api-reference/operations/boards-runtime-add-context/) to preview admission and [`POST /items`](/docs/developer/api-reference/operations/boards-runtime-add-item/) to add it.
### Position a new item safely
[Section titled “Position a new item safely”](#position-a-new-item-safely)
`Position` is optional on `POST /api/workspace/boards/{boardIdOrKey}/items`. When it is omitted, Moltaro appends the new item to the destination status. Explicit boundaries describe the current order in that same board, status, and cycle:
* `PreviousBoardItemId` alone inserts after the current last item;
* `NextBoardItemId` alone inserts before the current first item;
* both ids insert between two items that are currently adjacent.
Read the current board order immediately before constructing explicit boundaries. A removed item, an item from another board/status/cycle, a stale edge, or a non-adjacent pair returns HTTP `400` with the stable code `moltaroBoards.runtime.validation.positionInvalid`; re-read the board and decide the position again. Do not retry the same stale payload unchanged.
```json
{
"TargetKind": 0,
"TargetModelId": "pYZqCLZWb0K1",
"TargetObjectId": "89aed6c628e54e2ab4a782ae30fb156d",
"RepeatMode": 1,
"Position": {
"PreviousBoardItemId": "item-before",
"NextBoardItemId": "item-after"
}
}
```
## Step 4 — Read move options and validate
[Section titled “Step 4 — Read move options and validate”](#step-4--read-move-options-and-validate)
The server computes what moves are allowed — the transition graph, permission checks, and process rules all apply. Never hardcode status flows; ask:
```bash
curl -s https://ops.example.com/api/workspace/boards/support_triage/items/1WtfVd0cpu9x/move-options \
-H "Authorization: Bearer "
```
```json
{
"Data": {
"BoardItemId": "1WtfVd0cpu9x",
"FromBoardStatusId": "2FEzzRX4vMNg",
"Options": [
{ "BoardStatusId": "2FEzzRX4vMNg", "DisplayName": "Triage",
"Allowed": true, "BoardTransitionId": null, "Errors": [] },
{ "BoardStatusId": "x2lpSYpWQ3q6", "DisplayName": "In progress",
"Allowed": true, "BoardTransitionId": "qRO10Ii6gAbn", "Errors": [] },
{ "BoardStatusId": "gc3NgI61wO4D", "DisplayName": "Resolved",
"Allowed": false, "BoardTransitionId": null,
"Errors": [ { "Code": "moltaroBoards.runtime.transitionNotFound",
"Message": "This card cannot be moved to that status." } ] }
]
},
"Success": true
}
```
`Resolved` is blocked because the board defines no `Triage → Resolved` transition. `POST .../move/validate` ([operation](/docs/developer/api-reference/operations/boards-runtime-validate-move-item/)) runs the same checks for one concrete move as a dry run — no state change:
```bash
curl -s -X POST https://ops.example.com/api/workspace/boards/support_triage/items/1WtfVd0cpu9x/move/validate \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "ToBoardStatusId": "gc3NgI61wO4D" }'
```
```json
{
"Data": {
"Allowed": false,
"BoardItemId": "1WtfVd0cpu9x",
"FromBoardStatusId": "2FEzzRX4vMNg",
"ToBoardStatusId": "gc3NgI61wO4D",
"BoardTransitionId": null,
"Errors": [
{ "Code": "moltaroBoards.runtime.transitionNotFound",
"Message": "This card cannot be moved to that status." }
]
},
"Success": true
}
```
Note the envelope: `Success: true` because the validation itself succeeded — the verdict lives in `Data.Allowed` and `Data.Errors`.
The validation result also returns `CapturedAtUtc` and ordered `ConstraintDecisions` when Board Constraints were evaluated. Execute promptly with the same `RowVersion`. The move command rebuilds the authoritative plan under its lock; preview is not a reservation. For the same row versions and proposed state, preview and execute use the same Business Invariant and Board Constraint preparation path and return the same ordered failures.
The move-options endpoint evaluates all destination candidates as one request-local batch. Board definition lookups, Constraint configuration, compiled plans, and bounded entity dependencies are shared across candidates; the runtime does not repeat full validation once per status.
When a transition has required item links, its move option and validation result include `LinkPrerequisites`. Each row reports the configured scope, `CurrentCount`, `MissingCount`, and `Satisfied`. Only active links to non-removed items in the exact configured Board target definition count. Missing links return `moltaroBoards.runtime.moveLinkPrerequisiteMissing`.
The move request can satisfy missing rows with `LinkPrerequisiteSelections`. Select an existing readable item with `OtherBoardItemId`, or atomically create a target Board item with `CreateItem`; supply exactly one per selection:
```json
{
"ToBoardStatusId": "x2lpSYpWQ3q6",
"RowVersion": "6f2acd60-ac51-4adb-ac19-49784f243436",
"LinkPrerequisiteSelections": [
{
"BoardTransitionLinkPrerequisiteId": "required-parent",
"OtherBoardItemId": "parent-board-item-id"
}
]
}
```
Moltaro derives the link kind and direction from configuration; clients cannot override them. The selected item must match the configured Board and target definition, normal read/link permissions and link invariants still apply, and link creation plus movement commit in one transaction. With `CreateItem`, the new target, its Board Data, the link, and the move all commit or roll back together.
## Step 5 — Move the item
[Section titled “Step 5 — Move the item”](#step-5--move-the-item)
`POST /api/workspace/boards/{boardIdOrKey}/items/{boardItemId}/move` ([operation](/docs/developer/api-reference/operations/boards-runtime-move-item/)) executes the move; send the `RowVersion` you last saw, exactly like a record update:
```bash
curl -s -X POST https://ops.example.com/api/workspace/boards/support_triage/items/1WtfVd0cpu9x/move \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "ToBoardStatusId": "x2lpSYpWQ3q6",
"RowVersion": "6f2acd60-ac51-4adb-ac19-49784f243436" }'
```
```json
{
"Data": {
"Id": "1WtfVd0cpu9x",
"BoardStatusId": "x2lpSYpWQ3q6",
"IsOpen": true,
"RowVersion": "738941bd-5d82-4013-b64d-0063012b83c9",
"Card": { "Number": 1, "Subject": "VPN drops every hour", "BoardStatusDisplayName": "In progress" }
},
"Success": true
}
```
A stale `RowVersion` fails the move instead of silently overriding a concurrent change — keep the value returned by each mutation.
## Search board items through live Entity search targets
[Section titled “Search board items through live Entity search targets”](#search-board-items-through-live-entity-search-targets)
`POST /api/workspace/boards/{boardIdOrKey}/items/search` combines `SearchText` with status, cycle, target, Board Data, Target Data, responsibility, tag, due date, and link filters. The filters narrow the Board candidate set; `SearchText` then matches Board-owned text and identity or the live target projection.
For an Entity target, Moltaro uses that definition’s current configured `SearchTargets`, including supported direct and related fields, table paths, number, comments, and attachments. The same access-aware Entity search SQL used by record lists is applied in bulk to target ids already present on the Board. There is no Board-side copy of Entity content, synchronization job, item reopen, or Board rebuild. Updating `SearchTargets` changes the next Board search after the normal definition snapshot refresh.
Entity content contributes a match only when the caller can normally read the root record and every protected linked path involved in that configured target. Restricted text cannot change the returned rows or `TotalCount`. Each Board target definition is evaluated separately, so equal target ids from different Entity definitions cannot cross-match. If `SearchTargets` is empty, configured Entity content does not match; stable target identity, readable target presentation, and Board-owned text remain available as compatibility fallbacks. Cursor paging and the other Board filters are unchanged.
Boards does not copy Entity text into Board Data and does not own a separate search index, reconciliation task, or reindex/rebuild operation. Each request reads the current Entity PostgreSQL storage using the same parameterized search/index contract as Data Explorer. Candidate ids are processed in bounded keyset batches; each match batch is written to request-local indexed PostgreSQL temporary tables and released from application memory. Count and page queries join those tables, while Board Data uses keyset batches without repeated count or OFFSET queries. The complete search, including Count and page materialization, has a 30-second deadline. If a target provider fails or any stage times out, search fails closed with `moltaroBoards.runtime.searchProviderUnavailable`; Moltaro does not return a partial page or partial `TotalCount`, and diagnostics never include `SearchText`.
## Search link candidates within an explicit scope
[Section titled “Search link candidates within an explicit scope”](#search-link-candidates-within-an-explicit-scope)
`POST /api/workspace/boards/{boardIdOrKey}/items/{boardItemId}/link-candidates` returns non-removed items that the caller may read and may link from both endpoints. Use `BoardTargetDefinitionIds` whenever the workflow already knows which target types are valid:
```bash
curl -s -X POST https://ops.example.com/api/workspace/boards/support_triage/items/1WtfVd0cpu9x/link-candidates \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "SearchText": "VPN",
"BoardTargetDefinitionIds": ["ojmxCpYFwfCY"],
"LinkType": 1,
"Direction": 0,
"Limit": 10 }'
```
`BoardTargetDefinitionIds` scopes the complete search pipeline, not only the last result filter. Moltaro invokes target search only for the supplied active definitions, searches Board Data only for their owning active Boards, and queries final candidates only from those definitions. An unknown or inactive id fails with the stable `moltaroBoards.runtime.targetDefinitionNotFound` error. Read access, `ManageItemLinks`, Business Invariants, duplicate-link rules, and Child or Related cycle rules still apply to every returned candidate; inaccessible items do not disclose target metadata.
Omitting `BoardTargetDefinitionIds` intentionally keeps workspace-wide search across active Boards and target definitions. Each provider and Board Data presearch is bounded before final candidate materialization. This bounded workspace-wide search returns a limited page; clients must not expect every match to be loaded. The endpoint also supports free text and exact `#RunNumber` lookup. Caller cancellation and the HTTP request deadline propagate through provider and database work; after an unknown or cancelled response, issue a new bounded search instead of assuming that a background search continues.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* Process collaboration — per-item [comments](/docs/developer/api-reference/operations/boards-runtime-create-process-comment/), attachments, and the [process activity feed](/docs/developer/api-reference/operations/boards-runtime-process-activity-feed/).
* History and audit — per-item [history](/docs/developer/api-reference/operations/boards-runtime-history/) and [audit trail](/docs/developer/api-reference/operations/boards-runtime-audit-trail/); see also [Record history and audit](/docs/developer/audit-trail/).
* Item links, responsibilities, due dates, and cycles — the rest of the [Boards runtime tag](/docs/developer/api-reference/operations/tags/boards-runtime/).
# Entitlement Operations walkthrough
> Enable the module, create an information-service-access model and a plan with a consumable quantity, grant an entitlement, consume, and read the remaining balance.
This walkthrough sells a **support-hours package**: customers buy a plan that covers a support service and includes ten consumable hours. It goes from an empty workspace to a granted, partially consumed entitlement with a readable ledger — configuration through the Configuration API, grant and consumption through the public runtime API.
All examples were executed against a real Moltaro installation; response bodies are real, trimmed for length. Concepts are in the [Entitlement Operations user documentation](/docs/user/entitlement-operations/); the complete contract is in the [Configuration API reference](/docs/developer/configuration-api-reference/) and the [API reference](/docs/developer/api-reference/).
Workspace URL
Both the Configuration API calls and Runtime API calls on this page go to the target installation’s `WORKSPACE_API_BASE_URL`, never to `moltaro.com`. See [Connect to a workspace API](/docs/developer/workspace-api-connection/) before copying an endpoint.
The workspace owner supplies the guide, URL, key, module setup, and required permissions through the Portal or administrative handoff. An agent does not provision or elevate itself. Apply the durable idempotency, polling, and ledger-verification rules in [Reliable API automation](/docs/developer/reliable-api-automation/).
## Calling Entitlement Operations from workspace C# automation
[Section titled “Calling Entitlement Operations from workspace C# automation”](#calling-entitlement-operations-from-workspace-c-automation)
For an Action, TriggerHandler, Command, Job, or HttpEndpoint in the Net Operation Project, prefer `IEntitlementAutomationCommandService` and `IEntitlementAutomationQueryService` from `Moltaro.Package.NET.EntitlementOperations.Automation`. They wrap the package domain services with the complete application flow for grant, lifecycle, quantity ledger, access/history, and renewal operations. Request DTOs contain a stable `IdempotencyKey` but no caller-controlled `ActorUserId` or correlation field.
Writes persist as `moltaro-system-automation`; the original user, function run, function/version, and correlation remain in origin, audit, resource event, and ledger metadata. Each facade call owns a separate application scope/transaction. A successful return means its main transaction committed; reuse the same idempotency key on retry, use `RowVersion` for concurrency, and poll any returned function job id at `GET /api/workspace/functions/jobs/{jobId}`. Validation and BeforeSaveMutation phases return `moltaro.automation.executionPhase.unsupported`.
The older `IEntitlementRuntime*` and `IEntitlementRenewal*` services remain advanced `TrustedDirectDb` APIs and do not promise the application side effects above. See the exact [C# Entitlement recipes](/docs/developer/business-logic/csharp-business-logic/recipes/#entitlement-grant-consume-access-and-history) and the installation’s `developer-surface` response.
## How the pieces fit
[Section titled “How the pieces fit”](#how-the-pieces-fit)
* An **entitlement model** declares who owns entitlements (the *party*, a record type) and what they get access to (the *resource*, another record type). Models are typed — software license, membership, information service access, warranty — and the type decides which terms a plan may carry. Consumable quantities are available on **information service access** and **warranty** models.
* A **plan** is a sellable package inside a model: covered resources, duration, and quantity terms.
* An **entitlement** is one granted plan for one owner. Quantity operations (consume, reverse, adjust) are ledgered.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
Two record types with one record each — the party and the resource. Create them exactly like in the [Configuration quickstart](/docs/developer/configuration-quickstart/); this walkthrough uses:
* `Customer` (definition `vGMraALVTZk5`) with the record *Acme GmbH* (`0f46096527cd4a24ae26087c4040919c`);
* `SupportService` (definition `SR6dzMPbIx0h`) with the record *Priority support* (`3188ece4de5d428db3666adc57ef6990`).
The admin endpoints below need the **Admin** or **Configurator** role; plan and entitlement runtime operations are also open to subjects with the model-scoped **Manage plans** / **Manage entitlements** permissions.
## Step 1 — Enable the module
[Section titled “Step 1 — Enable the module”](#step-1--enable-the-module)
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/entitlement-operations/enable \
-H "Authorization: Bearer " -H "Content-Type: application/json" -d '{}'
```
([operation](/docs/developer/configuration-api-reference/operations/admin-entitlement-operations-enable/); `GET /api/workspace/admin/entitlement-operations/setup` reports module state and existing models at any time.)
## Step 2 — Create the entitlement model
[Section titled “Step 2 — Create the entitlement model”](#step-2--create-the-entitlement-model)
`POST /api/workspace/admin/entitlement-operations/models/information-service-access` ([operation](/docs/developer/configuration-api-reference/operations/admin-entitlement-operations-create-information-service-access-model/)) binds the party and resource roles to the two record types:
```bash
curl -s -X POST https://ops.example.com/api/workspace/admin/entitlement-operations/models/information-service-access \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "DisplayName": "Support access",
"PartyType": 0, "AccessMode": 0, "ResourceScopeMode": 0,
"OwnerParty": { "DisplayName": "Customer", "EntityDefinitionId": "vGMraALVTZk5" },
"Resource": { "DisplayName": "Support service", "EntityDefinitionId": "SR6dzMPbIx0h" } }'
```
```json
{
"Data": {
"Id": "8Ip2QXlDaNLk",
"Key": "supportAccess",
"DisplayName": "Support access",
"PartyType": 0,
"AccessMode": 0,
"ResourceScopeMode": 0,
"OwnerParty": { "Id": "tocEHOfa9qw8", "DisplayName": "Customer",
"EntityDefinitionId": "vGMraALVTZk5", "EntityDisplayName": "Customers" },
"Resource": { "Id": "CNC7hR6QHXkB", "DisplayName": "Support service",
"EntityDefinitionId": "SR6dzMPbIx0h", "EntityDisplayName": "Support services" },
"Plans": [],
"IsActive": true,
"RowVersion": "7a9cf98a-5962-4a23-9e80-281b5e7cc94f"
},
"Success": true
}
```
`PartyType: 0` is an individual owner party, `AccessMode: 0` is direct (non-delegated) access, and `ResourceScopeMode: 0` covers exactly the resources a plan lists. Before writing, `POST .../setup-plan` validates a model shape without saving, and `GET .../setup/relationship-paths` discovers delegation and hierarchy paths for the delegated shapes — both useful for agents assembling a model from workspace facts.
## Step 3 — Create a plan with a consumable quantity
[Section titled “Step 3 — Create a plan with a consumable quantity”](#step-3--create-a-plan-with-a-consumable-quantity)
`POST /api/workspace/entitlement-operations/models/information-service-access/{modelId}/plans` ([operation](/docs/developer/configuration-api-reference/operations/entitlement-operations-plans-create-information-service-access-plan/)) creates the sellable package. `Resources` lists the concrete covered records; `ConsumableQuantity` adds a one-time quantity pool (use `TimeBoundQuantity` plus window terms for a per-period quota instead):
```bash
curl -s -X POST https://ops.example.com/api/workspace/entitlement-operations/models/information-service-access/8Ip2QXlDaNLk/plans \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "DisplayName": "Support 10-hour pack", "IsActive": true,
"DurationValue": 12, "DurationUnit": 1,
"ConsumableQuantity": 10,
"Resources": [ { "EntityDefinitionId": "SR6dzMPbIx0h",
"ResourceEntityInstanceId": "3188ece4de5d428db3666adc57ef6990",
"SortOrder": 1 } ] }'
```
```json
{
"Data": {
"Id": "5M05saIptqZI",
"Key": "support-10-hour-pack",
"DisplayName": "Support 10-hour pack",
"EntitlementModelId": "8Ip2QXlDaNLk",
"IsActive": true,
"DurationValue": 12,
"DurationUnit": 1,
"ConsumableQuantity": 10.0000,
"Resources": [
{ "Id": "f7sHOYnuJjsN", "EntityDefinitionId": "SR6dzMPbIx0h",
"ResourceEntityInstanceId": "3188ece4de5d428db3666adc57ef6990", "SortOrder": 1 }
],
"RowVersion": "966cb62b-6294-42d6-b3f1-ac701e10ecac"
},
"Success": true
}
```
`DurationUnit` is `0` days, `1` months, `2` years — this plan grants twelve months of access.
## Step 4 — Grant an entitlement
[Section titled “Step 4 — Grant an entitlement”](#step-4--grant-an-entitlement)
Granting switches to the public runtime API. `POST /api/workspace/entitlement-operations/entitlements` ([operation](/docs/developer/api-reference/operations/entitlement-operations-entitlements-grant/)) needs only the plan and the owner record; the plan resolves the model, terms, and covered resources. `grant-preview` runs the same resolution as a dry run:
```bash
curl -s -X POST https://ops.example.com/api/workspace/entitlement-operations/entitlements \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "PlanId": "5M05saIptqZI", "OwnerEntityInstanceId": "0f46096527cd4a24ae26087c4040919c" }'
```
```json
{
"Data": {
"Id": "SxNWSs7BjQK8",
"EntitlementNumber": 1,
"EntitlementModelId": "8Ip2QXlDaNLk",
"EntitlementPlanId": "5M05saIptqZI",
"Owner": { "EntityDefinitionId": "vGMraALVTZk5", "EntityDisplayName": "Customer",
"EntityInstanceId": "0f46096527cd4a24ae26087c4040919c" },
"Status": 2,
"EffectiveState": 1,
"StartsAtUtc": "2026-07-22T15:51:31.09229+00:00",
"EndsAtUtc": "2027-07-22T15:51:31.09229+00:00",
"Limits": [
{ "Id": "HTCJy9FeWGK9", "LimitFamily": 0,
"StartsAtUtc": "2026-07-22T15:51:31.09229+00:00",
"EndsAtUtc": "2027-07-22T15:51:31.09229+00:00", "IsActive": true },
{ "Id": "0q94w96LMhOD", "LimitFamily": 1,
"GrantedAmount": 10.0000, "AvailableAmount": 10.0000,
"ConsumedAmountSnapshot": 0.0000, "IsActive": true }
],
"QuantityLedger": [],
"AllowedActions": { "CanRenew": true, "CanSuspend": true, "CanRevoke": true,
"CanConsume": true, "CanReverse": false, "CanAdjust": true }
},
"Success": true
}
```
The plan’s terms became two **limits**: `LimitFamily: 0` is the duration term (twelve months from grant), `LimitFamily: 1` is the consumable quantity pool. Keep the quantity limit id — quantity operations target it explicitly when an entitlement carries more than one quantity limit.
## Step 5 — Consume quantity
[Section titled “Step 5 — Consume quantity”](#step-5--consume-quantity)
`POST /api/workspace/entitlement-operations/entitlements/{entitlementId}/quantity/consume` ([operation](/docs/developer/api-reference/operations/entitlement-operations-entitlements-consume-quantity/)):
```bash
curl -s -X POST https://ops.example.com/api/workspace/entitlement-operations/entitlements/SxNWSs7BjQK8/quantity/consume \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "Quantity": 1.5, "ReasonText": "Incident #4812 troubleshooting" }'
```
```json
{
"Data": {
"Id": "SxNWSs7BjQK8",
"Limits": [
{ "Id": "HTCJy9FeWGK9", "LimitFamily": 0, "IsActive": true },
{ "Id": "0q94w96LMhOD", "LimitFamily": 1,
"GrantedAmount": 10.0000, "AvailableAmount": 8.5000,
"ConsumedAmountSnapshot": 1.5000, "IsActive": true }
],
"QuantityLedger": [
{ "Id": "A4BO8YgXl6fu", "EntitlementLimitId": "0q94w96LMhOD",
"Operation": 1, "Quantity": 1.5000, "BalanceAfter": 1.5000,
"ReversibleQuantity": 1.5000,
"ReasonText": "Incident #4812 troubleshooting",
"OccurredAtUtc": "2026-07-22T15:51:32.648059+00:00",
"CorrelationId": "ad8d16de0793473da712f35ef0e94c31" }
]
},
"Success": true
}
```
Mistakes are corrected with `.../quantity/reverse` (undo a consumption) and `.../quantity/adjust` (set a corrected level); every operation supports an `IdempotencyKey` so an agent can retry safely.
## Step 6 — Read the state
[Section titled “Step 6 — Read the state”](#step-6--read-the-state)
`GET /api/workspace/entitlement-operations/entitlements/{entitlementId}` ([operation](/docs/developer/api-reference/operations/entitlement-operations-entitlements-get/)) returns the full detail — lifecycle `Status` and computed `EffectiveState`, periods, resources, limits with remaining quantities, and the entitlement’s own quantity ledger. [`GET .../entitlements/summary`](/docs/developer/api-reference/operations/entitlement-operations-entitlements-summary/) aggregates operational indicators (active entitlements, expiring soon, failed renewals, usage pressure) for dashboards.
There is no HTTP “may this party use this resource right now” endpoint: integrations read entitlement state through detail and summary, while workspace C# logic can run the full access evaluation in-process through `IEntitlementRuntimeAccessService` — see [C# business logic](/docs/developer/business-logic/csharp-business-logic/).
## Step 7 — The ledger
[Section titled “Step 7 — The ledger”](#step-7--the-ledger)
Every entitlement operation lands in the append-only workspace ledger — the grant itself included. `GET /api/workspace/entitlement-operations/ledger` ([operation](/docs/developer/api-reference/operations/entitlement-operations-ledger-list/)):
```json
{
"Data": {
"Items": [
{ "Id": "A4BO8YgXl6fu", "EntitlementId": "SxNWSs7BjQK8",
"EntitlementPlanDisplayName": "Support 10-hour pack",
"Operation": 1, "Quantity": 1.5000, "BalanceAfter": 1.5000,
"EntitlementLimitId": "0q94w96LMhOD",
"ReasonText": "Incident #4812 troubleshooting",
"Actor": { "DisplayName": "owner" },
"OccurredAtUtc": "2026-07-22T15:51:32.648059+00:00" },
{ "Id": "HSLKRscSPMjW", "EntitlementId": "SxNWSs7BjQK8",
"EntitlementPlanDisplayName": "Support 10-hour pack",
"Operation": 0, "Quantity": 0.0000, "BalanceAfter": null,
"Actor": { "DisplayName": "owner" },
"OccurredAtUtc": "2026-07-22T15:51:31.139382+00:00" }
]
},
"Success": true
}
```
The ledger is the audit surface for quantity truth — reversals and adjustments appear as their own entries instead of rewriting history, and each entry carries the actor, the acted-on limit, and a `CorrelationId` for tracing.
## Configure a renewal policy
[Section titled “Configure a renewal policy”](#configure-a-renewal-policy)
Renewal is configured per model, then attached to a plan. Create a policy on the model through the Configuration API:
```http
POST /api/workspace/entitlement-operations/models/{modelType}/{modelId}/renewal-policies
Content-Type: application/json
{
"DisplayName": "Annual manual review",
"Mode": 1,
"LeadDays": 30
}
```
`Mode` selects the workflow: `0` Disabled (no renewal workflow, no lead time or key is stored), `1` ManualReview (a background worker opens a manual-review renewal operation when the active period enters the `LeadDays` lead window), and `2` External (a trusted external system decides, using the stored `ExternalPolicyKey`).
Assign the policy to a plan through the plan’s optional `RenewalPolicyId`, so new grants from that plan inherit it. ManualReview renewal operations are then observed, retried, and cancelled through the public renewal-operations endpoints (`ListRenewalOperationsAsync` from C#) — never reconstruct renewal state yourself.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* Lifecycle — `suspend`, `resume`, `revoke`, `expire`, and `renew` on the same entitlement resource.
* [Boards for entitlement-related work](/docs/user/boards/) — put the related Entity record on the Board for the normal Runtime UI workflow. A direct Entitlement Board target is an advanced API/C# scenario without full Runtime Board UI support.
# Record history and audit
> Reading the record change feed over the API, configuring audit settings, and emitting audit events from C# project code.
Moltaro keeps an auditable history for record types that enable the audit trail: who changed what, when, and through which surface. This page covers the three developer touchpoints — reading history over the API, configuring audit per record type, and emitting business events from workspace C# code. For the user-facing view see [Audit trail](/docs/user/access-and-governance/audit-trail/) and [History and audit](/docs/user/data-and-records/history-and-audit/).
All examples were executed against a real Moltaro installation; response bodies are real, trimmed for length.
Workspace URL
The audit and configuration routes below are served by the target installation’s `WORKSPACE_API_BASE_URL`, not by the public documentation host. See [Connect to a workspace API](/docs/developer/workspace-api-connection/).
## Reading the record change feed
[Section titled “Reading the record change feed”](#reading-the-record-change-feed)
`GET /api/workspace/entity/{entityIdOrKey}/instances/{instanceId}/changes` ([operation](/docs/developer/api-reference/operations/entity-instances-changes-list-changes/)) returns the change history of one record, newest first, paged with `page`/`pageSize`. The caller needs read access to the record **and** the **Read changes history** permission — history can expose values the current record state no longer shows:
```bash
curl -s "https://ops.example.com/api/workspace/entity/SupportTicket/instances/98520ecb203d4188abf10298a5a638e2/changes?page=1&pageSize=25" \
-H "Authorization: Bearer "
```
```json
{
"Data": {
"Items": [
{
"Id": "1e0b029f00ad49e7a3f193050414efd6",
"Operation": 1,
"ChangedAt": "2026-07-22T15:51:22.950314+00:00",
"Actor": { "Id": "3aba23d9-ef9b-4df8-a1cb-69a29781d5c4", "Name": "owner" },
"RecordContext": { "EntityDefinitionName": "SupportTicket", "DisplayNameSingular": "Support ticket" },
"FieldChanges": [
{ "Key": "ModifiedAt", "DisplayName": "Modified", "SystemField": 4,
"BeforeValue": null, "AfterValue": "2026-07-22T15:51:22.950314+00:00" },
{ "Key": "Priority", "DisplayName": "Priority", "SystemField": null,
"BeforeValue": "High", "AfterValue": "Medium", "FieldId": "l1eqb13B03NG" }
],
"TableChanges": [],
"AssignmentChanges": [],
"BusinessEvent": null
},
{
"Id": "f2992e9199d94ecb8c1bb6bc00533442",
"Operation": 0,
"ChangedAt": "2026-07-22T15:51:20.332602+00:00",
"Actor": { "Id": "3aba23d9-ef9b-4df8-a1cb-69a29781d5c4", "Name": "owner" },
"FieldChanges": [
{ "Key": "Number", "DisplayName": "Number", "SystemField": 0,
"BeforeValue": null, "AfterValue": "1" },
{ "Key": "Title", "DisplayName": "Title", "SystemField": null,
"BeforeValue": null, "AfterValue": "Printer in hall B is jammed" },
{ "Key": "Priority", "DisplayName": "Priority", "SystemField": null,
"BeforeValue": null, "AfterValue": "High" }
]
}
],
"TotalCount": 2,
"Page": 1,
"PageSize": 25
},
"Success": true
}
```
Each item carries the operation kind, the actor, the record context, and per-field `BeforeValue`/`AfterValue` pairs — for user-facing field kinds the values are display values (the Select field above reads `"High"`, not the option key). System columns (number, created/modified stamps) appear as `SystemField`-tagged changes next to schema fields. Child-table row changes, assignment changes, and — when the entry was written by code rather than a field edit — a `BusinessEvent` payload (see below) complete the shape. Deleted and archived records keep their history within the definition’s retention settings.
Two module surfaces have their own history reads with the same philosophy: board items expose [`history`](/docs/developer/api-reference/operations/boards-runtime-history/) and [`audit-trail`](/docs/developer/api-reference/operations/boards-runtime-audit-trail/), and Entitlement Operations keeps quantity truth in the [append-only ledger](/docs/developer/entitlement-operations/#step-7--the-ledger).
## Configuring audit per record type
[Section titled “Configuring audit per record type”](#configuring-audit-per-record-type)
Audit is a per-definition setting, and new definitions start with it **on** with unlimited retention. `PUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/audit-settings` ([operation](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-update-audit-settings/)) changes it:
```bash
curl -s -X PUT https://ops.example.com/api/workspace/admin/entity-definitions/pYZqCLZWb0K1/audit-settings \
-H "Authorization: Bearer " -H "Content-Type: application/json" \
-d '{ "AuditTrailEnabled": true, "AuditTrailRetentionMode": 1,
"AuditTrailRetentionDays": null,
"RowVersion": "a244a235-1dc6-4e8b-81cf-2db6b972cd55" }'
```
`AuditTrailRetentionMode` is `0` disabled, `1` keep forever, `2` keep a rolling window of `AuditTrailRetentionDays` days. The definition detail exposes the current state as `AuditTrailEnabled` — the [Integration quickstart](/docs/developer/integration-quickstart/#step-3--list-entity-definitions) discovery response shows it per record type, so an agent knows up front whether a record carries history.
## Audit from C# project code
[Section titled “Audit from C# project code”](#audit-from-c-project-code)
Workspace C# logic (the [Net Operation Project](/docs/developer/business-logic/csharp-business-logic/)) has an explicit relationship with the audit trail:
* **Record saves made by function code do not write automatic field-level audit.** Automatic save auditing is a host-runtime concern; for project code it is off and cannot be switched on per save. What users change through the product surfaces is audited by the platform; what your code changes is yours to narrate.
* **The supported emit path is a business event.** `MoltaroDbContext` exposes `AddBusinessEventAsync`, which appends a display-safe event to the record’s audit trail — it requires the definition to have the audit trail enabled:
```csharp
await Context.AddBusinessEventAsync(
ticketId,
new MoltaroBusinessEvent(
Title: "Escalated to on-site service",
Message: "SLA breach predicted; field engineer dispatched.",
Values:
[
new MoltaroBusinessEventValue("engineer", "Engineer", "K. Malek"),
new MoltaroBusinessEventValue("eta", "ETA", "2026-07-23")
]),
cancellationToken);
```
* **Business events surface in the change feed.** The entry arrives with the acting user and shows up in the `changes` endpoint as a `BusinessEvent` payload and in the WebApp record history — one narrative for humans and agents.
* **Reading audit from C# goes through the API, not the DbContext.** The generated workspace contract does not expose audit rows as queryable DbSets; read history through the `changes` endpoint above.
* **Boards and Entitlement application automation has fixed attribution.** `IBoardAutomationCommandService` and `IEntitlementAutomationCommandService` persist as `moltaro-system-automation`. Their origin metadata retains the original user, `FunctionRunId`, function identity/version, and `FunctionContext.CorrelationId`, so operators can join a function run to Boards history/resource events or the Entitlement audit/ledger. Automation request DTOs intentionally cannot replace that actor.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Access and governance](/docs/user/access-and-governance/) — roles, entity access models, and the permissions that gate history reads.
* The [workspace activity feed](/docs/developer/api-reference/operations/activity-feed-query/) — cross-record activity for dashboards and monitoring.
* [C# business logic](/docs/developer/business-logic/csharp-business-logic/) — the full programming model around the code shown here.
* [Reliable API automation](/docs/developer/reliable-api-automation/) - the cross-surface table for automatic record audit, explicit business events, Boards history, Entitlement ledger truth, and project publication state.
# Data structure
> How Moltaro models data, in the terms the product itself uses.
Data structure is where a Moltaro installation gets its shape. Configurators use entity definitions, fields, relationships, templates, catalogs, and classifiers to model the work people actually do.
This is the configuration-facing section. It uses the same terms the product shows in the Constructor, so what you read here matches what you see on screen.
## Terminology bridge
[Section titled “Terminology bridge”](#terminology-bridge)
User documentation and the running product use friendly terms; the Constructor and the API use system terms. They describe the same things:
| User docs say | The Constructor and API say | Meaning |
| ------------- | ------------------------------------- | -------------------------------------------------------------------------- |
| Record type | Entity definition | The configured shape for a kind of record: fields, access, logic, screens. |
| Record | Entity instance (often just “entity”) | One actual business object of that type. |
| Fields | Schema | The stored field list of an entity definition. |
When you work in the Constructor or call the [public API](/docs/developer/api-reference/), expect the system terms: the API groups are [Entity Definitions](/docs/developer/api-reference/operations/tags/entity-definitions/) and [Entity Instances](/docs/developer/api-reference/operations/tags/entity-instances/). When you read [Data and records](/docs/user/data-and-records/), expect the friendly terms.
## Where structure is configured
[Section titled “Where structure is configured”](#where-structure-is-configured)
Data structure lives in the **Constructor** area of the web app. Its Data model group contains:
* **Entity Explorer** — the list of entity definitions. Opening one shows the configuration sections documented under [Entity definitions](/docs/user/data-structure/entity-definitions/).
* **Catalogs** — hierarchical trees that organize records. See [Catalogs](/docs/user/data-structure/catalogs/).
* **Duplicate detection** — exact and fuzzy matching profiles that find likely duplicate records. See [Duplicate detection](/docs/user/data-structure/duplicate-detection/).
* **Templates** — guided starters that create ready-to-adjust configuration. See [Templates](/docs/user/data-structure/templates/).
## In this section
[Section titled “In this section”](#in-this-section)
* [Entity definitions](/docs/user/data-structure/entity-definitions/) — the main guide: schema, projection, location, access, transfer, logic, and UI surfaces of a record type.
* [Relationships and containment](/docs/user/data-structure/relationships-and-containment/) — references, inverse references, associations, and governed parent-child structure.
* [Duplicate detection](/docs/user/data-structure/duplicate-detection/) — configure exact or fuzzy rules and understand complete Details checks, partial bounded Table checks, and durable complete full scans.
* [Templates](/docs/user/data-structure/templates/) — guided starting points.
* [Catalogs](/docs/user/data-structure/catalogs/) — hierarchical organization.
* [Classifiers](/docs/user/data-structure/classifiers/) — shared reference values.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* Read [Data and records](/docs/user/data-and-records/) for the user-facing view of the same concepts.
* Read [Entity definitions](/docs/user/data-structure/entity-definitions/) to start configuring.
* Use the [API reference](/docs/developer/api-reference/) when an integration or agent needs to read definitions or work with records programmatically.
# Catalogs and classifiers
> Hierarchical classification and browsing across record types.
Catalog is the platform definition for a governed hierarchy. A **Classifier** is the currently supported Catalog kind for giving records reusable hierarchical categories such as product area, department, capability, region, or service family.
Classifier nodes are called **categories**, not folders. A category can be selected even when it has child categories. The logical Classifier root is only a navigation point and is never stored as a record value.
## How records participate
[Section titled “How records participate”](#how-records-participate)
A record type participates through an explicit single-value **Classifier field** bound to one Classifier. Several fields can use the same Classifier, and a record type can use several Classifiers when it needs independent classification axes.
The record stores the selected category identity. Moltaro resolves its current name and breadcrumb from the Classifier tree. Moving or renaming a category therefore updates the current display without rewriting every record. Audit history keeps the full breadcrumb captured when the field changed, so later tree changes do not rewrite historical evidence.
## Browsing a Classifier
[Section titled “Browsing a Classifier”](#browsing-a-classifier)
A Classifier can be added to a configured user menu. Its runtime page shows the category tree and one globally paged list containing records from all record types that use that Classifier.
* At the logical root, the page shows all classified records.
* For a category, **Category** shows direct assignments and **Entire branch** also includes descendants.
* Search, record-type filtering, common presentation sorting, and pagination work across the mixed result.
* Records the user cannot view appear only as restricted entries and do not reveal names, numbers, subtitles, or classification assignments.
Users with the required record and field update access can add records through the shared record picker or remove one concrete Classifier-field assignment. These actions use the ordinary record update flow, including validation, concurrency checks, formulas, side effects, and audit history.
## Access boundaries
[Section titled “Access boundaries”](#access-boundaries)
Catalog reader roles can discover and browse the Classifier. Catalog administrator roles can also maintain its category tree. Workspace Owners and Admins can manage it as well; Configurator has no implicit runtime access.
Catalog access never grants record access. Opening a result, changing a Classifier field, or clearing an assignment is checked again through the record type, record, and field permissions. Classifier-based row security is a separate future capability and is not implied by placing a record in a category.
# Classifiers
> Shared classification and reference data.
Classifiers are shared values used to describe records consistently. They are useful for categories, types, regions, priorities, industries, reasons, or other controlled choices that users should not type differently each time.
Use a classifier when the value is mainly a label for filtering, reporting, or consistent entry. Use a full record type when the object needs its own fields, relationships, history, access, or lifecycle.
In the product, a classifier is a kind of catalog: a reusable taxonomy whose nodes are administered from the catalog runtime. Simple per-field choice lists are different — those are Select field options configured on the entity’s schema (see [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/)). Reach for a classifier when several record types must share one taxonomy.
## Classifier fields on records
[Section titled “Classifier fields on records”](#classifier-fields-on-records)
A Classifier field binds one record field to one Classifier Catalog. Users pick one category from that Catalog, including a non-leaf category when the model requires it. Lists, cards, and details show the category’s current breadcrumb, so renaming or moving a category updates what users see without rewriting every record.
Selecting or changing a category requires read access to the bound Catalog in addition to the ordinary record and field permissions. Losing Catalog read access does not erase an already stored value or prevent unrelated record updates when that value stays unchanged.
Data Explorer filters support exact category, set, null, and branch matching. **Is in category branch** includes the selected category and all descendants. **Is not in category branch** excludes that branch and does not include records whose Classifier field is empty. Use the explicit null operators when empty records are required.
Branch filters require read or management access to the bound Catalog. Renaming or reordering a category does not change saved branch filters; moving a category updates their result immediately. If Catalog read access is lost, a saved view keeps the condition but cannot apply it until the condition is removed or access is restored.
## Classifier catalog browser
[Section titled “Classifier catalog browser”](#classifier-catalog-browser)
Administrators can place a specific Classifier in a user menu. Its runtime page combines the category tree with one paged list of classified records from every record type that uses that Classifier. The logical root shows all classified records. A selected category shows exact assignments by default and offers an **Entire branch** scope that includes descendants.
Catalog readers may browse the tree and content, but Catalog access does not grant access to the records themselves. Unreadable records appear only as restricted entries. Users with ordinary record and field update access can add records through the shared picker or remove one concrete field assignment. These changes use the normal record update pipeline and appear in the record’s audit history.
Audit history stores the full category breadcrumb captured when the field changed. Renaming or moving a category updates current record displays but does not rewrite older audit entries.
Validation Rules and Statements also support exact, set, and null comparisons. Use **Insert category** in either editor to choose a category and insert a portable `CATEGORY('catalog-key', 'path-key', ...)` value. Rules may compare two Classifier fields only when both use the same Classifier Catalog. Branch matching is a Data Explorer query capability, not a Validation Rule or Statement operator.
Creating or changing a Classifier expression requires Catalog read or management access. Existing Rules and Statements continue to run if the author later loses Catalog access; their other metadata can still be updated while the expression stays unchanged. A category used by a Rule or Statement cannot be deleted or moved with its subtree until that dependency is removed.
JSON data transfer represents a Classifier value as stable category `PathKeys` relative to its Catalog. CSV export writes the same path as a JSON array cell.
# Duplicate detection
> Configure exact and fuzzy rules, then run complete record checks or durable full scans without automatic merge.
Duplicate detection helps users review records that may represent the same business object. A configurator creates a matching profile for one record type, adds exact or fuzzy rules, and chooses whether users can launch the check from a record list, a record’s Details page, or both.
Moltaro does not merge, delete, or block records automatically. A user reviews the returned candidates and decides what to do next.
## Configure a profile
[Section titled “Configure a profile”](#configure-a-profile)
Open Constructor > **Duplicate detection**, select a record type, and create a profile. Fields inside one rule must all match; separate enabled rules are alternatives.
An **Exact normalized** rule compares normalized values:
* email ignores case and surrounding whitespace;
* phone ignores punctuation and requires at least seven digits;
* text key ignores case, surrounding whitespace, and repeated spaces;
* date compares the stored calendar date.
A **Fuzzy** rule contains at least one fuzzy text field. It may also contain exact guard fields. For example, an exact customer type or country can prevent similar names in another business scope from being returned.
Fuzzy comparison is available for String and Text fields with the TextKey normalizer. Configure each fuzzy field with:
* maximum edit distance from 1 to 3;
* minimum text length;
* weight from 1 to 100;
* preserved or ignored diacritics;
* ordered tokens or tokens compared in any order.
Moltaro uses Unicode-aware Damerau-Levenshtein distance. It recognizes bounded insertions, deletions, substitutions, and adjacent transpositions. Ignoring diacritics removes Unicode combining marks; it is not transliteration. `Ł` and `L`, for example, are still different unless Unicode itself decomposes the character. **Any order** sorts whitespace-separated tokens and preserves duplicates. Punctuation remains significant.
Every fuzzy field must pass its own distance and minimum-length settings. A highly similar field cannot compensate for another field that fails. Weights affect the effective rule score only after all required fields pass. The profile’s minimum score and confidence thresholds are then applied to the effective group score. These values are deterministic match indicators, not statistical probabilities.
Empty or invalid required values do not match. A readable value that exceeds a hard safety limit fails the run without exposing the value, record id, or hidden match count.
## Understand groups and pairs
[Section titled “Understand groups and pairs”](#understand-groups-and-pairs)
Exact rules can create a group containing two or more records with the same normalized key. Fuzzy matching always returns an honest pair of exactly two records. If A is similar to B and B is similar to C, but A is not similar to C, Moltaro shows A+B and B+C—not an invented A+B+C cluster.
If several rules find the same pair, the drawer shows one pair with all matched rules. It displays the effective score and confidence plus safe evidence such as distance, threshold, similarity, and weight. Raw and normalized matching values are not returned as matching evidence.
## Check one record from Details
[Section titled “Check one record from Details”](#check-one-record-from-details)
The Details page runs a complete anchored check over every active or archived record the current user can read. It is not limited to the first page or the first 250 visible records. Exact rules use an optimized database candidate query; fuzzy rules stream bounded readable values and apply the same matching contract used by full scans.
The successful result is complete and shows either matching groups/pairs or a complete empty state. At most 250 distinct candidates can be reviewed in one anchored result. If there are more, Moltaro stops without returning partial groups and reports that the anchored match is too broad. Narrow the rules before retrying.
Moltaro also stops with a retryable availability error if the complete request exceeds its five-second execution budget. The result drawer shows the localized server message. Closing the drawer or cancelling the client request cancels the work and does not return a partial result.
## Check a record type from a list
[Section titled “Check a record type from a list”](#check-a-record-type-from-a-list)
The first Table page check remains deliberately fast and bounded: it scans at most 250 actor-visible records and labels the result as partial whenever more visible records exist. Constructor previews use the same bounded behavior. A partial empty result does not prove that the full record type contains no duplicates.
If fuzzy rules would produce more groups than a safe interactive response can hold, Moltaro rejects the preview without showing a truncated result. Refine the rules or use the durable full scan after narrowing the configuration.
From a partial Table result, select **Scan all records** to enqueue a complete scan. The request returns immediately; the existing Worker Host performs exact and fuzzy matching in the background and the same drawer shows queued, counting, matching, saving, completed, failed, or cancelled state. You may close the drawer or reload the Table page. Reopen duplicate detection to recover the latest saved scan for the same profile and active/archive scope.
A completed full scan covers the complete actor-visible point-in-time scope and replaces partial groups. Candidate groups and long candidate lists are paged from the saved result. The Table page launcher and group review remain desktop-only; mobile record cards do not expose these target-wide operations.
Full scans have finite execution, comparison, memory, and retained-result budgets. A very broad or pathological result fails without publishing partial groups. A transiently failed scan can retry; cancellation is explicit. Moltaro does not start scheduled scans automatically and does not add a separate job center for duplicate detection.
One complete full scan supports up to eight enabled rules, up to four fields in each rule, and up to sixteen distinct matching fields in total. The settings screen validates fuzzy profiles against the rule bound and validates every fuzzy field setting. A legacy exact profile outside the full-scan shape can still run a bounded review but cannot enqueue a complete scan.
## Access and privacy
[Section titled “Access and privacy”](#access-and-privacy)
Duplicate detection uses the same record and field read rules as the rest of Moltaro. Inaccessible records and unreadable matching fields do not contribute to a result. Unreadable large values are not length-inspected or transferred. Error responses do not reveal hidden record ids, raw or normalized field values, or the number of hidden matches.
The full scan is pinned to the requesting user and the profile, entity, access, and archive scope that were current when it was queued. Moltaro rechecks access before processing, immediately before publishing success, and whenever a saved result is opened. If access, matching configuration, the entity definition, or a candidate record changes, the old result becomes unavailable instead of being shown partially or with stale data. Unrelated record changes can continue while the Worker scans; the result is a coherent point-in-time review rather than a workspace-wide write lock.
## Current boundaries
[Section titled “Current boundaries”](#current-boundaries)
Duplicate detection does not provide semantic or vector similarity, nickname dictionaries, transliteration, language-specific phonetics, scheduled scans, automatic merge, or create/edit blocking. Record merge, where enabled separately, remains an explicit reviewed action.
## Related guidance
[Section titled “Related guidance”](#related-guidance)
* [Data structure](/docs/user/data-structure/) explains where profiles fit in Constructor configuration.
* [Record matching for developers](/docs/developer/record-matching/) describes the API, additive fuzzy configuration, saved-scan lifecycle, and portable Entity Definition YAML contract.
* [Records](/docs/user/data-and-records/records/) explains record lists and Details pages.
* [Errors and responses](/docs/developer/errors/) documents the API envelope and stable error-code contract.
# Entity definitions
> How record types are configured in Moltaro.
An entity definition is the configuration behind a record type. It controls the fields, display values, Entity Security configuration, runtime screens, relationships, data transfer behavior, and logic that users experience when they work with records.
Use these pages when you configure Moltaro or need to understand why a record type appears, behaves, or enforces rules in a particular way.
## How this section is organized
[Section titled “How this section is organized”](#how-this-section-is-organized)
Start with [What an entity definition is](/docs/user/data-structure/entity-definitions/what-is-entity-definition/) and [General settings](/docs/user/data-structure/entity-definitions/general-settings/) when you need the basic identity and purpose of a record type. The rest of this section follows the same mental model as the Entity Definition screen.
### Data model
[Section titled “Data model”](#data-model)
Use these pages for the shape of record data and where that data comes from:
* [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/)
* [Projection](/docs/user/data-structure/entity-definitions/projection/)
* [Location](/docs/user/data-structure/entity-definitions/location/)
### Governance
[Section titled “Governance”](#governance)
Use these pages when the question is who can see or change records, or how data moves in and out under control:
* [Entity security](/docs/user/data-structure/entity-definitions/access-model/)
* [Data transfer](/docs/user/data-structure/entity-definitions/data-transfer/)
### Automation & logic
[Section titled “Automation & logic”](#automation--logic)
Use these pages when a record type needs rules, calculated behavior, or consistent user-facing meaning. The Constructor splits this into four tabs — Mutation effects, Logic, Validation, and Object context facts — covered together in the logic overview:
* [Logic overview](/docs/user/data-structure/entity-definitions/logic-overview/)
* [Presentation and statements](/docs/user/data-structure/entity-definitions/presentation-and-statements/)
### User interface
[Section titled “User interface”](#user-interface)
Use these pages when the question is how records appear in the running product:
* [Display fields](/docs/user/data-structure/entity-definitions/display-fields/)
* [Runtime screens](/docs/user/data-structure/entity-definitions/runtime-screens/)
* [UI surface library](/docs/user/data-structure/entity-definitions/ui-surface-library/)
* [Card item layout](/docs/configuration/card-item-layout/)
# Entity Security
> How Security Statements govern who can see and change records.
An Entity Definition is governed by Security Statements assigned to Permissions. A Security Statement can evaluate the current user, stable role keys, responsibilities, record data, references, and a captured workspace clock. There are no Entity access modes and no inherited-access maintenance jobs.
## Root and action permissions
[Section titled “Root and action permissions”](#root-and-action-permissions)
`View` is the root permission. If no valid enabled Security Statement is assigned to `View`, Moltaro blocks access to Entity records. Every record action requires `View` plus its exact permission, for example:
* update: `View + Update`;
* archive or delete: `View +` the exact lifecycle action;
* restore: `View + ReadArchive + Restore`;
* comments, attachments, and tags: `View +` the exact ancillary action.
Create also checks each supplied `Field:CreateWrite`, applies configured initial responsibilities, and verifies final `View`. If final `View` is denied, the complete create transaction rolls back and does not reveal the attempted ID.
## Responsibilities
[Section titled “Responsibilities”](#responsibilities)
Responsibility Definitions are stable, named business responsibilities such as Owner, Assignee, or Reviewer. They define the allowed subject type, whether one or many subjects can be assigned, whether assignment is required, whether future scheduling is allowed, eligible Responsibility Groups, and whether the responsibility is enabled. Assignment Rules separately authorize `Add`, `Replace`, and `Close`; `Update` does not grant assignment authority.
Assignments use users or Responsibility Groups and are stored as immutable, auditable change sets. Their history remains available after a definition is disabled when the caller has `View + Responsibilities:History`.
## Field access
[Section titled “Field access”](#field-access)
`Field:Read`, `Field:CreateWrite`, and `Field:Write` are independent permissions. A denied field is absent from record JSON, nested rows, table metadata, display values, and reference previews. Search requires `Field:Read`; conditionally readable fields cannot be used for sort, filter, grouping, or aggregation unless access is proven unconditional for the captured actor and clock.
When no field-specific assignment exists, the field inherits its root permission: read inherits `View`, create-write inherits `Create`, and write inherits `View` or `Update`. Explicit field assignments are global allow-lists for that exact field, not local denies.
## Query isolation
[Section titled “Query isolation”](#query-isolation)
Moltaro applies the Security Statement SQL predicate before user filters, count, and pagination. Direct reads and list reads use the same authorization snapshot, so hidden rows do not create page gaps or disclose their positions.
Owner and Admin have no implicit access to Entity records. Administration access never substitutes for `View` or field permissions.
When Moltaro creates a new standalone Entity Definition, it also creates an ordinary **Admin access** policy. The policy grants the built-in Admin role all record-level permissions available at that moment; fields inherit the matching record permissions. This saves the initial setup step but does not create a hidden Admin bypass. You can edit or delete the policy, and Moltaro will not restore it during system or package reconciliation. Existing Entity Definitions are not changed automatically, and an imported Entity Definition keeps its explicit Security configuration, including an intentionally empty one.
## Manage Entity Security in the WebApp
[Section titled “Manage Entity Security in the WebApp”](#manage-entity-security-in-the-webapp)
Open the Entity Definition and use the three entries under **Security**:
* **Responsibilities** configures responsible subjects and assignment rules;
* **Access Policies** creates or edits a condition together with its exact record, lifecycle, transfer, collaboration, history, and responsibility Permissions. Review the server Plan before applying the policy atomically;
* **Permissions** manages explicit field refinements and displays inherited fields. It also provides the manual normalization Plan for refinements whose Statement-key sets exactly match their root permission.
Security Statements remain separate from calculated Entity Statements. Saved policy changes preserve the canonical Statement and Permission Assignment runtime model. **Raw configuration — Advanced** keeps the immediately effective expert endpoints available and invalidates any outstanding Plan.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* Read [Access and governance](/docs/user/access-and-governance/) for the product-wide access concepts (roles, permissions, and audit).
* Read [Responsibility and assignments](/docs/user/working-with-records/responsibility-and-assignments/) for how users work with assignments day to day.
* Read [On-premise operations](/docs/operations/) before upgrading a workspace from the legacy Entity access model.
# Data transfer
> Import and export profiles for governed movement of entity records.
The **Transfer** tab defines named data transfer profiles for a record type. A profile controls which fields move, how records are matched, and how references are represented — so imports and exports stay predictable and governed.
## Profile settings
[Section titled “Profile settings”](#profile-settings)
* **Direction** — import, export, or both.
* **Format** — JSON is the implemented format; CSV ZIP export exists for tabular delivery.
* **Identity mode** — how incoming rows match existing records: by record id, by record number, or by a chosen field value.
* **Fields** — which fields participate, with per-field import and export participation.
* **Import mode** — create only, update only, or create-or-update.
* **Table handling** — how child table rows are applied: replace all rows or upsert by row id.
* **Reference resolvers** — how Reference and Inverse reference fields are represented in the payload: by referenced record id (default), by record number, or by one selected field of the target.
Export profiles appear in the Data Explorer export menus; import profiles appear in its import flow. Imports run as a plan-then-apply flow, so changes can be reviewed before they are applied.
## Records, not configuration
[Section titled “Records, not configuration”](#records-not-configuration)
Data transfer moves **record data**. Moving the entity *configuration* itself (the definition with its fields and settings) is a separate definition export/import feature available from the entity definition header menu.
## Data transfer in the API
[Section titled “Data transfer in the API”](#data-transfer-in-the-api)
Integrations use the [Entity Data Transfer](/docs/developer/api-reference/operations/tags/entity-data-transfer/) operations: list export and import profiles, export JSON or CSV ZIP, fetch an import template, and run import plan and apply steps.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* Read [Import and export](/docs/user/working-with-records/import-and-export/) for the user-facing workflow.
* Read [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/) for the field types that profiles move.
# Display fields
> Read-time display-only values for table and card surfaces.
The **Display fields** tab defines read-time string values that table and card surfaces can render — computed labels, badges, and summaries that make grids and cards readable without opening every record.
Display fields are display-only: they are not searchable, sortable, filterable, or editable, and they do not add stored fields to the data model.
## Ready example: Full name
[Section titled “Ready example: Full name”](#ready-example-full-name)
Create a Display Field named **Full name** and keep its final **Otherwise** rule. Use this value template:
```text
{LastName} {FirstName}
```
Replace `LastName` and `FirstName` with the exact field keys from the entity schema. The result is evaluated when a record is read and can then be selected as a Table column or Card item.
## How a display field works
[Section titled “How a display field works”](#how-a-display-field-works)
Each display field has a key, a display name, and an ordered rule list. Every rule pairs a DSL **condition** with a **value template**; rules are evaluated top to bottom, the first matching condition renders its template, and the list always ends with an **Otherwise** rule so a value is always produced. Condition and template syntax is documented in the [expression language reference](/docs/dsl/rule-expressions/).
## Three display mechanisms, three purposes
[Section titled “Three display mechanisms, three purposes”](#three-display-mechanisms-three-purposes)
* **Display name and subtitle rules** (Overview tab) compute how the record is labeled everywhere — see [Presentation and statements](/docs/user/data-structure/entity-definitions/presentation-and-statements/).
* **Display fields** (this tab) add extra read-time values that table and card surfaces can place as columns or items.
* **Stored fields** (Schema tab) hold the actual data — see [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/).
For derived stored values, choose a [Calculated Field](/docs/dsl/calculated-fields/) when the supported formula language is sufficient, or a C# [before-save mutation function](/docs/developer/business-logic/entity-scoped-logic/) when the derivation needs procedural logic. Neither is a replacement for a Display Field: they persist data, while a Display Field renders read-time text.
API clients replace the complete ordered Display Field rule set through `PUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/display-fields`. Use the installation-local Configuration OpenAPI document for the exact request schema and current `RowVersion` contract.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Runtime screens](/docs/user/data-structure/entity-definitions/runtime-screens/) and [UI surface library](/docs/user/data-structure/entity-definitions/ui-surface-library/) for the surfaces that consume display fields.
# Fields and schema
> The field types an entity definition can store and the schema rules behind them.
The **Schema** tab of an entity definition defines the stored fields of a record type. In the Constructor, open **Entity Explorer**, select an entity definition, and choose **Schema** in the Data model group.
Fields live on the entity’s primary table or inside nested **Table** fields (child rows). Every field has a key, a display name, a field type, and rules that control when it is required, visible, and editable.
## Field types
[Section titled “Field types”](#field-types)
These are the field types the Constructor offers, using the same names you see in the field editor:
| Field type | What it stores |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Yes/No | A true/false value. |
| Date | A calendar date without a time component. |
| Time | A time of day without a date. |
| Date and time | One exact moment. It is displayed and edited in the workspace time zone as one minute-precision field; users do not edit an offset. |
| User | A link to a workspace user. This is people-valued data; it does not grant access by itself. |
| Role | A link to a workspace role. |
| Whole number | An integer value. Can carry the Line number semantic role (see below). |
| Decimal | A number with configured precision (1–28) and scale. Precision and scale are required settings. |
| File | A governed workspace file reference. File access is checked when the file is attached; storage goes through Moltaro file services. |
| Address | A structured postal address (full address, street lines, locality, region, postal code, country) with optional coordinates. A field setting can require coordinates. Address fields feed location and map behavior. |
| Select | One value from an admin-defined option list with stable option keys and display labels. The **Allow multiple** setting turns it into a multi-select that stores several option keys. |
| Classifier | One category from a bound hierarchical Classifier Catalog. It is available on the primary table, stores one category, and displays the category’s current breadcrumb. |
| String | Single-line text with a maximum length (default 512, up to 4096). |
| Text | Long multi-line text without a length cap. |
| Table | Repeatable child rows with their own columns. Rows belong to the owning record and have no independent identity, access, or detail page. |
| Money | An amount plus an ISO currency code. A base currency is a required setting. |
| Reference | A link to a record of another entity definition. See [Relationships and containment](/docs/user/data-structure/relationships-and-containment/) for reference settings. |
| Inverse reference | The reverse of a Reference: the records whose paired Reference field points at this record. It stores no value of its own and cannot be required. Writing it as a list of record ids rewrites the paired Reference on each affected source record. |
Two facts worth knowing when you model data:
* Use **Date** for a calendar fact and **Time** for a time-of-day fact. Use **Date and time** only when the value must identify one exact moment. The latter stores one UTC instant and does not preserve the sender’s original offset or a named time zone.
* Changing a field’s type deletes the data already stored in that field. The Constructor warns before this happens, and the action cannot be undone.
## Markdown fields
[Section titled “Markdown fields”](#markdown-fields)
A configurator can give a **String** or **Text** field the **Markdown** semantic role. The field still stores Markdown source, but Moltaro presents it according to the screen:
* record details and preview drawers show formatted headings, lists, task lists, tables, quotes, emphasis, links, and code;
* the field label includes a **Full screen** action when a saved value is not empty, opening the same formatted content in a full-screen reader;
* lists, history, child tables, and Board cards show a compact plain-text excerpt instead of Markdown punctuation or link destinations;
* create and edit forms use a Markdown editor with **Source**, **Preview**, and desktop **Split** modes, formatting actions, undo/redo, and a field-level full-screen editor. The full-screen editor changes the same draft and does not save or cancel the outer form;
* record, Association-create, Board-target, and Board Data forms can add governed PNG, JPEG, and WebP images by upload, clipboard paste, drag and drop, or selection from the File System. The image is attached to its exact record or Board Data owner only when the outer form saves successfully.
On a small screen, use **Source** or **Preview**; Split is intentionally hidden. Closing a full-screen reader or editor returns focus to the button that opened it. The editor preserves whitespace, line endings, and Markdown punctuation in the value you submit.
Moltaro supports a fixed safe Markdown subset. Raw HTML, scripts, iframes, remote images, `data:`/`blob:`/`file:` images, SVG, raw file endpoints, and executable URL schemes are not rendered or fetched. Inline images are inspected before display; pending, quarantined, rejected, missing, or unauthorized content shows no file body. External web links open separately. Oversized or excessively complex source is rejected when the record is saved.
Association create keeps its image draft with the Entity being created. A composite Board create keeps target Entity and Board Data drafts separate; Board Data edit uses the Board item destination. Rich Board Data details resolve through that Board destination, while target Entity details retain normal Entity authorization. Board cards, lanes, headers, search, history, and tooltips remain plain-text previews and never fetch image bodies.
## Required, visible, and editable are rules
[Section titled “Required, visible, and editable are rules”](#required-visible-and-editable-are-rules)
Field behavior is conditional, not just a checkbox:
* **Required**, **Visible**, and **Editable** are each a rule that can be always on, always off, or a condition over the record’s data.
* Each rule can apply to record creation, record update, or both.
* A separate **Read-only** setting makes the runtime skip writes to the field entirely.
This is why the same field can be required in one situation and optional in another, or editable during creation but locked afterwards.
## Default values
[Section titled “Default values”](#default-values)
A field can define an initial value for new records. Configure it in the field editor under **Default value**:
* **Fixed value** uses a value you enter now.
* **Current date** is available for Date fields and is resolved in the workspace time zone when the record is created.
* **Current date and time** is available for Date and time fields and is resolved when the record is created.
Fixed defaults are available for Yes/No, Whole number, Decimal, String, Text, Money, Select, scalar Reference, Date, Time, and Date and time fields. A multi-select default must contain one or more distinct configured options. A Reference default is selected through the existing record picker; only the workspace-local record ID is stored, while Number and Display Name are resolved according to current access. Defaults are not available for calculated fields, Line number fields, User, Role, File, Address, Classifier, multiple Reference, Inverse reference, or Table itself. Fields inside a Table can still have their own defaults for each new child row.
Defaults follow a strict missing-value rule:
* Moltaro uses the default only when the field is absent while a new record or child row is created.
* An explicitly cleared value, `false`, or `0` is kept and is not replaced.
* Updating an existing record never reapplies a default.
* Adding or changing a default does not update existing records.
Reference defaults are checked again when a record or new child row is saved. If the selected record was deleted, archived, became inaccessible, belongs to a different record type, or no longer satisfies Reference Eligibility, the save fails without partial changes or disclosure. A user can choose a permitted replacement or explicitly clear the field. Existing records are never rewritten.
Create forms show fixed values so the user can review or change them. A **Current date** default also shows the workspace-local date in its ordinary Date input, so it can be edited. This is a preview: if it is left untouched, the field is omitted from the request and Moltaro resolves the workspace date again when the record is actually created. Changing or clearing the preview sends that explicit value instead. An open create form updates an untouched preview if the workspace date changes at midnight. **Current date and time** continues to stay empty because the server resolves the exact instant on save. Reference defaults are hydrated into the normal Reference control before the first create-form rule evaluation. An untouched selection stays schema-owned and is omitted from the request; replacement or clear is sent explicitly. The same behavior applies to Date fields in new Table rows. Edit forms always show the stored value and never reapply a default.
## Other schema rules
[Section titled “Other schema rules”](#other-schema-rules)
* **Field behavior conditions** control whether a field is visible, required, or editable in Create and Update operations. They can read fields in the same record and one scalar through a direct Reference, for example `FacilityType.KoboName = 'poe'`. Generated Forms load that referenced value automatically, including after a new selection. Invalid saved conditions block the Form with a configuration error instead of applying an incorrect result. See [Field behavior conditions](/docs/dsl/field-behavior/).
* **Uniqueness and indexes** are entity-owned constraints defined over one table on the Schema tab, not a per-field flag.
* **Calculated fields** store an expression instead of accepting manual input. A local calculation derives a value from the same record; an aggregate calculation (for example `SUM(Positions, TotalPrice)`) summarizes the child records of an Inverse reference field.
* **Statements** are named boolean conditions defined on the Schema tab. Screens and rules reuse them for filtering and highlighting; see [Presentation and statements](/docs/user/data-structure/entity-definitions/presentation-and-statements/).
* **Search targets** are explicit paths configured on the Search tab. String and Text fields are searchable directly; Address uses its full address and File uses file name and description. Reference, Inverse reference, and Table fields are path segments rather than terminals. Display name, number, comments, and attachments are supported system terminals. Paths may cross up to the configured relation depth, and runtime field and record access is enforced at every segment. See [Record search](/docs/configuration/entity-search/) for matching semantics, path examples, and change guards.
* **Semantic roles** add meaning on top of a type: String and Text fields can be marked as Markdown for rich rendering, and a Whole number field on the primary table can act as a Line number that the runtime maintains as a dense counter within a group defined by a Reference field.
## Schema in the API
[Section titled “Schema in the API”](#schema-in-the-api)
Integrations read the configured schema and write field values through the public API:
* [List entity definitions](/docs/developer/api-reference/operations/entity-definitions-list/) and [return one entity definition](/docs/developer/api-reference/operations/entity-definitions-get/) expose the schema of each record type, including field keys and types.
* The [Entity Instances](/docs/developer/api-reference/operations/tags/entity-instances/) operations create, read, query, and update records. Instance payloads return a `Fields` dictionary keyed by field key; each entry carries a `State` and a `Value`, and restricted fields are present with no value.
* Money amounts and Decimal field-envelope values are serialized as strings in API JSON to avoid floating-point precision loss. Decimal strings are padded to the field’s configured scale. Reference values resolve to a compact payload with a `Resolved`, `NotFound`, or `Restricted` state.
* Date-and-time values are ISO 8601 timestamps with an explicit `Z` or numeric offset. Moltaro returns their equivalent UTC value and preserves precision through microseconds.
* Field definition responses expose the typed `DefaultValue` configuration. Entity Instance create requests may omit a field to use its default. Sending the key with `null`, `false`, `0`, an empty string, or an empty selection is explicit caller input and suppresses the default.
* A Markdown field is identified by its `SemanticRole`; its `Value` remains the exact Markdown source. Read envelopes can also include `PlainTextPreview` for compact clients. Integrations must not treat that preview as the editable or transferable value, and they must not render the source as trusted HTML. Inline attachment tokens are opaque identifiers, not URLs or access grants; clients must use the authenticated destination-scoped Markdown resolver instead of building a file endpoint.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* Read [Relationships and containment](/docs/user/data-structure/relationships-and-containment/) for references, inverse references, and record-to-record links.
* Read [Fields and field types](/docs/user/data-and-records/fields-and-field-types/) for the user-facing view of fields.
* Read [Projection](/docs/user/data-structure/entity-definitions/projection/) for how field values are prepared for lists and screens.
# General settings
> Identity, features, audit, and display settings on the entity Overview tab.
The **Overview** tab of an entity definition collects the settings that give a record type its identity and baseline behavior. Four cards matter most: Base settings, feature switches, Audit settings, and Display settings.
## Base settings
[Section titled “Base settings”](#base-settings)
* **Internal name** — the technical identifier used for storage and APIs. It must start with a Latin letter and be unique ignoring case.
* **Display name (singular)** and **Display name (plural)** — the user-facing labels for one record and for lists and sections.
* **Status** — a computed label: Active, Limited (read-only or some operations disabled), or Pending deletion.
* **Read-only** — blocks create, update, archive, restore, delete, assignments, and import for the whole record type.
* **Operation locks** — a finer runtime operation policy: disable specific actions (Create, Update, Archive, Restore, Delete, Assignments, Import, Export) without making the whole entity read-only.
## Feature switches
[Section titled “Feature switches”](#feature-switches)
Per-entity toggles for **Comments**, **Attachments**, and **Tags**.
Search is configured separately on the **Search** tab. Add one or more explicit paths to scalar fields, related records, child Table rows, or supported system content. Search is unavailable while the target list is empty.
## Audit settings
[Section titled “Audit settings”](#audit-settings)
* **Audit trail enabled** — whether record changes are recorded as governed history.
* **Retention policy** — keep audit entries forever or for a fixed number of days.
Association entities always keep their audit trail on.
## Display settings
[Section titled “Display settings”](#display-settings)
* **Number prefix** — an optional prefix for auto-generated record numbers.
* **Backend default sort** — an ordered list of sort targets used by API list queries when the caller does not send an explicit sort. When empty, the fallback is record number, then record id.
* **Display name rule** and **Subtitle rule** — ordered presentation rules that compute each record’s display name and subtitle for lists, lookups, references, cards, dashboards, and reports. See [Presentation and statements](/docs/user/data-structure/entity-definitions/presentation-and-statements/).
## Security overview
[Section titled “Security overview”](#security-overview)
Entity record access is owned by Security Statements linked through Permission Assignments. Root `View` controls discovery, actions and fields refine that access, and governed Responsibilities plus Assignment Rules control responsibility changes. Configure these artifacts in the three Security sections of the Entity Definition; see [Entity security](/docs/user/data-structure/entity-definitions/access-model/).
# Location
> Coordinate sources for geo classification of records.
The **Location** tab configures where a record type reads coordinates for geo classification. It is about geography, not hierarchy: records with coordinates can be classified into geo zones and used in maps, geo filters, and location-aware responsibility.
## Location sources
[Section titled “Location sources”](#location-sources)
The tab manages a list of named location sources. For an entity definition, a source reads coordinates from one **Address field** on the entity’s schema (see [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/) for Address fields and their optional required-coordinates setting).
Each source has a key, a display name, and an enabled flag. Classification runs against all enabled geo zone sets in the workspace; no zone binding is configured on this tab.
## What users experience
[Section titled “What users experience”](#what-users-experience)
When location sources are configured and records carry coordinates, records participate in map views, geo zone filters, and geography-based responsibility features. Records without coordinates simply stay unclassified.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/) for the Address field type.
* Geo zones themselves are administered in the Administration area, outside the entity definition.
# Logic overview
> The four Automation & logic surfaces of an entity definition.
The **Automation & logic** group of an entity definition has four tabs. They hold different kinds of logic, and knowing which is which saves time:
| Tab | What it holds |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| Mutation effects | Declarative field copies configured without code. |
| Logic | Workspace C# functions bound to this entity (validation, before-save mutations, triggers, actions). |
| Validation | Object-level rules written in the Moltaro expression language (DSL). |
| Object context facts | Module facts (Boards, Entitlements) materialized onto records. |
## Mutation effects
[Section titled “Mutation effects”](#mutation-effects)
Configuration-only effects that copy values into fields during create and update. Each effect has a target field, a same-record or Reference source path, trigger fields, an apply-on mode (create, update, or both), an assignment mode (set-if-empty or overwrite), and an optional **Apply when** condition.
**Always** is the default. **Never** keeps the effect configured but excludes it from the current save. A typed expression can read fields on the current record, `Operation.Mode`, and one Reference hop, for example:
```text
UsePrimary = TRUE AND Product.Price > 0
```
Fields read at the root of the condition act as additional triggers. Changes to the referenced Product do not start a bulk backfill; the condition is re-evaluated when the order is created, edited, or previewed in its Form.
When multiple set-if-empty effects target the same field, Moltaro uses their global order. If the current winner becomes false, the next active non-empty source becomes the fallback. If none remains, Moltaro clears the value only when it is still owned by effects; a user or Package SDK value is preserved. If a Reference or field cannot be read safely, the previous value is retained and no lower fallback is chosen.
`VisibleWhen` is different: it only controls whether a Form shows a field. A hidden field is not automatically cleared and visibility never substitutes for `ApplyWhen`.
## Logic (workspace C#)
[Section titled “Logic (workspace C#)”](#logic-workspace-c)
A diagnostics view of the C# functions bound to this entity from the workspace operation project: **Validation** functions that block invalid saves, **Calculations** (before-save mutation functions that normalize or derive fields), and **Triggers** (asynchronous post-commit handlers). Create actions scaffold C# templates and open the development studio. Authoring details live in [Business logic](/docs/developer/business-logic/).
## Validation (DSL rules)
[Section titled “Validation (DSL rules)”](#validation-dsl-rules)
Object-level checks written as boolean expressions that must return TRUE before a record can be saved. Each rule has a stable key, an expression, a failure message with per-locale translations, an enabled toggle, and an order — enabled rules run in order, and the first failing rule produces the message users see.
DSL rules cover required-if conditions, numeric ranges, date ordering, pattern matching, and depth-one reference reads. They intentionally exclude inverse-reference traversal, child-table aggregates, and raw SQL; use workspace C# for those. DSL validation runs before C# business rules. The full expression syntax is documented in the [expression language reference](/docs/dsl/validation-rules/).
## Object context facts
[Section titled “Object context facts”](#object-context-facts)
ID-only wiring that pulls module facts onto entity records: the current board item state for selected boards, or active entitlements for selected entitlement models. Configured facts become bindable columns and items on table and card surfaces and Data Explorer filters.
## Evaluation order
[Section titled “Evaluation order”](#evaluation-order)
During a save: mutation effects run first, then local calculated fields, then the record persists, then aggregate calculations refresh. Validation (DSL, then C#) gates the save itself.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Presentation and statements](/docs/user/data-structure/entity-definitions/presentation-and-statements/) for display-oriented rules.
* [Business logic](/docs/developer/business-logic/) for authoring, runtime behavior, and diagnostics of C# and declarative logic.
# Presentation and statements
> Display name rules, subtitle rules, and reusable named conditions.
Two mechanisms turn record data into consistent user-facing meaning: presentation rules compute each record’s display name and subtitle, and statements name reusable boolean facts.
## Display name and subtitle rules
[Section titled “Display name and subtitle rules”](#display-name-and-subtitle-rules)
Configured under Display settings on the entity **Overview** tab. Each target (display name, subtitle) has an ordered rule list; rules are evaluated in order, and the first matching rule wins.
A rule has one of three modes:
* **Field** — use one field’s value;
* **Template** — compose a string from several values;
* **Conditional** — apply only when a DSL condition matches (empty condition means always).
Condition and template syntax is documented in the [expression language reference](/docs/dsl/presentation-rules/).
The results are presentation-cache values used everywhere a record is shown compactly: lists, lookups, reference labels, cards, dashboards, and reports. Until a display name rule matches, records fall back to their record number.
Do not confuse these with [Display fields](/docs/user/data-structure/entity-definitions/display-fields/), which are additional read-time values for table and card surfaces.
## Statements
[Section titled “Statements”](#statements)
Statements are named boolean conditions defined on the entity **Schema** tab. Each statement is a reusable predicate over the record’s data (for example, “is overdue” or “needs review”).
Screens and rules reuse statements consistently: they appear as Data Explorer filters, drive grid and card highlighting, and keep the meaning of a business condition in one place instead of re-writing the expression on every surface. The statement expression syntax is documented in the [expression language reference](/docs/dsl/statements/).
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Display fields](/docs/user/data-structure/entity-definitions/display-fields/) for computed labels on tables and cards.
* [Logic overview](/docs/user/data-structure/entity-definitions/logic-overview/) for validation and mutation logic.
* [Business logic](/docs/developer/business-logic/) when the topic becomes authoring and diagnostics.
# Projection
> Publishing one entity over multiple source record types.
A projection entity publishes one unified record type over several source entity definitions. The classic example: separate `Product`, `Service`, and `CustomOffering` entities projected into one `Offering` entity so screens, references, and reports can treat them as one list.
Each projection row points to exactly one source record through one member reference field. Opening a projection row opens its source record.
## What the Projection tab configures
[Section titled “What the Projection tab configures”](#what-the-projection-tab-configures)
* **Target references (members)** — one member reference per source entity definition. These define which source types the projection unifies.
* **Field mappings** — per member, map a source value to a projection field or to the projection’s display name or subtitle. Values are copied from the source record.
* **Synchronization** — projections stay synchronized automatically; the tab shows source versus projection row counts and any issues, and offers a manual rebuild with detailed counters.
## Configuration workflow
[Section titled “Configuration workflow”](#configuration-workflow)
1. Create a normal data entity definition (the projection, e.g. `Offering`).
2. Add ordinary fields the projection should carry: `DisplayCode`, `Price`, `Currency`.
3. Add one **single-value Reference field per source** (e.g. `ProductRef`, `ServiceRef`). These become the member reference fields.
4. Enable projection and select the member reference fields (at least two).
5. Configure one mapping group per member.
6. Save, then run the initial rebuild from the Synchronization card.
Member reference field rules: the field must sit on the projection’s primary table, be a single-value `Reference` (not multi-value), and point to a standalone data entity — not a part, an association, or another projection (projection-on-projection is not supported). Once selected, the field becomes **projection-managed**: it is a storage link, hidden from ordinary forms and not user-editable.
## Mapping semantics
[Section titled “Mapping semantics”](#mapping-semantics)
A mapping copies one source value when the member’s row synchronizes:
* **Target** — a projection field (`Field` + target field), or the projected `DisplayName` / `Subtitle` presentation values (no target field).
* **Source path** — ordered field-key segments from the source row, e.g. `["Price"]` or `["Category", "DisplayName"]`; intermediate segments must be single-value references.
* **Transform** — `Copy` is the only transform in this version; no expressions or functions inside mappings.
Give every member a `DisplayName` mapping; members without one fall back to the source record’s own display name. The projection’s system record number is technical only — map the source number into an ordinary field such as `DisplayCode` when users need it.
## Row lifecycle
[Section titled “Row lifecycle”](#row-lifecycle)
Projection rows are system-managed. Direct create, update, delete, archive, import, and bulk mutations against the projection entity are rejected with a business error; changes to source records drive synchronization (including archive/restore and delete). Mapped target fields are read-only at runtime — the source row stays the source of truth.
Enabling projection runs a preflight scan and rejects inconsistent existing rows; the Synchronization view computes live consistency counters (missing, stale, duplicated rows) and offers a rebuild that reconciles everything.
## Configuration API
[Section titled “Configuration API”](#configuration-api)
Projections are managed through four operations of the [Configuration API](/docs/developer/configuration-api-reference/): [read](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-get-projection/) and [replace](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-update-projection/) the configuration, [check synchronization](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-get-projection-synchronization/), and [rebuild rows](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-rebuild-projection/). The update replaces the full member and mapping configuration in one call.
## When to use a projection
[Section titled “When to use a projection”](#when-to-use-a-projection)
Use a projection when several record types must appear as one — a shared selector, one combined list, one reference target — while each source keeps its own schema, logic, and governance. If the types genuinely share structure and rules, consider one entity definition instead.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/) for the fields a projection can map into.
* [Relationships and containment](/docs/user/data-structure/relationships-and-containment/) for reference mechanics.
# Runtime screens
> The entity list, details page, and drawer that users work in.
The **Runtime screens** tab configures the three host screens where users work with a record type. Each host screen selects reusable surfaces from the [UI surface library](/docs/user/data-structure/entity-definitions/ui-surface-library/) and adds its own layout and behavior. Everything defaults to **Auto** — a generated layout — until a custom configuration is saved.
An Entity List selector may explicitly choose a [Parent Tree View](/docs/configuration/hierarchies/configuring-parent-tree-view/), or inherit one from the entity’s default Table Surface. Data Explorer and Details/Drawer related-table hosts preserve that Tree presentation with lazy child paging.
## Entity list
[Section titled “Entity list”](#entity-list)
The collection page used by Data Explorer. Configuration covers:
* which **table surface** the list renders;
* **list filters** — the filters people can add: fields, system fields, statements, and object context facts;
* **actions** available from the list.
The desktop Data Explorer grid supports selecting several records for available group actions. Its narrow/mobile card layout intentionally omits selection and the bulk-action bar so each record stays compact and readable; mobile actions operate on one record at a time. Entering the mobile layout clears any desktop selection and pending group action, so returning to desktop starts unselected.
## Details
[Section titled “Details”](#details)
The full record page used when a record is opened:
* a **summary card** and an **edit form**, selected from the surface library;
* **actions** — function-backed toolbar actions with label, icon, and placement;
* **navigation and sections** — the page layout: cards, sections, tabs, and related-data blocks, including module blocks for Boards and Entitlements.
Entity Instance details do not render a separate signal ribbon. With the **Auto** layout, Moltaro generates the standard Record data, Comments, Attachments, Assignments, and Stats sections, plus available related-data sections. Runtime capabilities and permissions may hide unavailable sections. Boards and Entitlements are not added automatically; add their hosted sections explicitly. Object Context Facts appear through configured card or table fields rather than through a separate ribbon.
## Drawer
[Section titled “Drawer”](#drawer)
The preview drawer opened from lists and lookup flows. It selects its own summary card and form and has its own navigation and sections, so a quick preview can stay lighter than the full details page. Like the full details page, it does not reserve space for a separate signal ribbon.
Card item widths are responsive to the host’s available space. A shared Card Surface keeps one authored [Column span](/docs/configuration/card-item-layout/) for each item, while Drawer and Details clamp it to the number of columns that fit their own container.
## Runtime screens in the API
[Section titled “Runtime screens in the API”](#runtime-screens-in-the-api)
Integrations and agents can fetch the resolved UI configuration through the [Entity UI](/docs/developer/api-reference/operations/tags/entity-ui/) operations: list page, details page, drawer, card, and form payloads.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [UI surface library](/docs/user/data-structure/entity-definitions/ui-surface-library/) for the reusable tables, cards, and forms these screens select.
* [Card item layout](/docs/configuration/card-item-layout/) for responsive item widths shared by Drawer and Details.
* [Display fields](/docs/user/data-structure/entity-definitions/display-fields/) for extra read-time values on tables and cards.
# UI surface library
> Reusable table, card, and form surfaces for a record type.
The **Surface library** tab manages the reusable leaf surfaces of a record type: **tables**, **cards**, and **forms**. Runtime screens select these surfaces, so the same record type looks and behaves consistently in a detail page, drawer, list, board, or related section.
## Surfaces
[Section titled “Surfaces”](#surfaces)
A Table Surface, including the default, can own a [Parent Tree View](/docs/configuration/hierarchies/configuring-parent-tree-view/). It follows one indexed scalar self-Reference. Every table host renders the effective surface presentation; reference inputs require the full Lookup picker for Tree.
* **Tables** — explicit grid configurations. One table is starred as the default grid.
* **Cards** — summary cards with configured items.
* **Forms** — field layouts for create and edit.
Each surface is a saved, named configuration with a stable key. Surfaces can be generated from the schema, created manually, cloned, and deleted. One surface per type is the **default**, used whenever a runtime screen’s selector does not pick a specific one.
Card Fields, TextBlocks, and Alerts can set a responsive **Column span**. The same Card may use fewer columns in a narrow Drawer and more columns on Details, so the value is clamped to the space that currently fits. Field types do not become full-width automatically. See [Card item layout](/docs/configuration/card-item-layout/) for the editor workflow, examples, and Configuration API contract.
For an editable **Date** field on a Form, the item can choose the initial calendar year shown while the value is empty: the current workspace year, a fixed year such as 1950, or a relative year. This is useful for dates such as a birth date without forcing users through decades of month navigation. It is a display preference, not a default value: opening the calendar does not fill the field, mark the form as changed, or save a date. A stored date, keyboard input, and configured minimum, maximum, or allowed-date rules remain authoritative.
The host screens (entity list, details, drawer) are always available and are configured on [Runtime screens](/docs/user/data-structure/entity-definitions/runtime-screens/); create table, card, and form surfaces explicitly when those screens need them.
## Table query controls
[Section titled “Table query controls”](#table-query-controls)
Table filters and sort options are explicit parts of the surface. They are not derived from visible columns, and a useful business field can be configured for filtering, sorting, both, or neither.
A query target may traverse ordinary references. If an Aid record stores `Community`, one table can contain the distinct targets `Community`, `Community / District`, and `Community / District / Region`. The full path is the identity, so sharing the same first reference is allowed.
The filter target and the selected-value presentation are separate:
* the target path tells the server which terminal field to query;
* the reference breadcrumb path tells a lookup how to display the selected terminal record.
The terminal field controls the input. A terminal Region reference uses the normal Region list, picker, or lookup and filters by Region record ID. Runtime permissions are checked across the complete path.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [Runtime screens](/docs/user/data-structure/entity-definitions/runtime-screens/) for how host screens select surfaces.
* [Card item layout](/docs/configuration/card-item-layout/) for responsive Card widths in Drawer and Details.
* The [Entity UI](/docs/developer/api-reference/operations/tags/entity-ui/) API operations return the resolved surfaces integrations can render.
* [Table filters and sorting](/docs/configuration/table-filters-and-sorting/) explains related paths, configuration options, and verification.
# What an entity definition is
> The configured description behind every record type.
An entity definition is the configured description of a business record type in Moltaro. Users see the result as records, lists, detail pages, forms, related sections, and allowed actions. In user documentation the same concept is called a *record type*; one concrete record is an *entity instance*.
Entity definitions are managed in **Constructor > Entity Explorer**. Opening one shows its configuration sections: Overview, then Data model (Schema, Projection, Location), Governance (Access, Transfer), Automation & logic (Mutation effects, Logic, Validation, Object context facts), and User interface (Display fields, Runtime screens, Surface library). This documentation section follows the same structure.
## Entity definition types
[Section titled “Entity definition types”](#entity-definition-types)
Every entity definition has a type that fixes what it is for:
* **Data** — a normal business record type. Most entities are Data entities.
* **Association** — a many-to-many relationship modeled as its own records. See [Relationships and containment](/docs/user/data-structure/relationships-and-containment/).
* **Part** — records owned by another aggregate, such as a board. Part records are hidden from standalone data browsing and route access through their owner.
* **Dictionary** — a creation preset for simple reference-data lists. It produces a normal Data entity shaped for lookup values.
## Identity
[Section titled “Identity”](#identity)
An entity definition has a stable id and an **internal name** — the technical identifier used for storage and APIs. The internal name must be unique and is separate from the display names users see. Display wording lives in the singular and plural display names described under [General settings](/docs/user/data-structure/entity-definitions/general-settings/).
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [General settings](/docs/user/data-structure/entity-definitions/general-settings/) for identity, features, audit, and display settings.
* [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/) for the stored data model.
* Integrations can read entity definitions through the [Entity Definitions](/docs/developer/api-reference/operations/tags/entity-definitions/) API operations.
# Relationships and containment
> References, inverse references, associations, and governed parent-child structure.
Moltaro connects records through a small set of named mechanisms. Knowing which one is in play tells you where it is configured and how it behaves:
* a **Reference** field links a record to exactly one record of another entity definition;
* an **Inverse reference** field shows the reverse of one Reference as a list;
* an **Association** is a separate entity definition that models a many-to-many relationship as its own governed records;
* **containment** describes a parent-child business relationship. Access is still decided by the child’s current Security Statements and Permission Assignments, which may explicitly traverse an allowed parent or catalogue fact.
One terminology warning: record-to-record links are schema References or Associations. Human ownership such as Owner, Assignee, or Reviewer is modeled as a governed Responsibility and authorized through Assignment Rules. See [Entity security](/docs/user/data-structure/entity-definitions/access-model/).
## Reference fields
[Section titled “Reference fields”](#reference-fields)
A Reference is a schema field (Constructor > Entity Explorer > entity > **Schema**) that stores a link to one record of a configured target entity definition. A Reference is strictly single-value; there is no multi-reference field type. The target entity is chosen at creation and cannot change later.
Reference settings with operational consequences:
* **Delete behavior** — “Prevent deletion while referenced” (the safe default), “Delete linked records”, or “No action”. Preventing deletion returns a clear error until every active or archived record stops pointing at the target. Cascade permanently deletes the pointing records together with the target and is available only for References in the primary table. No action can leave a link that later resolves as `NotFound`.
* **Archive behavior** — cascade archives and restores the records that reference the current record together with it.
* **Audit roll-up** — “Show in referenced record history”: this record’s audit events also appear in the referenced record’s history.
* **Allow sorting** — lets lists sort by the linked record’s display value. The Constructor warns that sorting this way can ignore record-level security on the target entity.
In runtime forms a Reference renders as a list, long list, or lookup picker, and can display as a simple label or as breadcrumbs following a configured reference path.
In API JSON, a Reference is written as the target record’s id and read back as a compact resolved preview only when the source field is readable and the target record grants root `View`. Missing and inaccessible targets have one non-disclosing unavailable shape; neither the target identity nor presentation details are returned.
Delete behavior affects only permanent deletion. Archiving a record uses the separate Archive behavior. A record may delete itself even if it points to itself, and its own child rows do not block deletion of that same aggregate. Existing References configured as No action keep that setting; creating a new Reference defaults to Prevent deletion while referenced.
## Inverse reference fields
[Section titled “Inverse reference fields”](#inverse-reference-fields)
An Inverse reference is declared explicitly on the entity being pointed at. It pairs with one primary-table Reference field on the source entity and materializes the list of records whose paired Reference points at the current record. It has no stored column and cannot be required.
Inverse references power much of the related-record experience:
* related-data lists and tables on detail pages and drawers;
* aggregate calculated fields such as `SUM`, `COUNT`, `AVG`, `MIN`, and `MAX` over the linked child records;
* reverse lookups in queries (for example, records with or without any pointing records).
An inverse reference can also be written: a create or update payload may set it to a list of source record ids, and the runtime rewrites the paired Reference on each affected source record in the same transaction. This requires update access on every affected source record.
## Associations: many-to-many relationships
[Section titled “Associations: many-to-many relationships”](#associations-many-to-many-relationships)
When many records must link to many records, Moltaro models the relationship as an **Association** — an entity definition whose type is Association (entity definitions have a type: **Data**, **Association**, **Part**, or the **Dictionary** creation preset).
Creating an association uses a wizard with Base, Left endpoint, Right endpoint, and Security steps. It generates:
* two required endpoint Reference fields on the association entity, one for each side;
* a generated Inverse reference field on each endpoint entity, so both sides list their association rows;
* a binding that locks these generated fields together.
An association row is itself a governed record: it has its own list page, drawer, permissions, and an always-on audit trail, and it can carry its own fields (for example, a role, share, or valid-from date on the link). Both endpoint records’ histories include the association’s audit events.
Rules worth knowing: endpoints can target only Data entity definitions, endpoint values are required and immutable after creation, no entity may declare a Reference to an association, and associations cannot contain Table fields. Ordinary one-to-many relationships should stay Reference + Inverse reference; associations are specifically for many-to-many.
## Child rows are not relationships
[Section titled “Child rows are not relationships”](#child-rows-are-not-relationships)
A **Table** field holds repeatable child rows inside one record. Rows have no independent identity, access, or detail page, so they are structure inside a record, not a relationship between records. A Table column can itself be a Reference to another entity.
## Structure is not implicit access
[Section titled “Structure is not implicit access”](#structure-is-not-implicit-access)
References, association endpoints, parent-tree fields, Parts, and catalogs describe business structure. They do not copy or inherit Entity access. An active Security Statement can traverse an explicitly supported reference or catalogue fact, while root `View` and field projection still apply separately to every Entity record. See [Security Statements](/docs/user/data-structure/entity-definitions/access-model/).
## Relationships in the API
[Section titled “Relationships in the API”](#relationships-in-the-api)
* Reference and Inverse reference values are read and written through the [Entity Instances](/docs/developer/api-reference/operations/tags/entity-instances/) operations — see [query records](/docs/developer/api-reference/operations/entity-instances-query-list/) and the instance [read](/docs/developer/api-reference/operations/entity-instances-read-get-by-id/) operation.
* Association rows are ordinary entity instances of the association definition and use the same instance endpoints.
* Responsibilities have dedicated Add, Replace, Close, candidate, and history operations under the Entity runtime surface.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* Read [Relationships between records](/docs/user/data-and-records/relationships-between-records/) for the user-facing view.
* Read [Fields and schema](/docs/user/data-structure/entity-definitions/fields-and-schema/) for Reference and Inverse reference as field types.
* Read [Catalogs](/docs/user/data-structure/catalogs/) for hierarchy and catalog security.
# Templates
> How templates help create configured product areas.
Templates are guided starters for common data and process structures. They help configurators create a usable starting point faster than building every record type, field, relationship, screen, and rule by hand.
A template should create ordinary Moltaro configuration. After it is applied, the installation can still adjust the generated record types and workflows to match the team’s real operation.
Templates live in Constructor > **Templates**. Applying one is a wizard: choose parameters, preview the generated plan — record types, fields, ready-to-use screens, duplicate-detection profiles, generated logic, security setup, navigation, and optional demo data — then apply it. The output is ordinary editable workspace configuration; templates bootstrap entities and do not stay linked to them afterwards.
# Expression language overview
> Where Moltaro expressions are used and how the DSL surfaces differ.
Moltaro uses one small expression language (the Moltaro DSL) wherever an Entity Definition, Board, or another supported configuration surface needs logic without code. You write one expression per setting; the server parses it, checks it against the surface-owned schema, and reports precise errors before anything is saved.
The language has a shared core — literals, comparisons, boolean logic, null checks — and several surfaces that add what makes sense in their context. An expression that is valid as a statement is not automatically valid as a calculated field, so always check the surface-specific page.
## Where expressions are used
[Section titled “Where expressions are used”](#where-expressions-are-used)
| Surface | Kind | Typical example |
| -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------- |
| [Mutation effects](/docs/developer/business-logic/declarative-logic/#conditional-effects-with-applywhen) — `ApplyWhen` | Boolean condition | `UsePrimary = TRUE AND Product.Price > 0` |
| [Field behavior](/docs/dsl/field-behavior/) — `VisibleWhen`, `RequiredWhen`, `EditableWhen` | Boolean condition | `Status = 'draft' OR Amount IS NULL` |
| [Presentation rules](/docs/dsl/presentation-rules/) — display name and subtitle | Condition + text template | `{upper(Code)} — {coalesce(Nickname, Name)}` |
| [Statements](/docs/dsl/statements/) — reusable named predicates | Boolean condition | `DueDate >= @today+7d AND OwnerUser = @currentUserId` |
| [Board Statements](/docs/dsl/board-statements/) — Board-owned process predicates available for Configuration API authoring and preview | Boolean condition over Board Item, Board Data, and a scoped Target | `BoardData.Resolution IS NOT EMPTY` |
| [Validation rules](/docs/dsl/validation-rules/) — object-level save checks | Boolean condition | `MATCHES(Code, '^[A-Z]{3}-[0-9]{3}$')` |
| [Reference Eligibility](/docs/dsl/reference-eligibility/) — constrain one Reference candidate | Boolean condition over `Current` and `Candidate` | `Current.Type = 'Bug' AND Candidate.Type IN ('Feature', 'Task')` |
| [Calculated fields](/docs/dsl/calculated-fields/) — stored computed values | Value expression | `ROUND(Total * (1 - Discount / 100), 2)` |
## How the surfaces differ
[Section titled “How the surfaces differ”](#how-the-surfaces-differ)
* **Mutation effects** use `ApplyWhen` to decide whether an automatic write is a candidate before source resolution and fallback selection. They see primary scalar fields, `Operation.Mode`, and one actor-safe Reference hop; they do not use `VisibleWhen`, child tables, aggregates, classifier predicates, or runtime variables.
* **Field behavior** conditions see the record’s own fields and first-level reference paths. They do not support variables such as `@today`.
* **Presentation rules** pair a boolean condition with a text template; the template renders field values and a few text functions.
* **Entity Statements** add `@today`/ `@currentUserId` variables and direct child-table checks (`EXISTS`, `COUNT`). They can compare Classifier fields with typed `CATEGORY(...)` values.
* **Board Statements** are reusable predicates for Board Constraints. They add Board Item, Board Data, and an explicitly scoped Target context. Enabled transition, status-entry, status-exit, and mutation-local status-invariant Constraints enforce them.
* **Validation rules** must return `TRUE` for the record to save, support `@today`/`@currentUserId`, add the `MATCHES` regex function, and can compare Classifier fields with typed `CATEGORY(...)` values.
* **Reference Eligibility** compares the source `Current` record with a possible `Candidate` for one scalar Reference. Compiled dependencies drive Form and Table-filter candidate pre-filtering, and the active rule is authoritative for API writes.
* **Calculated fields** are value expressions, not conditions: arithmetic, numeric/money functions, and aggregate functions over child records.
## Fail fast with the validation endpoints
[Section titled “Fail fast with the validation endpoints”](#fail-fast-with-the-validation-endpoints)
The Configuration API validates expressions without saving anything. Rule and validation-rule diagnostics include the offending token and resolved field path. Calculated-field diagnostics return the request field, localized message, and stable code; a valid result also reports the inferred calculation kind. Mutation effects select `ContextType: MutationEffect` on the shared rule-expression endpoint. Field behavior, mutation effects, presentation rules, and statements share that endpoint but retain different schemas and capabilities. Use these endpoints to iterate until the expression binds cleanly:
* [`POST …/rule-expressions/validate`](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-validate-rule-expression/) for field behavior, mutation-effect, presentation, and statement expressions;
* [`POST …/validation-rules/validate-expression`](/docs/developer/configuration-api-reference/operations/admin-entity-validation-rules-validate-expression/) for validation rule expressions;
* [`POST …/calculation/validate`](/docs/developer/configuration-api-reference/operations/admin-entity-fields-validate-calculation/) for calculated field expressions (also reports the inferred local or aggregate kind);
* `POST /api/workspace/admin/boards/boards/{boardId}/statements/validate` for Board Statements. See the [Board Statement Configuration API authoring flow](/docs/dsl/board-statements/#configuration-api-authoring-flow).
The entity-definition endpoints belong to the [Configuration API reference](/docs/developer/configuration-api-reference/).
## Reading order
[Section titled “Reading order”](#reading-order)
1. [Syntax and types](/docs/dsl/syntax-and-types/) — literals, operators, field paths, null handling.
2. [Function reference](/docs/dsl/functions/) — every function, grouped by surface.
3. The surface page for what you are configuring: [rule expressions](/docs/dsl/rule-expressions/), [Board Statements](/docs/dsl/board-statements/), [validation rules](/docs/dsl/validation-rules/), [Reference Eligibility](/docs/dsl/reference-eligibility/), or [calculated fields](/docs/dsl/calculated-fields/).
# Board Statement DSL
> Boolean expression profile for Board constraints and future Board automation filters.
Runtime boundary
Board Statements can be authored, validated, previewed, persisted, and moved through installation packages through the Board administration UI and Configuration API. Healthy `Transition`, `EnterStatus`, and `ExitStatus` Bindings can be enabled and are evaluated by Board runtime commands. Healthy `StatusInvariant` Bindings can also be enabled when their complete dependency closure is mutation-local; they govern supported Board Item, Board Data, and Target Entity mutations.
A Board Statement is a named boolean expression owned by one Board. The first consumer is a [Board Constraint](/docs/user/boards/constraints/). Its Binding combines one or more applicable Statements through `AND` or `OR`.
Only exact `TRUE` passes a Constraint. An expression result of `FALSE` or `NULL` is treated as a failed fact.
Board Statements extend the existing Moltaro statement language instead of introducing a separate general-purpose language.
They are evaluated over a bounded, typed mutation context and are not compiled into PostgreSQL functions. Moltaro reuses the shared parser, schema binder, type system, and diagnostics, then evaluates the bound plan in memory so proposed values and date/time operations have the same behavior in preview and execute.
## Context roots
[Section titled “Context roots”](#context-roots)
The context provides:
| Root | Meaning |
| ----------- | ------------------------------------------------------------------------------------------- |
| `BoardItem` | Direct process facts such as status, cycle, run, due date, timestamps, and target identity. |
| `BoardData` | Fields of the Board-owned record that stores process-specific data for the item. |
| `Target` | Fields and named Entity Statements of one explicitly selected target definition. |
During a move, the Statement reads the effective proposed state, including the destination status and values supplied by the command. Event selection itself belongs to the Binding; the first version does not expose arbitrary `Old` and `New` object graphs inside the expression.
## Expression capabilities
[Section titled “Expression capabilities”](#expression-capabilities)
| Capability | Syntax or behavior |
| -------------------------- | ---------------------------------------------------------------- |
| Boolean composition | `AND`, `OR`, `NOT`, parentheses |
| Comparison | `=`, `==`, `!=`, `<>`, `>`, `>=`, `<`, `<=` |
| Membership and ranges | `IN`, `NOT IN`, `BETWEEN` |
| Text tests | `CONTAINS`, `STARTS WITH`, `ENDS WITH` |
| Null and empty tests | `IS NULL`, `IS NOT NULL`, `IS EMPTY`, `IS NOT EMPTY` |
| Direct owned collections | `EXISTS ... WHERE (...)`, `COUNT(...)` |
| Reusable entity facts | references to named Entity Statements on `BoardData` or `Target` |
| Direct inverse collections | `EXISTS` and `COUNT` for event constraints only |
| Date variables | `@today` and `@now` captured once per evaluation |
| Actor variable | `@currentUserId` for event constraints |
Operands must be type-compatible. Null is not equal to a value; use `IS NULL` and `IS NOT NULL`. Raw SQL, arbitrary queries, C#, network calls, and deep or recursive object traversal are not part of the language.
A scalar Reference path yields only its referenced record id. For example, `Target.Customer = 'customer-id'` compares ids. `Target.Customer.Name` is not supported: a Reference is not an object traversal or a relationship between Boards.
## Operators by value family
[Section titled “Operators by value family”](#operators-by-value-family)
| Value family | Tests |
| --------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Text | truthiness, equality, `IN`/`NOT IN`, text tests, null, empty |
| Boolean | truthiness, equality, `IN`/`NOT IN`, null |
| Integer and decimal | truthiness, equality, ordering, `IN`/`NOT IN`, `BETWEEN`, null |
| Date, time, and date-time | truthiness, equality, ordering, `IN`/`NOT IN`, `BETWEEN`, null |
| User, Role, File, and scalar Reference stable ids; single Select option key | truthiness, equality, `IN`/`NOT IN`, null |
| Multi-Select | truthiness, `CONTAINS`, null, empty |
| Complex Address value | presence and null only |
| Direct owned Table | `EXISTS ... WHERE (...)` and `COUNT(...)` comparisons |
An Address whose text components are null, empty, or whitespace and whose coordinates are absent is canonicalized to `null`. An Address object is present when at least one text component or coordinate has a meaningful value. This rule is identical for API mutations, generated Package/Net Operation Project contexts, persisted snapshots, and proposed-state evaluation.
Money exposes its decimal `Amount` and normalized uppercase `CurrencyCode` components. A missing Amount makes the whole Money value canonical `null`, even when a currency value is stored. When Amount is present and CurrencyCode is omitted, `null`, empty, or whitespace, the field’s `BaseCurrencyCode` is used. Persisted and proposed values follow the same rule. Field-to-field comparison requires compatible types. Classifier fields and `CATEGORY(...)` literals are not exposed by the Board Statement V1 profile.
## Date and time values
[Section titled “Date and time values”](#date-and-time-values)
* Typed date/time values are written as quoted string literals.
* `DateOnly` uses ISO `YYYY-MM-DD`, for example `'2026-08-02'`.
* `TimeOnly` uses `HH:mm`, optionally followed by seconds and fractional seconds, for example `'14:30:00'`.
* `DateTimeOffset` includes `Z` or an explicit numeric offset and is normalized to UTC, for example `'2026-08-02T14:30:00+02:00'`.
* Compatible date and time values support equality, ordering, `IN`, and `BETWEEN`.
* `@today` is a `DateOnly` in the configured workspace time zone.
* `@now` is one captured `DateTimeOffset` instant.
Duration literals are `7d`, `12h`, `30min`, and `15s`. Required V1 arithmetic and extraction are:
| Expression | Meaning |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| `date + 7d`, `date - 7d` | Add or subtract whole calendar days. |
| `instant + 12h`, `instant - 30min` | Add or subtract fixed elapsed time. |
| `date1 - date2` | Signed integer calendar-day difference. |
| `instant1 - instant2` | Signed duration, comparable with a duration literal. |
| `YEAR`, `QUARTER`, `MONTH`, `ISO_WEEK`, `DAY`, `DAY_OF_WEEK` | Extract calendar components; ISO weekdays are Monday `1` through Sunday `7`. |
| `HOUR`, `MINUTE`, `SECOND` | Extract time components. |
| `DATE`, `TIME` | Extract workspace-local date or time from an instant. |
| `START_OF_MONTH`, `END_OF_MONTH` | Return the calendar boundary. |
| `ADD_DAYS`, `ADD_MONTHS`, `ADD_YEARS` | Add calendar units to a date. |
| `ADD_HOURS`, `ADD_MINUTES`, `ADD_SECONDS` | Add fixed elapsed units to an instant. |
Duration literals are signed whole numbers. Decimal durations such as `1.5h` and calendar durations such as `1month` or `1year` are invalid. Use `ADD_MONTHS` or `ADD_YEARS` when the operation is calendar-based.
The complete function contract is:
| Function | Accepted argument families | Result |
| ------------------------------------------------------------ | ------------------------------ | -------------------- |
| `YEAR`, `QUARTER`, `MONTH`, `ISO_WEEK`, `DAY`, `DAY_OF_WEEK` | `(Date)` or `(DateTimeOffset)` | Integer |
| `HOUR`, `MINUTE`, `SECOND` | `(Time)` or `(DateTimeOffset)` | Integer |
| `DATE` | `(DateTimeOffset)` | workspace-local Date |
| `TIME` | `(DateTimeOffset)` | workspace-local Time |
| `START_OF_MONTH`, `END_OF_MONTH` | `(Date)` | Date |
| `ADD_DAYS`, `ADD_MONTHS`, `ADD_YEARS` | `(Date, Integer)` | Date |
| `ADD_HOURS`, `ADD_MINUTES`, `ADD_SECONDS` | `(DateTimeOffset, Integer)` | DateTimeOffset |
`DateTimeOffset` component extraction first converts the instant to the workspace time zone. `TimeOnly` supports comparison and extraction but not arithmetic in V1, avoiding implicit day wrapping. Calendar month/year changes use explicit functions because those units do not have fixed duration. `ADD_MONTHS` and `ADD_YEARS` clamp to the last valid day of the resulting month: adding one month to January 31 produces the last day of February, and adding one year to February 29 in a non-leap year produces February 28.
Calendar and elapsed-time operations are intentionally different around a daylight-saving time change. `ADD_DAYS(date, 1)` means the next workspace-local calendar date. `instant + 1d` means exactly 24 elapsed hours. `DATE`, `TIME`, and all component functions convert an instant through the configured workspace time zone before extracting a value. The configured workspace time zone is therefore part of the expression’s meaning; the authoring endpoint describes the available temporal functions and literal families, not a replacement client-side calendar policy.
```text
YEAR(BoardData.RequiredAt) = YEAR(@today)
DAY_OF_WEEK(@today) IN (6, 7)
Target.DueDate + 7d <= @today
Target.DueAt - @now <= 48h
ADD_MONTHS(Target.ContractStart, 1) <= @today
```
`@now` and `@today` are captured from the same clock instant once for the entire evaluation. A long expression cannot observe two different values of “now”. Null date/time operands propagate `NULL`; only exact `TRUE` passes a Constraint. Invalid literal families, arithmetic overflow, and unavailable time-zone data fail closed.
## Entity Statement references
[Section titled “Entity Statement references”](#entity-statement-references)
A Board Statement can reuse a named [Entity Statement](/docs/dsl/statements/) from `BoardData` or its selected `Target` with `BoardData.Statements.` or `Target.Statements.`. Moltaro resolves the stable Statement name when validating the Board Statement and tracks the dependency. Renaming or deleting the referenced Statement is blocked while the Board Statement still depends on it. Updating the referenced Statement expression is an intentional semantic change and invalidates the definition cache so the Board plan is rebound.
The Entity Statement’s `ExposeInClient` setting does not control server-side Board evaluation. Referencing it does not publish its value to a client.
## Target scope
[Section titled “Target scope”](#target-scope)
A Statement that does not read `Target` can be used for any item on the Board. The safe first version binds a target-reading Statement to one exact Board target definition. Provider targets without a typed expression schema can use `BoardItem` and `BoardData`, but not `Target` fields.
One expression does not span several target definitions in V1. A Constraint Binding instead combines several target-scoped Statements through `AND` or `OR`, evaluating only the Statements applicable to the current item. The configuration must provide coverage for every possible target; there is no missing-member or missing-as-null fallback.
## Event-safe and invariant-safe expressions
[Section titled “Event-safe and invariant-safe expressions”](#event-safe-and-invariant-safe-expressions)
A transition, entry, or exit constraint checks one attempted event. It can read a supported first-level Reference or direct Inverse Reference collection because the result is needed only at that moment. A direct inverse collection supports `EXISTS` and `COUNT`, for example `COUNT(Target.ImplementationTasks) > 0`.
```text
EXISTS Target.ImplementationTasks WHERE (Status = 'ready')
COUNT(Target.ImplementationTasks) BETWEEN 1 AND 5
```
The collection must be the direct inverse exposed by `/authoring`. Paths from an inverse row into another Reference, and recursive or transitive inverse lookups, are rejected. Direct inverse collections are event-only and cannot be used by a `StatusInvariant` Binding.
A status invariant promises more: every supported mutation while the item remains in the status must preserve the Statement. Its dependencies are therefore limited to direct Board Item facts, the Board Data aggregate, the direct Target aggregate, and Entity Statements built only from those same mutation-local facts.
The first version rejects status-invariant use of:
* fields below independently changing References;
* inverse references and deeper paths;
* `BoardData.DisplayName` and `Target.DisplayName`;
* runtime- or projection-computed system values, including `Number`, `ModifiedAt`, `ModifiedByUserId`, and `LastActivityDateTime`;
* `LineNumber`, Classifier, and every other projection-managed field or owned collection;
* every calculated root or owned-row field;
* Entity Statements whose transitive closure reaches `DisplayName`, a calculated field, `Number`, `Modified*`, `LineNumber`, Classifier, a projection-managed aggregate, a Reference traversal, or another event-only dependency;
* calendar and time-component extraction from `DateTimeOffset` values, because all component functions, including `YEAR`, `QUARTER`, `MONTH`, `ISO_WEEK`, `DAY`, `DAY_OF_WEEK`, `HOUR`, `MINUTE`, `SECOND`, `DATE`, and `TIME`, interpret an instant in the configured workspace time zone;
* clock- or current-user-dependent expressions;
* external or arbitrary query data.
For example, `YEAR(Target.DueAt) = 2026` is valid for an event Binding but not for a `StatusInvariant` Binding when `DueAt` is a `DateTimeOffset`. Changing the workspace time zone can change the extracted calendar value without mutating the governed Target.
This prevents a configuration from being presented as an invariant when the platform cannot observe every mutation that could change its result.
## Configuration API authoring flow
[Section titled “Configuration API authoring flow”](#configuration-api-authoring-flow)
All routes below are under `/api/workspace/admin/boards/boards/{boardId}/statements` and require Board configuration access.
| Operation | Route |
| --------------------------------- | -------------------------------------------- |
| List Statements | `GET /` |
| Read one Statement | `GET /{boardStatementId}` |
| Read the server-owned DSL profile | `GET /authoring?boardTargetDefinitionId=...` |
| Validate and bind without saving | `POST /validate` |
| Evaluate explicit proposed values | `POST /preview` |
| Create | `POST /` |
| Update with `RowVersion` | `PUT /{boardStatementId}` |
| Delete with `RowVersion` | `DELETE /{boardStatementId}` |
| Replace display order | `POST /reorder` |
Always obtain roots, paths, operators, variables, functions, examples, and diagnostic codes from `/authoring`; do not hard-code a client-side copy. Omit `BoardTargetDefinitionId` for a Board-wide Statement. Supply one exact Entity Definition target id to expose the `Target` root. Unsupported provider targets are rejected rather than exposed with an incomplete schema.
`POST /validate` returns source-positioned diagnostics and the normalized expression, dependency manifest, complexity metrics, fingerprints, and event or status-invariant eligibility. `POST /preview` accepts only an explicit proposed context keyed by paths from `/authoring`; it does not become an unrestricted record-read endpoint.
Moltaro stores the authored expression and bind metadata, but recompiles and rebinds against the current schema when needed. Package YAML carries the portable source and target scope, not a serialized executable plan. A missing field, renamed referenced Entity Statement, incompatible schema, or damaged profile fails closed with a stable diagnostic.
## Copyable recipes
[Section titled “Copyable recipes”](#copyable-recipes)
Require both an owner and a future due date on Board Data:
```text
BoardData.Owner IS NOT NULL AND BoardData.DueDate > @today
```
Reuse a Target Entity Statement and add a Board-specific requirement:
```text
Target.Statements.IsCommerciallyReady AND BoardData.ApprovalCode IS NOT EMPTY
```
Accept a record when either a manual approval or a trusted Target fact passes:
```text
BoardData.ManualApproval = TRUE OR Target.Statements.IsAutomaticallyApproved
```
Require a contract to have started and not be older than one calendar year:
```text
Target.ContractStart <= @today
AND ADD_YEARS(Target.ContractStart, 1) >= @today
```
Require at least one directly related implementation task at an event boundary:
```text
EXISTS Target.ImplementationTasks WHERE (Status = 'ready')
```
## Invalid expressions and diagnostics
[Section titled “Invalid expressions and diagnostics”](#invalid-expressions-and-diagnostics)
The following examples are intentionally invalid:
```text
Target.Customer.Name = 'Acme'
@today + 12h
Target.DueAt + 1month
Target.StartTime + 30min
Target.UnknownField IS NOT NULL
```
They fail, in order, because Reference traversal is not supported; date arithmetic accepts whole days only; a month is not a fixed duration literal; time-only arithmetic is not supported; and the final path is absent from the selected profile. Board Statement expressions do not support inline comments, so explanatory text must stay outside the expression.
Validation diagnostics identify a stable code and the source start/length so an editor can highlight the exact failing token. Treat `/authoring` and `/validate` as the contract: do not infer field availability or reproduce the type checker in a client.
When validation unexpectedly changes after a schema edit, fetch `/authoring` again and validate the stored expression. A removed field, renamed Entity Statement, changed target definition, profile-version mismatch, or incompatible field type requires an explicit expression update. Moltaro never converts the missing dependency to `NULL` and never keeps executing a stale compiled plan.
# Calculated field expressions
> Local formulas and aggregate formulas for stored calculated fields.
A calculated field is a stored scalar field whose value is produced by an expression instead of user input. The value is persisted; runtime writes ignore user-supplied values for calculated targets. You configure one expression per field, and the server infers whether it is a **local** calculation (same record) or an **aggregate** calculation (over child records).
Supported target field types: `Integer`, `Decimal`, and `Money`.
## Display field, calculated field, or C\#
[Section titled “Display field, calculated field, or C#”](#display-field-calculated-field-or-c)
These are separate contracts:
| Requirement | Use |
| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Extra read-time text in Table and Card surfaces, such as a Full name value rendered from `{LastName} {FirstName}` | A [Display field](/docs/user/data-structure/entity-definitions/display-fields/). It adds no stored column and cannot be searched, filtered, sorted, edited, or used by business rules. |
| A stored value produced by a supported formula | A calculated field described on this page. The runtime owns and persists the target value. |
| A stored derivation that needs procedural logic or additional reads | A C# [before-save mutation function](/docs/developer/business-logic/entity-scoped-logic/). Backfilling existing records is a separate operation unless the C# workflow implements it. |
Display name and subtitle are presentation rules, not Display Fields or stored calculations.
## Local expressions
[Section titled “Local expressions”](#local-expressions)
A local expression runs on one record and may reference fields from the same entity table only — no dotted reference paths.
Building blocks:
* numeric literals (`12.50`), parentheses, unary `+`/`-`;
* arithmetic: `+`, `-`, `*`, `/`;
* same-table field keys: `Qty`, `Price`, `Discount`;
* functions: `IF`, `COALESCE`, `ROUND`, `ABS`, `MIN`, `MAX` (see the [function reference](/docs/dsl/functions/#calculated-fields--local-functions)).
```text
Qty * Price
Total * (1 - Discount / 100)
IF(Qty > 0, Qty, 0)
ROUND(Total * (1 - Discount / 100), 2)
COALESCE(ManualTotal, Total)
```
`IF` conditions use a deliberately small grammar: same-table numeric, money, and boolean fields, numeric literals, `TRUE`/`FALSE`/`NULL`, comparisons, `AND`/`OR`/`NOT`, and `IS [NOT] NULL`. Text operators, dates, references, and child-table forms are not available inside calculation conditions.
Typing is strict: numeric combines with numeric, money with money, and one `IF` branch may be `NULL`. Mixing money with plain numbers in one result is invalid. Integer targets truncate decimal results on write.
### Money behavior
[Section titled “Money behavior”](#money-behavior)
Money arithmetic keeps the source currency. Supported: money ± money, money × or ÷ number, number × money. Combining or comparing money values with different currency codes makes the calculation invalid for that record rather than silently converting.
## Aggregate expressions
[Section titled “Aggregate expressions”](#aggregate-expressions)
An aggregate expression runs from a parent record over its child records:
```text
SUM(Positions, TotalPrice)
COUNT(Positions, Id)
```
* The first argument is an **inverse-reference field on the parent entity**, paired with a `Reference` field on the child that points back to the parent.
* The second argument is a primary-table field on that child definition.
* `SUM`, `AVG`, `MIN`, `MAX` accept `Integer`, `Decimal`, and `Money` sources; `COUNT` with the child `Id` counts active rows, with another field it counts rows where that field is not null.
* Archived child rows are excluded.
* Empty set: `SUM` and `COUNT` return `0`; `AVG`, `MIN`, `MAX` return `NULL`.
* Money aggregates include only rows whose currency matches the result currency (the target field’s base currency, or the source currency when the target has none); any non-null row in another currency makes the result `NULL` instead of mixing currencies.
Not supported: filtered aggregates (`SUM(Positions, Amount WHERE …)`), path syntax (`SUM(Positions[Order].TotalPrice)`), collection paths outside aggregate functions, and arbitrary code.
## Evaluation order
[Section titled “Evaluation order”](#evaluation-order)
Within one save the runtime applies, in order: mutation effects → local calculated fields → persist → aggregate recalculation on affected parents → local calculated fields on those parents. This supports chains such as:
```text
OrderPosition.Price <- mutation effect copies Item.Price
OrderPosition.TotalPrice <- Qty * Price
Order.Total <- SUM(Positions, TotalPrice)
Order.TotalWithDiscount <- Total * (1 - Discount / 100)
```
Form previews show local calculated values as you edit; aggregate values are server truth after save.
## Authoring workflow
[Section titled “Authoring workflow”](#authoring-workflow)
For an API-first workflow, use the Configuration API served by the target installation:
1. Read the current field, including `CalculationExpression`, `CalculationKind`, and `RowVersion`: `GET /api/workspace/admin/entity-fields/{fieldId}`.
2. Validate an expression without saving: `POST /api/workspace/admin/entity-fields/{fieldId}/calculation/validate` with `{"Expression":"Qty * Price"}`.
3. After validation succeeds, save exactly that expression with the current row version: `PUT /api/workspace/admin/entity-fields/{fieldId}/calculation` with `{"Expression":"Qty * Price","RowVersion":""}`.
The generated [calculation validate operation](/docs/developer/configuration-api-reference/operations/admin-entity-fields-validate-calculation/) — errors include the request field, localized message, and stable code; a valid result also reports the inferred calculation kind (local or aggregate). Then set or change the expression through the field’s [calculation operation](/docs/developer/configuration-api-reference/operations/admin-entity-fields-update-calculation/) in the [Configuration API](/docs/developer/configuration-api-reference/). Invalid expressions are rejected with the same structured errors. Fields referenced by a calculation become schema dependencies: they block deletion and incompatible type changes until the calculation is updated.
# Field behavior conditions
> VisibleWhen, RequiredWhen, and EditableWhen expressions on entity fields.
Field behavior conditions are boolean expressions configured directly on a field of an entity definition. They decide, per operation, whether the field is shown, whether it must have a value, and whether it accepts changes. They share the expression language described in [Syntax and types](/docs/dsl/syntax-and-types/) and are validated through the shared rule-expression endpoint described in [Rule expressions](/docs/dsl/rule-expressions/).
## The three conditions
[Section titled “The three conditions”](#the-three-conditions)
| Setting | When the condition is `TRUE` | Default when empty | Outside its operation mode |
| -------------- | ---------------------------- | ------------------ | -------------------------- |
| `VisibleWhen` | The field is shown. | Visible | Hidden |
| `RequiredWhen` | The field must have a value. | Optional | Optional |
| `EditableWhen` | The field accepts changes. | Editable | Read-only |
An empty `VisibleWhen` or `EditableWhen` defaults to true within its operation mode; an empty `RequiredWhen` defaults to false. The literal `TRUE` in `RequiredWhen` means always required — definition API responses show it as `"RequiredWhen": "TRUE"`; other values are conditional expressions.
Visibility and editability are not access-control rules. Field permissions can still hide or protect a field regardless of these conditions, and `VisibleWhen` must not be used to conceal sensitive data from an otherwise authorized caller.
`VisibleWhen` also does not control declarative mutation effects. Use the effect’s `ApplyWhen` condition when a value-copy candidate should become active or inactive. Hiding a field never deletes its stored value, while an effect-owned target may fall back or clear under the separate mutation-effect ownership rules.
## Operation modes
[Section titled “Operation modes”](#operation-modes)
Each condition has a paired operation mode that controls when it is evaluated:
* `CreateAndUpdate` — evaluated for both new and existing records;
* `Create` — evaluated only while a new record is created;
* `Update` — evaluated only while an existing record is edited.
Outside its mode a condition contributes its inactive value: `VisibleWhen` is false, `RequiredWhen` is false, and `EditableWhen` is false. This makes `EditableWhen = TRUE` with mode `Create` a create-only field: the value can be supplied initially, the form treats it as read-only later, and the server rejects an update attempt. Likewise `VisibleWhen = TRUE` with mode `Update` hides the field on create and shows it on edit.
## Reading Operation.Mode
[Section titled “Reading Operation.Mode”](#reading-operationmode)
The expression itself can read `Operation.Mode`, whose value is `'Create'` or `'Update'`:
```text
Operation.Mode = 'Update' AND Status = 'draft'
```
Prefer the operation-mode setting for a simple create-only or update-only rule; use `Operation.Mode` when operation awareness is part of a larger condition.
## Expression profile
[Section titled “Expression profile”](#expression-profile)
A field behavior condition can use:
* fields of the same record and exactly one direct Reference hop (`Customer.IsBlocked`);
* `Operation.Mode`, with value `'Create'` or `'Update'`;
* the shared operators — comparisons, `AND`/`OR`/`NOT`, text operators, `IN`, `NOT IN`, null/empty checks.
Not supported: variables (`@today` is not available here), function calls, Classifier fields, the `BETWEEN` range operator, child-table traversal, inverse-reference traversal, and Reference chains deeper than one step. Write a range as `>= low AND <= high`; `BETWEEN` is available only in statements and validation rules (see [Availability by surface](/docs/dsl/syntax-and-types/#availability-by-surface)).
For a child-table field, the condition can use fields of that child row but cannot traverse a Reference.
In generated Forms, Moltaro obtains a referenced value such as `Customer.IsBlocked` from the same server-owned context used by every Reference picker. Existing records request the required include automatically; a newly selected candidate returns the declared projected values with the selection. This works even when the Reference has no Reference Eligibility rule.
```text
VisibleWhen: CustomerType = 'company'
RequiredWhen: Status IN ('approved', 'completed')
EditableWhen: Status = 'draft' AND Locked = FALSE
```
```text
Status = 'draft' OR Amount IS NULL
(Amount > 10 AND Status IN ('active', 'pending')) OR Name STARTS WITH 'VIP'
```
## Server-side enforcement
[Section titled “Server-side enforcement”](#server-side-enforcement)
Requiredness and editability are enforced by the server as well as the generated form. On save, a missing value for a field whose `RequiredWhen` evaluates to `TRUE` fails the request, and a change to a field whose `EditableWhen` evaluates to false is rejected — an API request cannot bypass the conditions by skipping the form.
If a saved expression is no longer valid for the current schema, the server returns a localized configuration error and does not open the Form with an invented fallback result. Fields or referenced values hidden by the caller’s permissions remain undisclosed and make the affected condition false.
## Validating expressions
[Section titled “Validating expressions”](#validating-expressions)
Field behavior conditions are validated by the shared rule-expression endpoint, `POST /api/workspace/admin/entity-definitions/{entityDefinitionId}/rule-expressions/validate`, with `ContextType` `FieldBehavior` and `ExpressionType` `Condition`. See [Rule expressions](/docs/dsl/rule-expressions/) for the shared validation contract and the neighboring condition surfaces, and [Syntax and types](/docs/dsl/syntax-and-types/) for literals, comparison semantics, and operator details.
# Function reference
> Every Moltaro expression function, grouped by the surface where it is available.
Functions are surface-specific: calculated fields have numeric and aggregate functions, validation rules add `MATCHES`, presentation templates have text functions, Entity Statements have the `EXISTS`/`COUNT` child-table forms, and Board Statements add an explicit calendar and elapsed-time profile. A function used outside its surface is rejected at validation time.
## Board Statements - date and time functions
[Section titled “Board Statements - date and time functions”](#board-statements---date-and-time-functions)
Available only in [Board Statement](/docs/dsl/board-statements/#date-and-time-values) expressions. The Board Statement page is the authoritative contract for accepted argument families, result types, workspace-time-zone behavior, month-end clamping, duration literals, and invariant eligibility.
| Functions | Behavior |
| ------------------------------------------------------------ | ------------------------------------------------------- |
| `YEAR`, `QUARTER`, `MONTH`, `ISO_WEEK`, `DAY`, `DAY_OF_WEEK` | Extract a calendar component from a date or instant. |
| `HOUR`, `MINUTE`, `SECOND` | Extract a time component from a time or instant. |
| `DATE`, `TIME` | Convert an instant to its workspace-local date or time. |
| `START_OF_MONTH`, `END_OF_MONTH` | Return a calendar-month boundary. |
| `ADD_DAYS`, `ADD_MONTHS`, `ADD_YEARS` | Add an integer number of calendar units to a date. |
| `ADD_HOURS`, `ADD_MINUTES`, `ADD_SECONDS` | Add an integer number of elapsed units to an instant. |
```text
YEAR(BoardData.RequiredAt) = YEAR(@today)
ADD_MONTHS(Target.ContractStart, 1) <= @today
```
## Statements and validation rules — `CATEGORY`
[Section titled “Statements and validation rules — CATEGORY”](#statements-and-validation-rules--category)
`CATEGORY(...)` is a typed portable literal available in Statements and Validation Rules. It is written like a function call, but it identifies one Classifier category rather than calculating a value at runtime.
| Form | Behavior |
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `CATEGORY('catalog-key', 'root-key', …, 'node-key')` | Resolves the stable Catalog key and category path when the expression is saved, then compares the stored category identity. |
```text
Capability = CATEGORY('product-area', 'operations', 'platform')
Capability IN (
CATEGORY('product-area', 'operations', 'platform'),
CATEGORY('product-area', 'operations', 'integration')
)
```
See [Classifier category values](/docs/dsl/syntax-and-types/#classifier-category-values) for supported operators, null behavior, and field-to-field restrictions.
## Calculated fields — local functions
[Section titled “Calculated fields — local functions”](#calculated-fields--local-functions)
Available in [calculated field](/docs/dsl/calculated-fields/) expressions.
| Function | Behavior |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IF(condition, whenTrue, whenFalse)` | Evaluates `condition`, then only the selected branch. Both branches are type-checked; branches must be compatible (numeric with numeric, money with money, one branch may be `NULL`). |
| `COALESCE(v1, v2, …)` | First non-null value, left to right, short-circuiting. |
| `ROUND(value, scale)` | Rounds a number or money value away from zero on midpoints. `scale` is an integer literal from `0` to `6`. |
| `ABS(value)` | Absolute value of a number or money value. |
| `MIN(v1, v2, …)` / `MAX(v1, v2, …)` | Smallest / largest non-null argument. All non-null arguments must be compatible numbers or compatible money values. |
```text
IF(Qty > 0, Qty, 0)
COALESCE(ManualTotal, Total)
ROUND(Total * (1 - Discount / 100), 2)
```
## Calculated fields — aggregate functions
[Section titled “Calculated fields — aggregate functions”](#calculated-fields--aggregate-functions)
Aggregates run from a parent record over its child records through an inverse-reference field. They are valid **only** as a calculated-field expression, and only in the two-argument form:
| Function | Behavior |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `SUM(InverseRef, SourceField)` | Sum of the source field over active child rows. Empty set → `0`. |
| `COUNT(InverseRef, SourceField)` | With the child `Id` field: counts active child rows. With another field: counts rows where that field is not null. Empty set → `0`. |
| `AVG(InverseRef, SourceField)` | Average over non-null values. Empty set → `NULL`. |
| `MIN(InverseRef, SourceField)` / `MAX(InverseRef, SourceField)` | Smallest / largest value. Empty set → `NULL`. |
```text
SUM(Positions, TotalPrice)
COUNT(Positions, Id)
```
See [calculated fields](/docs/dsl/calculated-fields/#aggregate-expressions) for source-field rules and money/currency behavior.
## Validation rules — `MATCHES`
[Section titled “Validation rules — MATCHES”](#validation-rules--matches)
Available only in [validation rule](/docs/dsl/validation-rules/) expressions.
| Function | Behavior |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `MATCHES(value, "regex")` | `TRUE` when the text value matches the .NET regular expression. The pattern must be a string literal. Matching is case-sensitive; use an inline flag such as `(?i)` for ignore-case. |
```text
MATCHES(Code, '^[A-Z]{3}-[0-9]{3}$')
MATCHES(Email, '(?i)@example\.com$')
```
## Presentation templates — text functions
[Section titled “Presentation templates — text functions”](#presentation-templates--text-functions)
Available inside `{ … }` placeholders of [presentation rule templates](/docs/dsl/presentation-rules/).
| Function | Behavior |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trim(x)` | Removes leading and trailing whitespace. |
| `upper(x)` / `lower(x)` | Uppercase / lowercase. |
| `coalesce(x, …)` | First non-null value. |
| `optionLabel(SelectPath)` | Configured caption for a direct `Select` field path. Null renders empty, unknown or removed keys fall back to the raw key, and multi-select values preserve stored order and join with `, `. |
```text
{upper(Code)} — {coalesce(Nickname, Name)}
{optionLabel(Category)} ({Category})
```
## Entity Statements and Board Statements — collection forms
[Section titled “Entity Statements and Board Statements — collection forms”](#entity-statements-and-board-statements--collection-forms)
Entity [Statements](/docs/dsl/statements/) use these forms against direct child tables. [Board Statements](/docs/dsl/board-statements/#event-safe-and-invariant-safe-expressions) use the same forms against direct owned collections and, for event Bindings only, direct inverse collections published by the server-owned authoring profile. Deeper and transitive collection paths are rejected.
| Form | Behavior |
| -------------------------------- | ------------------------------------------------------------------------------ |
| `EXISTS Table WHERE (condition)` | `TRUE` when at least one child row matches. |
| `COUNT(Table) ` | Compares the number of child rows, for example `COUNT(Items) BETWEEN 1 AND 3`. |
```text
EXISTS Items WHERE (Quantity > 0) AND COUNT(Items) BETWEEN 1 AND 3
```
Ordinary function calls are not supported in field-behavior or presentation conditions — those surfaces are operator-only.
# Presentation rules
> Templates and conditions that compute a record's display name and subtitle.
Presentation rules compute the two text labels every record carries: the primary **display name** and the secondary **subtitle** line. Each rule pairs a text **template** (what to render) with an optional **condition** (when the rule applies) and belongs to one target slot, `DisplayName` or `Subtitle`. Rules are stored on the entity definition, so the same labels appear wherever the record is shown or referenced.
## Evaluation order
[Section titled “Evaluation order”](#evaluation-order)
Rules are evaluated per target slot in ascending sort order. The first rule whose condition evaluates to `TRUE` wins and its template is rendered; later rules in the slot are not tried. An empty condition always matches, so a condition-less rule at the end of the slot acts as the default.
If no rule matches, or the winning template renders only empty text, the display name falls back to the record’s `Number` and the subtitle is simply omitted.
## Conditions
[Section titled “Conditions”](#conditions)
The condition uses the same profile as [field behavior conditions](/docs/dsl/field-behavior/): fields of the record, first-level reference paths (`Customer.IsBlocked`), and the shared operators — comparisons, `AND`/`OR`/`NOT`, text operators, `IN`, `NOT IN`, null/empty checks. Variables such as `@today`, function calls, and the `BETWEEN` range operator are not available here; write a range as `>= low AND <= high` (`BETWEEN` is available only in statements and validation rules — see [Availability by surface](/docs/dsl/syntax-and-types/#availability-by-surface)).
```text
Nickname IS NOT EMPTY
Status = 'archived'
```
## Templates
[Section titled “Templates”](#templates)
A template is plain text with `{ … }` placeholders. Inside a placeholder you can use:
* a field path: `{Name}`, `{Customer.DisplayName}`;
* a literal: `{'fallback'}`, `{42}`, `{TRUE}`, `{NULL}`;
* a text function call: `{upper(Code)}`, `{lower(Email)}`, `{trim(Code)}`, `{coalesce(Nickname, Name)}`;
* a Select caption lookup: `{optionLabel(Category)}`. The argument must be a direct path to a `Select` field.
Field paths can reach:
* scalar fields of the record’s primary table (`Table` fields are excluded);
* the system value `{Number}`;
* first-level `Reference` paths — the referenced record’s `Id`, `Number`, `DisplayName`, `Subtitle`, and its scalar fields;
* `Money` sub-values through `.Amount` and `.CurrencyCode`.
Fields protected by field-level access rules cannot be used: presentation output is visible to everyone who can see the record, and a rule that references a restricted field is skipped at runtime.
Missing values render as empty text. The final result is whitespace-normalized: consecutive whitespace collapses to single spaces and the text is trimmed, so a template like `{Code} — {Name}` degrades cleanly when one side is empty.
`optionLabel(SelectPath)` renders the configured option caption without changing the stored value. For example, when `Category` stores `logistics` and that option is configured as `Логістика`, `{optionLabel(Category)}` renders `Логістика`, while `{Category}` still renders `logistics`. A null value renders empty text. An unknown or removed option renders its raw stored key. A multi-select preserves stored order and joins captions and fallback keys with `, `. Resolution is culture-independent apart from the configured caption. Changing a referenced field from `Select` to another type is rejected while an active workspace presentation or display-field rule still calls `optionLabel` for that path; update the dependent rule first.
## Example
[Section titled “Example”](#example)
Two `DisplayName` rules — a specific rule first, a default last:
```text
Condition: Nickname IS NOT EMPTY
Template: {upper(Code)} — {Nickname}
Condition: (empty — always matches)
Template: {upper(Code)} — {coalesce(Name, 'unnamed')}
```
## Configuration
[Section titled “Configuration”](#configuration)
Presentation rules are part of the record type’s display settings, edited together with numbering in the Constructor. Through the [Configuration API](/docs/developer/configuration-api-reference/) the [update-display-settings operation](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-update-display-settings/) (`PUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/display-settings`) persists the complete rule list with sort orders and row versions.
Validate a condition or template before saving with the [validate-rule-expression operation](/docs/developer/configuration-api-reference/operations/admin-entity-definitions-validate-rule-expression/) (`POST /api/workspace/admin/entity-definitions/{entityDefinitionId}/rule-expressions/validate`) using `ContextType` `Presentation` and `ExpressionType` `Condition` or `Template`. The shared validation contract is described on [Rule expressions](/docs/dsl/rule-expressions/).
# Reference Eligibility expressions
> Current and Candidate roots, supported boolean composition, scalar paths, and V1 limits.
A Reference Eligibility expression returns `TRUE` when `Candidate` may be assigned to the dependent Reference on `Current`. The server compiles the same expression for metadata dependency analysis and impact SQL.
This expression profile is used by validation, metadata authoring and activation, impact preview, Form and Table-filter candidate consumers, and authoritative API mutation validation.
## Roots and paths
[Section titled “Roots and paths”](#roots-and-paths)
* `Current.Field` reads a direct persisted scalar on the source record.
* `Candidate.Field` reads a direct persisted scalar on a possible target.
* `Current.Reference.Field` reads one supported scalar through a direct, single-value Reference whose delete behavior is `Restrict`.
Examples:
```text
Current.Type = 'Feature' AND Candidate.Type IN ('Epic', 'Feature')
Candidate.Code = Current.Scope.Code
(Current.Type = 'Bug' AND Candidate.Type IN ('Feature', 'Task'))
OR (Current.Type = 'Feature' AND Candidate.Type IN ('Epic', 'Feature'))
```
Use `AND`, `OR`, `NOT`, and parentheses to compose cases. `IN (...)` is useful when one driver value permits several candidate values. Comparisons are type-checked: text compares with text, Select values use existing option keys, and numeric/date/time values use compatible values from the same family.
The Entity Definition editor offers the same shared code editor and syntax help as other Moltaro expressions. Its simple designer inserts one compatible `Candidate`-to-`Current` equality or inequality. Use the code editor for multi-branch matrices; the editor preserves the expression text and the server remains the parser and type-checking authority.
## V1 limits
[Section titled “V1 limits”](#v1-limits)
The V1 profile deliberately rejects:
* paths deeper than one Reference hop;
* collection fields, child tables, and inverse references;
* variables and functions;
* raw SQL;
* null-driver rules such as `Current.Driver IS NULL`;
* Part, Association, and Projection traversal;
* References that do not use `Restrict`.
Supported scalar operands are Bool, Integer, a valid Decimal, DateOnly, TimeOnly, DateTimeOffset, a bounded String, a Select with options, and a scalar Reference id. Text, read-only, calculated, deleted, collection, and malformed field shapes are not exposed as operands.
The validation endpoint returns stable codes plus the offending token or canonical path. See the [Reference Eligibility configuration workflow](/docs/configuration/reference-eligibility/) for routes, impact behavior, concurrency, and activation.
# Rule expressions
> Field behavior, mutation-effect, presentation, validation, and statement expressions.
Rule expressions are boolean conditions and text templates attached to an entity definition. They cover four surface families, each with its own page, all sharing the expression language described in [Syntax and types](/docs/dsl/syntax-and-types/) and one validation endpoint.
## Expression surfaces
[Section titled “Expression surfaces”](#expression-surfaces)
| Surface | What it does |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| [Field behavior conditions](/docs/dsl/field-behavior/) | `VisibleWhen`, `RequiredWhen`, and `EditableWhen` expressions on entity fields, evaluated per operation mode. |
| [Declarative logic](/docs/developer/business-logic/declarative-logic/) | Mutation-effect `ApplyWhen` conditions evaluated before the effect source and fallback selection. |
| [Presentation rules](/docs/dsl/presentation-rules/) | Templates and conditions that compute a record’s display name and subtitle. |
| [Statements](/docs/dsl/statements/) | Named boolean facts compiled to PostgreSQL for server-side filtering and runtime predicate payloads. |
Validation rules share the same language but are managed and validated through their own endpoints; see [Validation rule expressions](/docs/dsl/validation-rules/).
## Shared validation endpoint
[Section titled “Shared validation endpoint”](#shared-validation-endpoint)
These surfaces are checked by the same operation before saving:
```http
POST /api/workspace/admin/entity-definitions/{entityDefinitionId}/rule-expressions/validate
```
The request selects the context with `ContextType` (`FieldBehavior`, `MutationEffect`, `Presentation`, or `Statement`) and the language variant with `ExpressionType` (`Condition` for boolean expressions, `Template` for presentation text templates). The expression itself is sent as `Value`; field behavior expressions also pass `EntityDefinitionTableId` to name the table whose field schema applies.
Mutation-effect validation uses `ContextType: MutationEffect` and `ExpressionType: Condition`. The expression is bound to the primary table and may use primary scalar fields, `Operation.Mode`, and one direct Reference hop. The endpoint also enforces the mutation-effect limits: 8 KiB UTF-8, 256 AST nodes, boolean depth 32, and at most 16 distinct paths.
The check saves nothing and returns `IsValid` plus structured errors: each error carries a message, the offending token and resolved field path when available, and a stable code. The endpoint is part of the [Configuration API](/docs/developer/configuration-api-reference/).
# Statements
> Named boolean facts on an entity definition, compiled to PostgreSQL and evaluated server-side.
Statements are named, reusable boolean predicates stored on an entity definition. A statement captures one operational fact — overdue, blocked, eligible, needs review — as a single boolean expression, so screens, filters, and logic share one definition of that fact instead of repeating slightly different conditions in different places.
The expression is validated against the entity’s current schema when it is saved, then compiled to PostgreSQL. Runtime queries evaluate the compiled predicate on the server at query time; clients never evaluate statements themselves. Statements power two runtime surfaces: server-side filtering in the instance query model, and runtime predicate payloads used by grids, cards, and highlights.
## Name and display name
[Section titled “Name and display name”](#name-and-display-name)
`Name` is the stable technical key. It follows the same identifier rules as entity and field keys, and it is what query filters and runtime metadata bind to — rename it as deliberately as a field key. `DisplayName` is the human-facing label and can change freely.
## Expression profile
[Section titled “Expression profile”](#expression-profile)
A statement is one boolean expression — not a query. It can use:
* root scalar fields, supported system fields, and first-level `Reference` scalar fields;
* the shared operators — comparisons, `AND`/`OR`/`NOT`, text operators (`CONTAINS`, `STARTS WITH`, `ENDS WITH`), `IN`, `NOT IN`, `BETWEEN`, `IS NULL`, `IS EMPTY`;
* variables: `@today`, `@today+7d`, `@today-7d`, `@currentUserId`;
* direct child-table checks: `EXISTS Table WHERE (…)` and `COUNT(Table) `;
* Classifier categories through the portable `CATEGORY(...)` literal.
The supported system fields are `Number`, `DisplayName`, `CreatedAt`, `CreatedByUserId`, `ModifiedAt`, `ModifiedByUserId`, `LastActivityDateTime`, `ArchivedAt`, and `ArchivedByUserId`.
```text
DueDate < @today AND Status != 'closed'
DueDate >= @today+7d AND OwnerUser = @currentUserId
EXISTS Items WHERE (Quantity > 0) AND COUNT(Items) BETWEEN 1 AND 3
Capability = CATEGORY('product-area', 'operations', 'platform')
```
Classifier fields support `=`, `!=`, `IN`, `NOT IN`, `IS NULL`, and `IS NOT NULL`. Exact and set comparisons do not match null, and field-to-field equality or inequality requires the same Classifier Catalog. A statement compares the stored category identity, not its display name or breadcrumb; changing a Classifier expression requires Catalog access, but the saved statement evaluates independently of the viewer. See [Classifier category values](/docs/dsl/syntax-and-types/#classifier-category-values).
## What statements cannot do
[Section titled “What statements cannot do”](#what-statements-cannot-do)
* raw SQL, joins, or direct table access;
* references to other statements;
* inverse-reference traversal;
* Reference chains deeper than one step (`Customer.Owner.Department`);
* child tables deeper than one direct level;
* sorting — query results cannot be sorted by a statement.
## Exposing results to clients
[Section titled “Exposing results to clients”](#exposing-results-to-clients)
`ExposeInClient` controls whether the statement’s boolean result is published to clients. When it is `true`, runtime list and detail payloads include the result in their `Predicates` map, keyed by statement id, where grids, cards, and highlights consume it.
Two properties make exposure a deliberate publication decision:
* Only the boolean is published — for Classifier expressions the payload carries no Catalog or category identity — but a boolean can still reveal a derived business fact.
* Field-level read masking does not suppress an exposed statement. If the expression depends on fields the caller cannot read, the caller still sees the boolean result. This is intentional: exposing a statement publishes the derived fact itself, independent of field-level access to its inputs.
Keep `ExposeInClient = false` for server-only statements that should not become part of the client-visible derived-information surface.
## Filtering by a statement
[Section titled “Filtering by a statement”](#filtering-by-a-statement)
Statements are leaf conditions in the same filter tree as field conditions in the [instance query endpoint](/docs/developer/api-reference/). The field reference targets the statement by `StatementId` or `StatementKey`, and the condition compares against one boolean literal with `Eq` or `Ne`:
```json
{
"Filter": {
"Operator": 0,
"Conditions": [
{ "Field": { "StatementKey": "Overdue" }, "Operator": 0,
"Values": [ { "Value": true } ] }
]
}
}
```
Filtering by an exposed statement is an allowed way to act on the derived fact even when the underlying fields are not directly readable.
## Management and validation
[Section titled “Management and validation”](#management-and-validation)
Statements are managed through the statements operations of the [Configuration API](/docs/developer/configuration-api-reference/):
```text
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/statements
POST /api/workspace/admin/entity-definitions/{entityDefinitionId}/statements
GET /api/workspace/admin/entity-statements/{statementId}
PUT /api/workspace/admin/entity-statements/{statementId}
DELETE /api/workspace/admin/entity-statements/{statementId}
```
Validate an expression before saving with the shared endpoint `POST /api/workspace/admin/entity-definitions/{entityDefinitionId}/rule-expressions/validate` using `ContextType` `Statement` and `ExpressionType` `Condition`. The response reports validity plus structured errors. Shared literal and operator rules are in [Syntax and types](/docs/dsl/syntax-and-types/); the other declarative surfaces are mapped on the [rule expressions hub](/docs/dsl/rule-expressions/).
# Syntax and types
> Literals, operators, field paths, variables, and null handling in Moltaro expressions.
This page describes the shared syntax of Entity Definition expression surfaces. Board Statements reuse the lexical core but intentionally extend or override parts of the operator, temporal, variable, collection, and null semantics described below. For Board-owned expressions, use the [Board Statement profile](/docs/dsl/board-statements/) as the authoritative contract before applying any rule from this page.
## Literals
[Section titled “Literals”](#literals)
| Kind | Examples | Notes |
| ------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Number | `1`, `100`, `12.50` | Decimal separator is always `.` (invariant culture). |
| String | `'active'`, `"VIP"` | Single or double quotes; escapes `\n`, `\r`, `\t`, `\\`, `\'`, `\"`. Not available inside calculated-field arithmetic. |
| Boolean | `TRUE`, `FALSE` | Case-insensitive. |
| Null | `NULL` | Case-insensitive. |
| Classifier category | `CATEGORY('product-area', 'operations', 'platform')` | Statements and validation rules only. The first key identifies the Classifier Catalog; the remaining keys are the category path. |
## Field references
[Section titled “Field references”](#field-references)
* A bare identifier references a field of the record by its key: `Qty`, `Status`, `DueDate`. Rule and validation expressions resolve field keys **case-sensitively** — `qty` does not match `Qty`. Calculated-field expressions normalize same-table field keys, so `qty` and `Qty` resolve to the same field there.
* A dotted path reads one step through a reference field: `Customer.DisplayName`. Only first-level paths are supported; deeper chains such as `Customer.Owner.Department` are rejected.
* Calculated-field expressions are same-table only and do not accept dotted paths.
## Operators
[Section titled “Operators”](#operators)
In precedence order, lowest first (parentheses override as usual):
| Level | Operators |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Boolean | `OR`, then `AND`, then `NOT` |
| Comparison | `=`, `==`, `!=`, `<>`, `>`, `>=`, `<`, `<=` |
| Text | `CONTAINS`, `STARTS WITH`, `ENDS WITH` |
| Membership and range | `IN (a, b, …)`, `NOT IN (a, b, …)`; `BETWEEN low AND high` (statements and validation rules only — see [Availability by surface](#availability-by-surface)) |
| Null and empty | `IS NULL`, `IS NOT NULL`, `IS EMPTY`, `IS NOT EMPTY` |
| Arithmetic (calculated fields only) | `+`, `-`, `*`, `/`, unary `+`/`-` |
Keywords and operators are case-insensitive: `and`, `And`, and `AND` are the same operator.
Board Statements additionally support `BETWEEN`, typed date/time and duration arithmetic, and the exact function set published by their authoring profile. They do not inherit calculated-field arithmetic merely because the operator tokens are the same.
## Null and empty semantics
[Section titled “Null and empty semantics”](#null-and-empty-semantics)
* `IS EMPTY` treats `NULL`, a blank string, and an empty collection as empty.
* Equality: two null values are equal; comparing null with a value orders the null as the smallest value.
* Prefer `IS NULL` / `IS EMPTY` over `= NULL` — they state the intent directly.
* Classifier equality, inequality, `IN`, and `NOT IN` never match a null value. Use `IS NULL` or `IS NOT NULL` explicitly for a Classifier field.
Board Statements use three-valued null propagation: ordinary equality, ordering, text, membership, and arithmetic with `NULL` do not become `TRUE`; use `IS NULL`, `IS NOT NULL`, `IS EMPTY`, or `IS NOT EMPTY` explicitly. The [Board Statement null contract](/docs/dsl/board-statements/#operators-by-value-family) therefore overrides the Entity Definition comparison rules above.
## Classifier category values
[Section titled “Classifier category values”](#classifier-category-values)
Statements and validation rules use a portable typed literal for a Classifier category:
```text
CATEGORY('catalog-key', 'root-key', 'child-key')
```
The first argument is the stable Catalog key. The remaining arguments are the stable node keys from the Catalog’s logical root to the selected category. Display names and rendered breadcrumbs are not part of the comparison. The logical root itself is not a value, while a stored non-leaf category is valid.
Classifier fields support `=`, `!=`, `IN`, `NOT IN`, `IS NULL`, and `IS NOT NULL`. Field-to-field equality or inequality is valid only when both fields use the same Classifier Catalog. Field behavior, presentation, and calculated-field expressions do not support Classifier values.
## Value comparison rules
[Section titled “Value comparison rules”](#value-comparison-rules)
* Numbers compare across integer/decimal representations, including numeric strings.
* Dates and times compare across date, date-time, and parseable string values (invariant format such as `2026-07-25`).
* Strings are trimmed before comparison and compared ordinally — string comparison is case-sensitive.
## Variables
[Section titled “Variables”](#variables)
Statements and validation rules support runtime variables; field behavior and presentation expressions do not.
| Variable | Meaning | Available in |
| ------------------------ | --------------------------------------- | ---------------------------- |
| `@currentUserId` | Identifier of the acting user | Statements, validation rules |
| `@today` | Current date in the workspace time zone | Statements, validation rules |
| `@today+7d`, `@today-7d` | Date offset by a signed number of days | Statements, validation rules |
Example: `DueDate BETWEEN @today AND @today+30d`.
Board Statements expose `@today`, `@now`, and event-only `@currentUserId` under their own eligibility rules. See [Board Statement date and time values](/docs/dsl/board-statements/#date-and-time-values) and the server-owned authoring profile; this Entity Definition availability table does not describe the Board surface.
## Availability by surface
[Section titled “Availability by surface”](#availability-by-surface)
Most Entity Definition condition surfaces share comparisons, `AND`/`OR`/`NOT`, the text operators, `IN` / `NOT IN`, and null/empty checks. A few constructs are gated to specific surfaces and are rejected at validation time everywhere else — most notably `BETWEEN`, which is **not** available in field-behavior or presentation conditions even though `IN` / `NOT IN` are.
| Construct | Field behavior | Presentation | Statements | Validation rules | Calculated fields |
| ------------------------------------------------------------------- | :------------: | :-----------: | :--------: | :--------------: | :---------------: |
| `BETWEEN low AND high` | — | — | ✓ | ✓ | — |
| Variables (`@today`, `@currentUserId`) | — | — | ✓ | ✓ | — |
| `CATEGORY(...)` classifier literal | — | — | ✓ | ✓ | — |
| `MATCHES(value, "regex")` | — | — | — | ✓ | — |
| `EXISTS` / `COUNT(child)` child-table forms | — | — | ✓ | — | — |
| Numeric / aggregate functions (`IF`, `COALESCE`, `ROUND`, `SUM`, …) | — | — | — | — | ✓ |
| Text functions (`trim`, `upper`, `lower`, `coalesce`) | — | template only | — | — | — |
Field-behavior and presentation **conditions** are operator-only: they accept no function calls, and a range must be written as `>= low AND <= high`. The text functions apply only inside presentation **templates**. See the [Function reference](/docs/dsl/functions/) for the per-surface function catalog.
## Error reporting
[Section titled “Error reporting”](#error-reporting)
Expressions are parsed and bound against the entity schema when you save or call a validation endpoint. Rule-expression and validation-rule errors include a message, the offending token, the resolved field path where relevant, and a stable error code — enough to locate and fix the error programmatically. Calculated-field errors return the request field, localized message, and stable code. Board Statement diagnostics also return an exact source offset; the authoring UI renders it as a one-based line and column.
# Validation rule expressions
> Object-level save checks written in the Moltaro expression language.
Validation rules are object-level checks that run on entity instance create, update, and import. Each enabled rule holds one boolean expression: it must return `TRUE` for the record to save. A `FALSE` result blocks the save and returns the rule’s failure message. Rules run in ascending sort order, before any C# business logic, and the first failing message is returned.
## Choosing the right check
[Section titled “Choosing the right check”](#choosing-the-right-check)
* Use a field’s `RequiredWhen` condition when the only requirement is that one field has a value.
* Use a validation rule for everyday consistency checks that describe the record as a whole — required-if rules, ranges, date ordering, text quality, first-level reference checks, regex checks — or when the check compares multiple values, checks a reference, or needs a tailored failure message.
* Use a [C# validation function](/docs/developer/business-logic/entity-scoped-logic/) when the logic needs orchestration or goes beyond the expression profile below.
## Expression profile
[Section titled “Expression profile”](#expression-profile)
A validation expression can use:
* scalar fields of the record’s primary table;
* first-level reference paths such as `Customer.DisplayName`;
* the variables `@currentUserId`, `@today`, `@today+7d`, and `@today-7d`;
* the shared operators — comparisons, `AND`/`OR`/`NOT`, null/empty checks, text operators, `IN`, `NOT IN`, `BETWEEN`;
* the validation-only function `MATCHES(value, "regex")` (.NET regex, case-sensitive by default, `(?i)` for ignore-case);
* Classifier categories through the portable `CATEGORY(...)` literal.
Not supported: `EXISTS` and `COUNT` child-table forms (statement-only), aggregate functions, raw SQL, inverse-reference traversal, and deep reference chains.
```text
MATCHES(Code, '^[A-Z]{3}-[0-9]{3}$')
DueDate IS NULL OR DueDate >= StartDate
DueDate IS NULL OR DueDate >= @today
Amount <= Customer.CreditLimit
Capability = CATEGORY('product-area', 'operations', 'platform')
```
Classifier fields support `=`, `!=`, `IN`, `NOT IN`, `IS NULL`, and `IS NOT NULL`. Exact and set comparisons do not match null. Field-to-field equality or inequality requires the same Classifier Catalog. Classifier fields are supported on the record and through a first-level Reference path. In the rule editor, place the cursor in the expression and choose **Insert category** to insert a portable `CATEGORY(...)` value; category names and breadcrumbs are for display only and are not used for matching. The logical root of a Classifier Catalog is not a category value, while stored non-leaf categories are valid values. See [Classifier category values](/docs/dsl/syntax-and-types/#classifier-category-values).
## Failure messages
[Section titled “Failure messages”](#failure-messages)
`Message` is the required fallback failure-message template, with optional per-locale translations (`en`, `uk`, `de`, `pl`, `es`). Templates use the same `{ … }` placeholder style as presentation templates and can render:
* record fields: `{Name}`, first-level paths: `{Customer.DisplayName}`;
* system values: `{Number}`, `{DisplayName}`, `{Rule.Name}`, `{Rule.DisplayName}`, `{CurrentUserId}`, `{Today}`.
A field referenced only in a failure message still counts as a schema dependency: it blocks field deletion and incompatible type changes the same way an expression reference does.
## Authoring workflow
[Section titled “Authoring workflow”](#authoring-workflow)
Validate the expression before saving with the [validate-expression operation](/docs/developer/configuration-api-reference/operations/admin-entity-validation-rules-validate-expression/) — the response reports validity plus structured errors (message, token, field path, and stable code). Rules are managed through the validation-rule endpoints of the [Configuration API](/docs/developer/configuration-api-reference/); reordering takes the full active rule set with row versions.
Creating or changing an expression that uses `CATEGORY(...)` requires Catalog Reader or management access to the referenced Classifier Catalog. A saved rule continues to validate records if that access is later removed, and its name, message, enabled state, and other metadata can still be updated while the expression remains unchanged.
Disabled rules stay stored but are not evaluated.
# Workspace UI Project
> Workspace-owned Vue/TypeScript source compiled by Moltaro into full-page workspace UI pages.
The Workspace UI Project is the frontend authoring surface for one Moltaro installation: a single workspace-owned Vue/TypeScript source project that Moltaro checks and builds with its own packaged toolchain, and that publishes full-page workspace UI pages inside the WebApp. It is the frontend counterpart of the [Net Operation Project](/docs/developer/business-logic/csharp-business-logic/) — the workspace owns the source files, Moltaro owns the Vue, Pinia, Vuetify, TypeScript, Vite, ESLint, and host SDK versions, and a successful build produces an immutable runtime artifact that is activated explicitly.
One project can register multiple pages. Typical uses are workspace-specific operational workflows that configured surfaces cannot express: import conflict resolution, side-by-side comparison, specialized reconciliation, interactive planning, or screens that combine Moltaro data with an external browser API.
## When to use it
[Section titled “When to use it”](#when-to-use-it)
* **Configured Entity UI surfaces first.** The [UI surface library](/docs/user/data-structure/entity-definitions/ui-surface-library/) provides declarative Table, Card, and Form surfaces over entity data with no source code. Reach for a Workspace UI page only when the user experience cannot be represented by those surfaces.
* **C# for business decisions.** The Workspace UI Project owns only frontend source. Validation, mutation, integrations, scheduled work, and inbound HTTP endpoints remain [C# business logic](/docs/developer/business-logic/csharp-business-logic/); custom pages call existing APIs and functions instead of introducing new backend behavior.
## Where it lives in the product
[Section titled “Where it lives in the product”](#where-it-lives-in-the-product)
Authoring lives in the Constructor area under the **Automation & logic** group, on two pages:
* **UI Studio** (`/business-logic/ui-development`) — in-browser source editing, templates, Page Wizard generators, and save/check/build actions. Requires the *Manage source* permission.
* **UI Project** (`/business-logic/ui-project`) — project lifecycle: source revisions, build history, artifacts, activation, and ZIP download/upload.
Access is gated by the **Workspace UI Project** permission group: *View*, *Manage source*, *Build*, and *Publish*.
End users never see the project. They open published pages through [configured user menus](/docs/configuration/user-menus/) — the runtime menu leaf type is **Workspace UI page** — or directly by stable route:
```text
/apps/
```
The runtime page title shown for these routes is “Workspace page”.
## Trust and security model
[Section titled “Trust and security model”](#trust-and-security-model)
Workspace UI source is trusted workspace code compiled by Moltaro. The boundary is governance and review — explicit *Manage source*, *Build*, and *Publish* permissions plus revision and build history — not a hostile-code sandbox. A published page can show any data the signed-in user is allowed to read, so treat page source with the same review discipline as C# logic.
* Moltaro owns the framework, toolchain, and design-system versions. Source compiles against a versioned host contract; there is no user-managed frontend stack.
* Pages render inside the host-owned `Default` or `Fullscreen` layout and may use only compiler-isolated scoped or inline styling — never global application CSS.
* After a Moltaro update the source may need a rebuild, and a breaking host contract change may require source edits. `GET /api/workspace/admin/ui-project/compatibility` reports whether the active artifact still matches the host. See [Build, publish, and upgrade](/docs/developer/workspace-ui-project/build-publish-and-upgrade/).
* External browser calls are allowed and remain subject to normal browser and network policy (CORS, TLS, mixed content). Moltaro does not impose an application-level destination allowlist.
* The managed API client attaches the Moltaro bearer token only to app-relative Moltaro paths; it is never attached to an external URL.
* Confidential external credentials belong in server-side [C# logic](/docs/developer/business-logic/csharp-business-logic/). Compiled UI assets are client-delivered code and must never contain secrets.
* Server-side permissions remain authoritative for every Moltaro API call. A page manifest’s `RequiredPermissions` gate menu visibility and client-side access to the page; they do not replace authorization on any API call.
* Telemetry for the managed external client is bounded, and direct browser calls (plain `fetch`) are not captured by Moltaro.
## What it is not
[Section titled “What it is not”](#what-it-is-not)
* Not a visual page designer or no-code layout builder.
* No arbitrary npm dependencies, third-party build plugins, or remote JavaScript/stylesheet imports.
* No hot module replacement, and no activation without a full WebApp refresh.
* No server-side rendering.
* No component injection into standard Moltaro pages or Entity UI surfaces — pages are full-page routes only.
* One Workspace UI Project per workspace; there are no multiple independently activated projects.
* No automatic activation: a successful build produces an inactive artifact until someone with the *Publish* permission activates it.
## For AI agents
[Section titled “For AI agents”](#for-ai-agents)
Everything the UI Studio and UI Project pages do is available through the [Configuration API](/docs/developer/configuration-api-reference/) under `/api/workspace/admin/ui-project`, authenticated with a service-account API key: source revisions, templates, page generators, checks, builds, diagnostics, artifacts, and activation. The full lifecycle is documented in [Build, publish, and upgrade](/docs/developer/workspace-ui-project/build-publish-and-upgrade/).
These routes belong to the configured workspace API host, not the public documentation or portal host. Obtain `WORKSPACE_API_BASE_URL`, the guide, and the key through the [workspace connection handoff](/docs/developer/workspace-api-connection/) before starting API authoring.
Web Application registration, credential provisioning, and Agent integration remain Portal or administrator tasks. If the delivered handoff is incomplete, the agent asks the user to complete it rather than calling administration endpoints to provision itself. Use [Reliable API automation](/docs/developer/reliable-api-automation/) for source concurrency, build polling, activation recovery, and safety rules.
## In this section
[Section titled “In this section”](#in-this-section)
* [Project structure](/docs/developer/workspace-ui-project/project-structure/) — source tree, manifest, generated files, Studio, and the local ZIP workflow.
* [Pages, components, and stores](/docs/developer/workspace-ui-project/pages-components-and-stores/) — page templates, components, namespaced Pinia stores, routing, and localization.
* [Moltaro API client](/docs/developer/workspace-ui-project/moltaro-api-client/) — authenticated calls to Moltaro APIs, errors, and files/forms.
* [External API client](/docs/developer/workspace-ui-project/external-api-client/) — calling external browser APIs and handling credentials safely.
* [Design system](/docs/developer/workspace-ui-project/design-system/) — supported imports, page layouts, and styling rules.
* [Build, publish, and upgrade](/docs/developer/workspace-ui-project/build-publish-and-upgrade/) — check, build, diagnostics, activation, rollback, and rebuilds after updates.
* [Observability and troubleshooting](/docs/developer/workspace-ui-project/observability-and-troubleshooting/) — Monitoring signals, build history, and runtime failure recovery.
For the end-user view of published pages, see [Workspace pages](/docs/user/workspace-pages/).
# Build, publish, and upgrade
> The check, build, and explicit activation lifecycle of the Workspace UI Project, driven from UI Studio or the admin API.
The Workspace UI Project separates three deliberate steps: **check** the source, **build** an immutable artifact, and **activate** that artifact for end users. A save is not a deploy and a successful build is not a release. Workspace UI code is trusted workspace code compiled by Moltaro — the boundary is governance and review (permissions, immutable revisions, audited activation), not a hostile-code sandbox.
Everything on this page is available in the Constructor area — **UI Studio** (`/business-logic/ui-development`) and **UI Project** (`/business-logic/ui-project`), group **Automation & logic** — and through the admin API under `/api/workspace/admin/ui-project`, part of the [Configuration API](/docs/developer/configuration-api-reference/). UI Studio shows **Draft — publish required** whenever its working revision is not the active artifact. That status opens UI Project, so the path from a newly created page to Build and Activate remains visible without relying on hidden toolbar actions.
## Checks and builds
[Section titled “Checks and builds”](#checks-and-builds)
`POST /builds` queues one operation for one immutable source revision. The request requires a `RevisionId` — there is no implicit “latest source” build — and a `Kind`:
* **Check** (`Kind` = 0) validates the revision through the compile pipeline and produces no artifact.
* **Build** (`Kind` = 1) runs the same pipeline and stores a successful result as an **inactive immutable artifact**.
Builds run in the background; the build record reports `Status` (Queued, Running, Succeeded, Failed, Cancelled), `Stage`, and `ProgressPercent`. Structured compiler diagnostics stay attached to the build record — code, severity, stage, message, path, and line/column positions — so a failure is inspectable later without re-running it. A failed check or build never replaces the active artifact: whatever users currently see keeps running.
## Explicit activation
[Section titled “Explicit activation”](#explicit-activation)
Exactly one artifact is active at a time. A successful build changes nothing for end users until a caller with the **Publish** permission activates the artifact. Activation is audited, is rejected for artifacts that do not match the current host contract, and returns `RefreshRequired` — already-open sessions keep the previous UI until a full WebApp refresh, while new sessions load the new artifact. Active pages are served under `/apps/` as “Workspace page” and can be linked from user menus as “Workspace UI page” leaves (see [User menus](/docs/configuration/user-menus/)).
Three more lifecycle operations complete the picture:
* **Deactivate** takes the currently active artifact out of service; end-user pages become unavailable until another activation.
* **Rollback** restores a compatible previously published artifact — the fast path back when a release misbehaves.
* **Purge** deletes the stored JavaScript/CSS bytes of an inactive artifact while retaining its history metadata (hashes, sizes, versions, publisher).
Artifact state changes carry expected project and artifact row versions and fail with a conflict when concurrent administration changed state first.
## Compatibility after a Moltaro update
[Section titled “Compatibility after a Moltaro update”](#compatibility-after-a-moltaro-update)
Each artifact records the WebApp version, host contract version, and toolchain fingerprint it was built with. `GET /compatibility` reports whether the packaged build toolchain and runtime contract are ready and, with `?artifactId=`, whether one stored artifact still matches the current host contract (`IsCompatible`, `CanActivate`, `CanRollback`, plus safe localized diagnostics).
After a Moltaro update, the active artifact may no longer match the new host contract. Source revisions are stored server-side, so the remedy is a rebuild from the stored source followed by a new activation. A breaking host-contract change can additionally require source edits before the rebuild succeeds; the check pipeline and its diagnostics report exactly what broke.
If the active artifact is runtime-incompatible, only its custom `/apps/*` pages are unavailable; core Moltaro remains usable. Fix any source diagnostics, run Build, review the newly stored inactive artifact, Activate explicitly, and then perform a full browser refresh. Failed or stale checks/builds preserve the source revisions, history, and prior artifact, and no update or build auto-activates output.
## Endpoints and permissions
[Section titled “Endpoints and permissions”](#endpoints-and-permissions)
All routes live under `/api/workspace/admin/ui-project` and are gated by the **Workspace UI Project** permission group — **View**, **Manage source**, **Build**, **Publish**. Owner, Admin, and Configurator hold all of them implicitly.
| Endpoint | Permission | Purpose |
| ---------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------- |
| `GET /status` | any of the group | Project status; provisions the empty six-file system skeleton for source-capable callers |
| `GET /revisions`, `GET /revisions/{revisionId}` | View, Manage source, or Build | Immutable source revisions, newest first |
| `GET /source-tree` | Manage source | Source-tree metadata for one revision |
| `GET /source-files` | Manage source | One UTF-8 source file by revision and path |
| `POST /source-revisions/manual-edit` | Manage source | Create one immutable manual-edit revision |
| `GET /source-templates` | Manage source | Deterministic source templates |
| `POST /source-templates/preview`, `POST /source-templates/apply` | Manage source | Preview, or apply a template as one revision |
| `GET /page-generators/catalog` | Manage source | Typed page-generator, entity-field, and working-page capabilities |
| `POST /page-generators/preview`, `POST /page-generators/apply` | Manage source | Preview or apply one generated page; menu placement needs a trusted role |
| `DELETE /revisions/{revisionId}` | Manage source | Delete a disposable revision and its non-artifact build history |
| `GET /download` | Manage source | Deterministic source and generated IDE-project ZIP |
| `POST /upload` | Manage source | Upload a ZIP as one immutable revision, without auto-queuing a build |
| `GET /builds`, `GET /builds/{buildId}` | View or Build | Check and build history with diagnostics |
| `POST /builds` | Build | Queue a check or build for one revision |
| `POST /builds/{buildId}/cancel` | Build | Request cancellation of a queued or running build |
| `GET /artifacts`, `GET /artifacts/{artifactId}` | View or Publish | Stored artifacts, newest first |
| `GET /compatibility` | View or Publish | Toolchain, runtime-contract, and optional artifact compatibility |
| `POST /artifacts/{artifactId}/activate` | Publish | Activate one compatible inactive artifact |
| `POST /artifacts/{artifactId}/deactivate` | Publish | Deactivate the currently active artifact |
| `POST /artifacts/{artifactId}/rollback` | Publish | Roll back to a compatible previously published artifact |
| `POST /artifacts/{artifactId}/purge` | Publish | Purge bytes from an inactive artifact, retaining history metadata |
## Authoring through the API
[Section titled “Authoring through the API”](#authoring-through-the-api)
The full authoring loop works headlessly with a service-account API key sent as `Authorization: Bearer ` (see the [integration quickstart](/docs/developer/integration-quickstart/)). Enums are serialized as numbers; the OpenAPI document names each value through `x-enum-varnames`.
**1. Read the project state** (View). `GET /status` returns the project status and, for source-capable callers, provisions only `workspace-ui.json` plus the five empty locale files on first call. It never creates sample pages, stores, clients, translations, or a README.
**2. Read the source** (Manage source). `GET /source-tree` lists file metadata for the working or a requested revision; `GET /source-files?path=src/pages/MachineStatusPage.vue` returns one UTF-8 file.
**3. Edit** (Manage source). Create an immutable revision from file changes:
```http
POST /api/workspace/admin/ui-project/source-revisions/manual-edit
Authorization: Bearer
Content-Type: application/json
```
```json
{
"BaseRevisionId": "0197f2…",
"BaseSourceChecksum": "5f8a…",
"Comment": "Add machine status page",
"Changes": [
{ "Operation": 0, "Path": "src/pages/MachineStatusPage.vue",
"ContentUtf8": "…" }
]
}
```
`Operation` 0 upserts, 1 renames (with `NewPath`), 2 deletes. Alternatives that also produce one immutable revision: `POST /source-templates/apply`, `POST /page-generators/apply`, or `POST /upload` with a ZIP. Each response — like `GET /revisions` — carries the new revision `Id`.
**4. Check, then build** (Build). `RevisionId` is required; a `null` value is rejected. Queue a check first (`"Kind": 0`), and once it passes, a build:
```json
{ "RevisionId": "0197f3…", "Kind": 1 }
```
The response is the queued build record with its `Id`.
**5. Poll the build** (View or Build). `GET /builds/{buildId}` until `Status` is 2 (Succeeded), 3 (Failed), or 4 (Cancelled). On failure, read the attached diagnostics:
```json
{
"Status": 3,
"ErrorSummary": "1 error",
"Diagnostics": { "Diagnostics": [
{ "Code": "TS2304", "Severity": 2, "Message": "Cannot find name 'refx'.",
"Path": "src/pages/useMachineStatusPage.ts", "StartLine": 12 }
] }
}
```
**6. Find the artifact** (View or Publish). `GET /artifacts` lists stored artifacts newest first; match the new one by its `BuildId` and `RevisionId`. It is inactive until published.
**7. Activate** (Publish):
```http
POST /api/workspace/admin/ui-project/artifacts/{artifactId}/activate
```
```json
{ "ExpectedProjectRowVersion": "…", "ExpectedArtifactRowVersion": "…" }
```
The result reports `RefreshRequired: true`.
**8. Tell users to refresh.** Open sessions keep the previous UI until a full WebApp refresh; after refreshing, users see the new pages under `/apps/`.
# Design system and constraints
> Allowed imports, shared components, page layouts, and styling rules for Workspace UI pages.
Workspace UI pages run inside the Moltaro WebApp on the host’s own Vue, Pinia, Vuetify, vue-router, and vue-i18n instances. The design system is deliberately closed: an explicit import allowlist, a curated shared-component barrel, and a compiler that rejects any style able to escape the page. Workspace UI code is trusted workspace code compiled by Moltaro — the boundary is governance and review, not a hostile-code sandbox.
## Allowed imports
[Section titled “Allowed imports”](#allowed-imports)
The build accepts imports only from:
* `vue`, `pinia`, `vue-router`, `vue-i18n`;
* approved `vuetify/components` and `vuetify` composable exports;
* `@moltaro/ui` (the barrel below) and the `@moltaro/workspace-ui` host SDK ([pages, components, and stores](/docs/developer/workspace-ui-project/pages-components-and-stores/));
* local project-relative paths and the generated `@workspace-ui/*` alias ([project structure](/docs/developer/workspace-ui-project/project-structure/)).
Everything else is a build error: Node built-ins, build-tool modules, WebApp internals (`@/*`), remote URL modules, and unapproved npm packages. The check covers static and supported dynamic import forms alike.
The framework modules are further restricted to specific named exports, so an allowed module does not mean every export is importable:
* `vue-router` — only `useRoute`. Navigate with [`useNavigation()`](/docs/developer/workspace-ui-project/pages-components-and-stores/#navigation), not `useRouter` or `RouterLink`.
* `pinia` — only `defineStore` and `storeToRefs`.
* `vue-i18n` — only `useI18n` (see localization below).
* `vue` — reactivity, lifecycle, and `defineComponent` are available, but rendering and app-creation APIs (`h`, `render`, `createApp`, `Teleport`, `resolveDynamicComponent`, `cloneVNode`, `getCurrentInstance`) are rejected; use templates and the host composables instead.
A disallowed named import fails the build with a specific `WUI` diagnostic that names the offending symbol.
## Shared components: `@moltaro/ui`
[Section titled “Shared components: @moltaro/ui”](#shared-components-moltaroui)
| Component | Purpose |
| --------------------------- | ----------------------------------------------------------------------- |
| `AppCardHeader` | Standard card header with title, subtitle, eyebrow, and action row |
| `AppEmptyState` | Icon-plus-title empty or error state with an optional actions slot |
| `ConfirmDialog` | Confirmation dialog before destructive or irreversible actions |
| `DetailHeaderPanel` | Record detail header: eyebrow, title, number label, busy state, actions |
| `FormDialog` | Dialog shell for short forms with the shared header and footer contract |
| `MoltaroGridBase` | Shared AG Grid wrapper with Moltaro column and rendering conventions |
| `OperationalPageToolbar` | Toolbar for operational console pages |
| `OperationalUtilityDrawer` | Right-side utility drawer for filters, sorting, and columns |
| `PropertyItem` | Read-only label/value fact row for detail surfaces |
| `UtilityDrawerSection` | Collapsible content section inside a utility drawer |
| `UtilityDrawerSectionTitle` | Title row for a utility drawer section |
The barrel also exports the TypeScript types these components use, such as `MoltaroGridColumnDefinition` and `MoltaroGridRowClickedEvent`. Every export is part of the host contract; other WebApp components are unreachable.
## Page layouts
[Section titled “Page layouts”](#page-layouts)
Each manifest page declares one host-owned `Layout`; a missing or unknown value is a build error.
* **Default** — the normal document layout: the Moltaro navigation shell plus a container that grows with content and scrolls at document level. Use it for forms, detail pages, reports, and vertically flowing surfaces.
* **Fullscreen** — the Data Explorer-style application layout: the same shell plus a root that fills the content viewport with no document-level scrolling; the page owns its internal scroll areas. It never hides Moltaro navigation and never calls the browser Fullscreen API.
Both layouts expose the same Vue/Vuetify context; responsive behavior stays page-owned via `useDisplay()`, breakpoint props, and media or container queries. A page can read its resolved layout but cannot replace the shell.
## Styling rules
[Section titled “Styling rules”](#styling-rules)
Pages may use `