Skip to content

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.json
GET ${WORKSPACE_API_BASE_URL}/openapi/moltaro-public-v1.json

Public 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.

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.

Read Form options before authoring:

GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/form-surfaces/configuration/options

Data.RelatedTableTargets is the server-authoritative list. Each entry contains:

  • Key and Label for the Designer;
  • Target, the exact inverse field target to write back;
  • ChildEntityDefinitionId and child display names;
  • ChildReferenceFieldId, ChildReferenceFieldKey, and display name;
  • TableSurfaces, the child tables valid for CustomKey selection.

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 }
]
}

Form configuration is a complete replacement, not an item patch:

GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/form-surfaces/{surfaceKey}/configuration
PUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/form-surfaces/{surfaceKey}/configuration
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/form-surfaces/{surfaceKey}/configuration

Preserve 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.

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: 8
UiSurfaces:
- 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: true

If 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.

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.

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.

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 View is required to resolve the parent instance;
  • child View is 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.

An agent should finish the workflow only after it has:

  1. Read the installed Configuration and Runtime OpenAPI documents.
  2. Read RelatedTableTargets; never invent an inverse target or table key.
  3. Read the complete Form configuration and preserve all unrelated items.
  4. Write one item with ItemType = 3 and only eligible selectors.
  5. Read the Form configuration back and verify item id, inverse field, order, visibility, label/hint, selectors, priorities, keys, and roles.
  6. Export YAML v8 and verify the same values.
  7. Bootstrap with two different parents and verify each response contains only its own child rows.
  8. Test an editor, reader, and no-access actor; verify denied data does not appear in rows, counts, fields, actions, or errors.
  9. Change the Form item or selected Table Surface and verify an old ContextRevision fails closed.
  10. Verify Delete is absent for active rows; Archive a row, read Archived scope, then Delete it with the new row version.
  11. 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.