Form and Card 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 or Card. The item binds one persisted leaf-surface 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, 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 options for the target Form or Card before authoring:
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/form-surfaces/configuration/optionsGET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/card-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 and Card configuration are complete replacements, not item patches:
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}/configuration
GET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/card-surfaces/{surfaceKey}/configurationPUT /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/card-surfaces/{surfaceKey}/configurationGET /api/workspace/admin/entity-definitions/{entityDefinitionId}/ui/card-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, "ShowRelatedTableSaveNotice": 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.
For Card, preserve every Tabs, Items, and LabelRules entry and the
LabelPlacement and HideEmptyFields values. The item may
set TabId; the server always reads it back at structural ColumnSpan = 3.
Clients and package code cannot author a narrower Card Related Table.
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.
ShowRelatedTableSaveNotice is optional and defaults to true on Form and
Card Related Table items. It controls the independent-save notice on Form only;
Card hosts do not display this notice. Set it to false to hide the Form notice.
Clone, configuration readback, runtime bootstrap and YAML preserve this value;
child operations still save independently.
YAML v10
Section titled “YAML v10”Entity Definition portability uses YAML version 10. The inverse target is
encoded by the existing field reference members, and selectors use the new
RelatedTableSelectors collection:
Version: 10UiSurfaces: - 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. ShowRelatedTableSaveNotice: true 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}POST /api/workspace/entity/{parentEntityIdOrKey}/ui/card/{cardSurfaceId}/items/{cardItemId}/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/Card and item, inverse pair, current actor roles, child Table Surface, schema, security policy, and parent instance on every request.
EntityFormRelatedTableBootstrap or EntityCardRelatedTableBootstrap 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
leaf 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.
Save children with the parent (Form submission)
Section titled “Save children with the parent (Form submission)”A Form Related Table item with RelatedSaveMode = 1 (WithParent) does not
write children on its own. The configuration members of the item are:
| Member | Values | Meaning |
|---|---|---|
RelatedSaveMode | 0 Independent (default), 1 WithParent | Form only; Card is always Independent |
RelatedPresentation | 0 Grid (default), 1 Inline | Inline needs a unique constraint on the child Reference proving at most one child |
RelatedVisibleWhen, RelatedRequiredWhen | Field Behavior condition text or null | Over the parent record; decided again on the saved parent |
RelatedHiddenPolicy | 0 Keep, 1 Block, 2 Archive | What a save does with the children of a hidden section |
RelatedFormSelectors | surface selectors, slot RelatedForm = 10 | The child Form; default is the child’s default Form |
A configuration PUT sends RelatedSaveMode on every Related Table item, also
its default 0 (it is required in OpenAPI). An item without it is the item of a
client that does not know these members: when its stored settings are not the
defaults, the PUT is refused with HTTP 409
moltaro.ui.configuration.clientOutdated (Field
Items[{itemId}].RelatedSaveMode) and changes nothing, because an older client
would otherwise reset the section to independent saves. With RelatedSaveMode
present, the other five members may be left out the way a serializer that omits
nulls or defaults leaves them out: RelatedVisibleWhen and RelatedRequiredWhen
then read as null, RelatedFormSelectors as [], and RelatedPresentation
and RelatedHiddenPolicy as 0. YAML import and packages always carry the
members.
Several Related Table items may bind the same relation (for example a grid and
an inline view). They must agree on RelatedSaveMode, RelatedVisibleWhen,
RelatedRequiredWhen and RelatedHiddenPolicy; RelatedPresentation and
RelatedFormSelectors may differ. A later item that disagrees is refused with
moltaro.ui.relatedSaveMode.invalid at its first differing member.
The runtime Form model then carries, per WithParent item, RelatedChild
(child definition, cardinality, potential capabilities, the child Form,
ParentReferenceFieldKey, SectionItemId and PreauthorizationParentFieldKeys,
see Child requests before the parent is saved)
and the actor-shaped plans
RelatedVisibleWhenPlan / RelatedRequiredWhenPlan. SectionItemId is the
item that owns the relation’s one child draft, the first WithParent item of the
relation in Form order; every item of the relation names it. Stage each child
change once under it. Every stored Form carries Submission { Revision, MaximumRequestBytes, MaximumSteps }. A Form with a WithParent item saves
through the route below, and so does a related-create of any create Form (it
sends Origin; the WebApp’s related-create wizard always does):
POST /api/workspace/entity/{entityIdOrKey}/ui/form/{formSurfaceId}/submissions{ "OperationKey": "8a2e7d4c-1f0b-4c3e-9a77-2c1f6b9d5e10", "ExpectedRevision": "r1.<from Submission.Revision>", "Root": { "EntityInstanceId": null, "RowVersion": null, "Fields": { "Title": "Case 1" }, "Resources": { "FileUploadCreateKey": "<uuid of the root's direct uploads>" } }, "Sections": [ { "ItemId": "<RelatedChild.SectionItemId>", "Children": [ { "LocalId": "c1", "Create": { "Fields": { "Title": "Detail 1" } } }, { "LocalId": "c2", "Archive": { "EntityInstanceId": "<id>", "RowVersion": "<seen>", "Reason": "Closed" } } ] } ]}- Each section names a draft owner (
RelatedChild.SectionItemId), at most once. Another item of the same relation, or a second section of it, is refused withmoltaro.request.invalidatSections[i].ItemId. Conditions, the hidden policy and the Archive set are decided once per relation, andMetadata.ItemIdof an error names the owner. The hidden-policy preview andParentContext.ItemIdaccept any WithParent item of the relation. - Child actions are
Create,Update(changed fields only),ArchiveandDelete, each with aLocalId(^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$, notroot, notpolicy-…). The server sets every relation: a child never sends its Reference to the parent (moltaro.ui.form.submission.fieldServerOwned). - An edit with an empty
Root.Fieldsguards the parent (it must still be current and visible) without writing it; it names noResources. A request that would write no record at all is refused withmoltaro.ui.form.submission.nothingToSave. - A runtime table model may offer the
Editrow action (Action = 5) to a reader withoutUpdateon the record: it keeps the action when the Form of itsFormSelectorhas a WithParent item whose child records the reader may potentially create, update, archive or delete (a children-only edit). Offer it per row from the row’s own rights (readable, active, noUpdate), open that Form withGET /api/workspace/entity/{entityIdOrKey}/ui/form?surfaceKey={FormSelector.SurfaceKey}(the table-action Form route…/ui/table/actions/{actionId}/formstill requiresUpdate), show the record read-only and save with an emptyRoot.Fields(a Guard root) and the child changes. - A related-create sends
Origin { SourceEntityDefinitionId, SourceEntityInstanceId, SourceInverseFieldId }(a Create root only), taken from theCreateAction.Originof the related table or block; the server checks that the actor can read the source record and that the InverseReference pairs with the root’s Reference, then sets that Reference itself. An association record has anOrigintoo: its endpoint Reference pairs with the InverseReference the association generated on the source endpoint.CreateActiondoes not say how to save; the client decides from the create Form it loads. - A hidden section with the Archive policy must carry
HiddenPolicy { AffectedSetToken, Reason }fromPOST …/items/{itemId}/hidden-policy/preview?instanceId={parent}. - The whole request is one transaction within the host policy (default 100 written records, 1 MiB body, 15 s budget); every participant passes the same rights, validations and C# save functions as a single-record save.
A success answers Kind: "EntityFormSubmission" with Root and Children
(LocalId, ItemId, Action 0 Create, 1 Update, 2 Unchanged, 3 Archive,
4 Delete, and Result RecordIdentity / RecordReceipt / RecordRemoved). A
refusal places each error at its request path in Field (for example
Sections[0].Children[1].Create.Fields.Title) with Metadata.LocalId and
Metadata.ItemId. A cell of an owned Table names its row when the server knows
it (Root.Fields.Lines[1].Item, the index of the row in the sent array, also in
Metadata.RowIndex); an error of a whole column, such as a unique constraint of
the table, names the column only (Root.Fields.Lines.Item). The record and
composite routes keep Field = Lines.Item and carry the same
Metadata.RowIndex (and Metadata.RowId when the row has an id); the installed error catalog lists the codes and reactions under
the EntityFormSubmissionRest surface; a code it does not list leaves the outcome
unknown (UnlistedAnswer), as on the record and composite surfaces. The field
checks it lists, such as moltaro.instances.validation.requiredFields with one
moltaro.instances.field.required per empty field or Table cell, or
moltaro.instances.field.invalid, are NotApplied: nothing was saved, so the
person corrects the named field and saves again. A cascade that would archive or delete another participant is refused
with moltaro.instances.composite.duplicateTarget. The OpenAPI operation ids are
entity-form-submissions-execute and entity-form-submissions-preview-hidden-policy. The same OperationKey with the same body
replays the stored result (also after the Form changed); another body under that
key is refused (moltaro.security.idempotency.keyReuse). Confirm an unknown
outcome with GET /api/workspace/entity-mutation-outcomes?Operation=5&OperationKey=…
(Composite handle; steps Create, Update, Archive, Delete, a Delete step has
no RowVersion).
Child requests before the parent is saved
Section titled “Child requests before the parent is saved”Child-scoped requests made while the parent is a Form draft may send
ParentContext { EntityDefinitionId, FormSurfaceId, ExpectedRevision, ItemId, ParentInstanceId, ParentFields }. The server evaluates the parent draft
(ParentInstanceId null for a new parent, else the stored parent with the draft
over it) and lets every one-hop read through the child’s parent Reference
(Current.<Parent>.<Field>) read it:
| Request | Where ParentContext goes | What reads the parent draft |
|---|---|---|
| Reference candidates, selection, Lookup table, tree and hierarchy requests | request member ParentContext | Eligibility rules |
Form evaluation POST …/instances/form-evaluation and …/instances/{id}/form-evaluation | request member ParentContext | Effect sources and conditions (Parent.Kind), calculations over their results, and ParentReference in the answer for the child’s Form conditions |
File-field upload POST /api/workspace/files/uploads of a new child (FieldTarget.CreateOperationKey) | request member ParentContext, next to FieldTarget | The child’s Create and Field:CreateWrite pre-authorization |
Markdown attachment scope POST /api/workspace/markdown/scope/issue of a new child (OperationKind = Create, provider core.entity) | ProviderContextJson = {"ParentContext": {…}} (sized by the scope’s upload header, below; member names are read as in a request body) | The child’s Create, Attachments:Create and Field:CreateWrite pre-authorization |
ParentFields may hold only fields shown as field items of the parent Form. The
server overlays only the fields sent: every other field of a stored parent is
read as stored, and of a new parent as empty. A child reads only a parent draft
its actor could make on the parent Form, as the parent Form’s own evaluation
requires:
- a new parent (
ParentInstanceIdnull): aCreateright on the parent definition that the preview does not deny, withField:CreateWriteon every sent field; - a stored parent with
ParentFields:View,UpdateandField:Writeon every sent field, so send only the fields the user changed; - a stored parent without
ParentFields(a children-only edit):View.
Any other parent answers like one the actor cannot read or that does not exist.
The server sets the child’s parent Reference: a child may send it only as null or
as the stored parent, and a Form evaluation never answers it in Patch. Its
ParentReference names the parent with the projected values the child’s Form
conditions read; while the parent is unsaved its State is still 0
(Resolved) and Id, Number, DisplayName and Subtitle are null. The
answers per request:
| Request | Another record as the child’s parent Reference | Parent not readable, missing or not draftable | Stale ExpectedRevision |
|---|---|---|---|
| Reference candidates, selection, Lookup table, tree and hierarchy | runtime state ContextInvalid | 404 moltaro.instances.notFound | 409 moltaro.ui.form.submission.revisionChanged |
| Form evaluation | 400 moltaro.ui.form.submission.fieldServerOwned at DraftFields.<key> | 404 moltaro.instances.notFound | 409 moltaro.ui.form.submission.revisionChanged |
| File-field upload | 400 moltaro.instances.field.invalid at FieldTarget | 404 moltaro.instances.notFound | 409 moltaro.ui.form.submission.revisionChanged |
| Markdown scope issue | 404 markdown.inline.unavailable | 404 markdown.inline.unavailable | 404 markdown.inline.unavailable |
The File upload and the Markdown scope of a new child keep the context, so it
must stay small. Send in their ParentFields exactly the keys of
RelatedChild.PreauthorizationParentFieldKeys, each with its whole value, and
leave out a key the parent draft does not hold (a stored parent’s unchanged
field is read as stored). The list names the parent field items whose values
these pre-authorizations read: the fields the child’s Create,
Attachments:Create and Field:CreateWrite rights read through the parent
Reference, and the fields the parent’s own Create and Field:CreateWrite
rights read for a new parent. When a right reads a parent field the parent Form
computes (a calculated field or the target of a mutation effect), it names every
parent field item; when no right reads the parent, it is empty. Never shorten
or drop a value to fit: the rights would then be decided on another draft. The
list is derived from the rights of every role, not only the actor’s, so it can
name a field the actor’s own rights do not read, and it names keys only, never
values or conditions.
Size limits: the File upload’s policy context (the target, its DraftFields and
the ParentContext, as the server keeps it) is at most 64 KiB. The Markdown
scope’s token travels in the X-Moltaro-Markdown-Scope header of every draft
upload, so the server issues no scope whose header line, X-Moltaro-Markdown-Scope: <token>
with its line end, would exceed 7,168 bytes. The nginx in front of the API
answers its own 400 to a header line over 8 KiB; the rest is a margin for other
proxies. The token protects and encodes the whole scope, so it is larger than the
context: about 2,000 Ukrainian letters, 4,000 Latin ones or 350 emoji (which the
token carries escaped) fit in one context. Only the server can measure the token,
so a client sends the context and shows the refusal. A larger context is refused
before any right is decided, so the answer depends only on the request and the
caller’s own account, and nothing is uploaded or issued:
| Request | Answer | Field | Metadata |
|---|---|---|---|
| File-field upload | 400 moltaro.ui.form.parentContext.tooLarge | ParentContext | Maximum 65536, Length (the policy context, in characters; non-ASCII letters count once) |
| Markdown scope issue | 400 moltaro.ui.form.parentContext.tooLarge | ProviderContextJson | Maximum 7168, Length (the bytes of the scope’s upload header line, name and line end included; when the context’s own UTF-8 bytes already exceed the line, the line the context alone would make, a lower bound) |
A proxy you put in front of Moltaro must accept request header lines of at least 8 KiB.
Show the refusal to the person, for example by asking them to save the parent
first or to shorten its long values. ParentContext is a preview only: it grants
nothing, the File policy and the Markdown scope keep it with the upload or
draft, and the submission decides every right again on the saved records.
Save object-context members with the Form
Section titled “Save object-context members with the Form”Editable source members use the same Form submission route. Read their runtime
catalog and Submission.Revision from the configured Form. For an existing root,
pass instanceId to GET /api/workspace/entity/{entity}/ui/form; the server
requires View on that exact record and shapes admission for that record. Omit
instanceId for a new root. Optional objectContextFactIds selects up to 100
bindings of the same Entity without changing the chosen Form Surface.
Root.ObjectContextMembers and a child Create/Update’s ObjectContextMembers
contain source inputs alongside ordinary Fields. Copy the binding and schema
versions from the runtime catalog. Existing source edits require the complete
Handle from the current member snapshot:
{ "BindingId": "<binding id>", "BindingVersion": "<binding row version>", "SchemaVersion": "<provider schema version>", "Handle": { "Resource": { "ModuleKey": "Boards", "ResourceKind": "BoardItem", "ContextResourceId": "<board id>", "ResourceObjectId": "<item id>" }, "Version": "<opaque item and Part version>" }, "Admit": true, "Members": [ { "MemberKey": "status", "Value": "<status id>" }, { "MemberKey": "due", "Value": { "DueDate": "2030-05-15", "DueTime": "09:30:00" } } ]}Treat the handle as opaque; do not reconstruct or silently refresh it on save.
Admit = true participates in the source operation. With no handle it requests
placement of a missing source, only when the catalog offers Creation. Use its
defaults and supported Create inputs.
An optional explicit placement may be declined; automatic placement cannot be
suppressed. Admit = false sends no members and no handle and declines only an
optional placement. A source already present uses Admit = true, its handle and
edit inputs. Boards
status is a status ID string. Due is null to clear it, or a typed object with
DueDate and DueTime; DateTime mode requires time and DateOnly mode rejects
time. Other members use their advertised field type and input binding.
For a source-only edit send root identity, current root RowVersion and empty
Fields. This guards the root without changing its ordinary fields. The server
checks source permissions, versions, field rules and prerequisites and commits
the root, children and sources atomically. A failed check writes none of them.
Workflow context members remain read-only.
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 or Card configuration and preserve all unrelated items.
- Write one item with
ItemType = 3and only eligible selectors. - Read the leaf configuration back and verify item id, inverse field, order, visibility, label/hint, selectors, priorities, keys, and roles.
- Export YAML v10 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 leaf 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 or the containing Card stays in place.
- For Card, verify Details, drawer, related-card, host-block, and Boards consumers plus a narrow viewport; the table must remain full-container-width.
For UI setup, see Configure Form and Card Related tables. For the user workflow, see Related tables in forms and cards.