Form Related Table API and automation
This guide is the public contract for developers and AI agents that configure or consume a Related Table on an Entity Form. The item binds one persisted Form item to one direct inverse relationship. Moltaro owns the runtime relation filter and locked parent value; clients choose neither.
GET ${WORKSPACE_API_BASE_URL}/openapi/moltaro-config-v1.jsonGET ${WORKSPACE_API_BASE_URL}/openapi/moltaro-public-v1.jsonPublic JSON properties use PascalCase. Enums are numeric in JSON. Relevant
values are EntityUiItemType.RelatedTable = 3,
EntityUiSurfaceSelectorMode.Default = 0, and CustomKey = 1. Discover exact
schemas and enum metadata from OpenAPI instead of copying a request shape from
a different installation version.
Supported model
Section titled “Supported model”V1 accepts exactly one direct, explicitly paired InverseReference from a
standalone parent Data entity to a standalone child Data entity. The paired
child field must be one direct scalar Reference back to the parent.
It rejects Part, Association, Card items, generated Association inverse
links, multi-hop paths, owned Table fields, and relations whose endpoints or
pairing are not current. There is no client-defined runtime filter, binding, or
Unlink operation.
Discover an eligible target
Section titled “Discover an eligible target”Read Form options before authoring:
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/form-surfaces/configuration/optionsData.RelatedTableTargets is the server-authoritative list. Each entry
contains:
KeyandLabelfor the Designer;Target, the exact inverse field target to write back;ChildEntityDefinitionIdand child display names;ChildReferenceFieldId,ChildReferenceFieldKey, and display name;TableSurfaces, the child tables valid forCustomKeyselection.
An empty list is a supported result: the parent currently has no eligible relationship. Do not construct a target from field names when it is absent.
Example abbreviated option:
{ "Key": "field-parent-work-items", "Label": "Work items", "Target": { "TargetKind": 0, "EntityFieldDefinitionId": "field-parent-work-items", "SystemField": null, "ResponsibilityKey": null, "DisplayFieldId": null, "ObjectContextFactId": null }, "ChildEntityDefinitionId": "entity-work-item", "ChildEntityDisplayNameSingular": "Work item", "ChildEntityDisplayNamePlural": "Work items", "ChildReferenceFieldId": "field-work-item-parent", "ChildReferenceFieldKey": "Parent", "ChildReferenceFieldDisplayName": "Parent", "TableSurfaces": [ { "Key": "related", "Name": "Related work items", "IsDefault": false } ]}Configure with read-modify-write-read
Section titled “Configure with read-modify-write-read”Form configuration is a complete replacement, not an item patch:
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/form-surfaces/{surfaceKey}/configurationPUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/form-surfaces/{surfaceKey}/configurationGET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/form-surfaces/{surfaceKey}/configurationPreserve MaxWidth, ColumnCount, and every existing Items entry. Add or
change only the intended item, send the complete document, then read it back.
The following fragment illustrates the Related Table item; it is not a complete
PUT request:
{ "Id": "work-items", "ItemType": 3, "SortOrder": 40, "Column": 1, "ColumnSpan": 2, "Label": "Work items", "Hint": "Changes to work items are saved immediately.", "Visible": true, "Binding": { "RefKind": 0, "FieldId": "field-parent-work-items", "FieldKey": "WorkItems", "FieldType": 14, "ReferenceToEntityDefinitionId": "entity-work-item", "PairedReferenceFieldId": "field-work-item-parent" }, "RelatedTableSelectors": [ { "Id": "work-items-editors", "Priority": 20, "Mode": 1, "SurfaceKey": "related", "RoleIds": ["role-editor"] }, { "Id": "work-items-default", "Priority": 0, "Mode": 0, "SurfaceKey": null, "RoleIds": [] } ]}This fragment assumes a two-column Form. The server normalizes Column to 1
and ColumnSpan to the Form’s current ColumnCount, so readback is always a
full Form row. Clients should
not attach field editor, reference renderer, hierarchy, lookup selector, text,
alert, margin, or padding behavior to it. The Designer omits Column and Column
span controls even though the shared read model can return normalized layout
values.
Selector resolution is role-aware: higher Priority wins; at equal priority a
role-specific match precedes a global match. An empty list or Default uses
the child’s default Table Surface. CustomKey must be one of that child’s
Table Surfaces. It cannot select a table from another entity.
YAML v8
Section titled “YAML v8”Entity Definition portability stays at YAML version 8. The inverse target is
encoded by the existing field reference members, and selectors use the new
RelatedTableSelectors collection:
Version: 8UiSurfaces: - Surface: Form Key: edit ColumnCount: 2 Items: - Id: work-items ItemType: RelatedTable SortOrder: 40 Label: Work items Hint: Changes to work items are saved immediately. FieldRefKind: EntityField EntityFieldDefinitionId: field-parent-work-items RelatedTableSelectors: - Id: work-items-editors Priority: 20 Mode: CustomKey SurfaceKey: related RoleIds: - role-editor - Id: work-items-default Priority: 0 Mode: Default RoleIds: [] IsVisible: trueIf RelatedTableSelectors is absent or empty, runtime uses the child’s default
Table Surface. Export after import and compare the item id, inverse field id,
selector modes, keys, priorities, and roles. Do not raise the YAML version or
translate the item into a field or hosted-layout related section.
Runtime bootstrap
Section titled “Runtime bootstrap”Load a table only for an existing parent:
POST /api/workspace/entity/{parentEntityIdOrKey}/ui/form/{formSurfaceId}/items/{formItemId}/related-table?instanceId={parentInstanceId}{ "Page": 1, "ArchiveMode": 0}The request deliberately has no target, binding, filter, child entity, or Table Surface property. The server re-resolves the parent Form and item, inverse pair, current actor roles, child Table Surface, schema, security policy, and parent instance on every request.
EntityFormRelatedTableBootstrap returns the actor-shaped Table, either
Page or TreePage, CreateAction, runtime sorting, missing-surface state,
error state, child identity, effective ArchiveMode, explicit
AvailableArchiveModes, and opaque ContextRevision. Flat pages always use
ten rows. The child parent-Reference column is omitted. The locked equality
predicate is combined with Unified Security and runs in SQL before count and
pagination.
Abbreviated archive capability fragment:
{ "ArchiveMode": 0, "AvailableArchiveModes": [0, 1]}Send ArchiveMode = 0 for active children or ArchiveMode = 1 for archived
children. Treat the returned ArchiveMode as the effective scope and replace
local state with it after every response. AvailableArchiveModes is the only
source for the scope control: 0 requires child View; 1 requires child
View plus ReadArchive. Do not infer archive browsing from Archive, Restore,
or Delete row actions. The WebApp never requests All. Create is active-scope
only. Changing scope starts again at page 1 and obtains a fresh
ContextRevision.
For a Tree View table, use only revisions from the current bootstrap:
POST .../related-table/tree/roots?instanceId={parentInstanceId}POST .../related-table/tree/children?instanceId={parentInstanceId}Roots and children require ExpectedContextRevision,
ExpectedTableSurfaceId, ExpectedTableSurfaceRowVersion, page, archive mode,
and—only for children—ParentInstanceId. A context or surface change returns
HTTP 409; discard the old tree state and bootstrap again.
Personal sort settings
Section titled “Personal sort settings”Related Table persists sort only. It does not expose column-layout editing, saved views, filters, or Manual Order:
GET .../related-table/grid-settings?instanceId={parentInstanceId}&expectedContextRevision={contextRevision}PATCH .../related-table/grid-settings?instanceId={parentInstanceId}&expectedContextRevision={contextRevision}Example sort patch:
{ "ExpectedRowVersion": "00000000-0000-0000-0000-000000000000", "ExpectedTableSurfaceId": "table-surface-related", "Fields": [1], "ColumnLayout": null, "Sort": [ { "Field": null, "Target": { "Path": [ { "FieldId": "field-work-item-title", "FieldKey": null, "Special": null, "StatementId": null, "StatementKey": null, "ObjectContextFactId": null, "ObjectContextFactFilterKey": null } ] }, "Direction": 0 } ]}Use the row version returned by GET. On HTTP 409 with
moltaro.ui.gridSettings.concurrency.conflict, read settings again, reconcile
the user’s intended sort, and retry once with the new version. On
moltaro.ui.surface.conflict, bootstrap the Related Table again because its
Form item, relation, roles, schema, parent, or selected table is stale.
Child mutations and security
Section titled “Child mutations and security”CreateAction is server-shaped. If CanCreate is true, use its child entity,
Form selector, and LockedFields; do not let the caller edit or omit the
locked parent Reference. Edit uses the actor-shaped Form selected by the child
Table action. Open details uses the selected child Details page in a new tab.
Archive/Delete use ordinary Entity Instance mutation routes with their current
row version, operation key, confirmation, and audit reason.
Delete is a two-step lifecycle operation: the row must already have
ArchivedAt, and the grid must not expose Delete for an active row. After
Archive, the WebApp automatically switches to ArchiveMode = 1, bootstraps the
archived row, then submits Delete with that row’s current row version. An API
client must perform the same second bootstrap explicitly. Do not reuse a row
version or ContextRevision captured before Archive.
Every child operation is a separate committed mutation. It must refresh only this table and must not clear or submit the parent draft. Cancelling the parent cannot undo a completed child mutation.
Unified Security is authoritative for parent and child independently:
- parent
Viewis required to resolve the parent instance; - child
Viewis applied SQL-before-pagination; - denied child fields are omitted from columns and values;
- Create, Update, Archive, and Delete require their respective child action;
- Admin/Owner status is not a record-access shortcut;
- unreadable and unrelated child records must not affect count or page data.
Stable failures include moltaro.instances.access.denied,
moltaro.ui.surface.notFound, moltaro.ui.surface.conflict,
moltaro.ui.binding.field.invalid, and
moltaro.ui.gridSettings.sort.invalid. Branch on Errors[].Code, not a
localized message. See Errors and responses.
Agent completion checklist
Section titled “Agent completion checklist”An agent should finish the workflow only after it has:
- Read the installed Configuration and Runtime OpenAPI documents.
- Read
RelatedTableTargets; never invent an inverse target or table key. - Read the complete Form configuration and preserve all unrelated items.
- Write one item with
ItemType = 3and only eligible selectors. - Read the Form configuration back and verify item id, inverse field, order, visibility, label/hint, selectors, priorities, keys, and roles.
- Export YAML v8 and verify the same values.
- Bootstrap with two different parents and verify each response contains only its own child rows.
- Test an editor, reader, and no-access actor; verify denied data does not appear in rows, counts, fields, actions, or errors.
- Change the Form item or selected Table Surface and verify an old
ContextRevisionfails closed. - Verify Delete is absent for active rows; Archive a row, read Archived scope, then Delete it with the new row version.
- Reload after Add/Edit/Archive/Delete and verify persisted child state while the parent draft remains independent.
For UI setup, see Configure Form Related tables. For the user workflow, see Related tables in forms.