Backend Metadata to Frontend Table Actions
This page explains one of the most practical fullstack contract chains in Cabloy Basic:
backend field and row metadata can drive visible frontend table actions, while frontend action resources and generated contract consumers stay aligned with that backend truth.
This page sits between the pure direction guides:
It is not only a forward-chain page and not only a reverse-chain page.
Instead, it explains one concrete business thread where both directions usually cooperate.
Use this page together with:
- Contract Loop Playbook
- Backend OpenAPI to Frontend SDK
- Frontend Metadata Back to Backend
- Table Guide
- TableCell Authoring Cookbook
- Table + Resource CRUD Cookbook
- Tutorial 3: Frontend Metadata Sharing
- Tutorial 5: Backend Contract Sharing
- Backend Metadata to Frontend Table Actions Verify Playbook
- Backend Metadata to Frontend Table Actions Debug Checklist
- Backend Metadata to Frontend Table Actions Source Reading Map
Why this page exists
Several Cabloy docs already explain parts of this story:
- backend field contracts
- OpenAPI generation
- frontend metadata sharing
- custom table-cell authoring
- resource-driven CRUD pages
What contributors and AI workflows often still want is one continuous answer to this narrower question:
how does one row action or one table-cell decision travel from backend metadata to a visible frontend list page?
This page answers that question by treating table actions as one complete contract chain rather than as disconnected snippets.
The shortest correct mental model
If you only remember one idea, remember this one:
in Cabloy Basic, a visible frontend table action usually belongs to a larger contract chain: backend metadata chooses the action resource identity, frontend runtime resolves that resource, and generated API/model layers keep the action semantics aligned with the backend contract.
That means the visible button or link in a table row is often only the last step of the chain, not the first.
The main chain in one view
A practical row-action chain often looks like this:
- backend resource entity or row DTO defines field or row-action metadata
- that metadata uses
ZovaRender.cell(...),ZovaRender.tableActionRow(...),ZovaRender.tableActionBulk(...), orZovaRender.block(...) - frontend resource/page runtime consumes the generated schema or DTO-backed block metadata
basic-page:blockPageandbasic-page:blockTablefeed row schema and data intoZTable- the
a-tableruntime resolves the referencedtableCellresource - the
tableCellbean renders the visible action and may delegate to commands or model methods - if the action uses custom backend endpoints, frontend generated API and thin model facades keep the action semantics aligned with backend truth
That is the chain this page makes explicit.
Two common categories of table action work
Before diving into files, separate two cases.
Case A: metadata-only or built-in action path
Use this mental model when:
- built-in actions are enough
- row actions such as view/update/delete/create already exist
- the backend mainly needs to point to existing frontend resources
Representative examples include:
basic-table:actionViewbasic-table:actionUpdatebasic-table:actionDeletebasic-table:actionCreatebasic-table:actionOperationsRow
In this case, the main work is usually on the reverse-sharing side:
- backend metadata chooses frontend resource identities
- frontend runtime consumes them
Case B: custom action path with backend contract changes
Use this mental model when:
- the row action corresponds to a new backend endpoint such as
summary/:idordeleteForce/:id - frontend should consume a newly generated API surface
- the same resource-owner model should stay the semantic owner
In this case, both directions cooperate:
- forward chain: backend controller/DTO changes generate frontend API consumers
- reverse chain: backend metadata points to frontend action resources that expose those new actions in the list page
This is the most practical reason to keep the forward and reverse chains conceptually separate but operationally connected.
Step 1: Backend metadata chooses the visible action resource identity
The first source of truth for table action visibility often lives in backend field or row DTO metadata.
Representative entity-level field example:
@Api.field(
v.title($locale('Name')),
v.min(3, $locale('ZodErrorStringMin')),
v.required(),
ZovaRender.order(1),
ZovaRender.cell('basic-table:actionView'),
)
name: string;2
3
4
5
6
7
8
Representative row-action-column example:
@Api.field(
v.title($locale('Operations')),
ZovaRender.order(1, 'max'),
ZovaRender.cell('basic-table:actionOperationsRow', {
actions: [
ZovaRender.tableActionRow('basic-table:actionUpdate'),
ZovaRender.tableActionRow('basic-table:actionDelete'),
],
}),
)
_operationsRow?: unknown;2
3
4
5
6
7
8
9
10
11
This is one of the most important fullstack facts about list pages.
The backend contract is not trying to render the button itself. It is choosing the frontend resource identity that should render the button.
A practical reading takeaway is:
- the backend owns the action contract surface
- the frontend owns the action implementation
- metadata is the bridge
Step 2: Block metadata composes the list page around those actions
Table actions do not live in isolation. They appear inside a resource-page block composition.
Representative shape:
ZovaRender.block('basic-page:blockPage', {
blocks: [
ZovaRender.block('basic-page:blockFilter'),
ZovaRender.block('basic-page:blockToolbarBulk', {
actions: [ZovaRender.tableActionBulk('basic-table:actionCreate')],
}),
ZovaRender.block('basic-page:blockTable'),
ZovaRender.block('basic-page:blockPager'),
],
});2
3
4
5
6
7
8
9
10
That means the row-action chain usually sits inside a larger page contract chain:
- block composition decides the page shape
- row or field metadata decides the cell/action resources
- frontend blocks and table runtime consume both surfaces together
Step 3: ModelResource becomes the stable frontend resource owner
On the frontend side, the standard list runtime begins with the resource-owner model:
rest-resource.model.resource
Its responsibilities include:
- resource bootstrap
- permissions
schemaFilterschemaRow- list query state
- item query state
- mutation ownership
This matters because the page runtime is not expected to rediscover resource contracts ad hoc.
A practical reading takeaway is:
- pages consume resource semantics
- the model owns query and mutation semantics
When a custom row action still belongs to the same business resource, keep that action inside the existing resource-owner story instead of inventing a competing owner.
Step 4: basic-page feeds the row schema into ZTable
On a standard CRUD list page:
basic-page:blockPageowns the resource statebasic-page:blockTablepassesdata,schemaRow, andtableScopeintoZTable
That means the backend metadata chain becomes visible in the table only after this resource-page handoff:
- backend row schema / row DTO metadata exists
ModelResourceexposes the row schemablockPageexposes that schema to the page runtimeblockTablepasses it intoZTablea-tableresolves the cell render resources from table-scene schema metadata
A practical rule is:
- if the action metadata looks right but the page still does not show it, inspect the page-block handoff before changing the cell bean itself
Step 5: a-table resolves the referenced tableCell resources
Inside the table runtime, ControllerTable resolves the render metadata into a real render provider.
Important behaviors include:
- no render -> text fallback
- onion-like render string -> resolve through the
tableCellscene tableCellbean options merge with column propscheckVisible(...)can filter a render before the column/cell is shown
This is why ZovaRender.cell('basic-table:actionView') or ZovaRender.cell('basic-table:actionOperationsRow', ...) is enough on the backend side.
The frontend runtime already knows how to turn that contract identity into a real action render.
Step 6: tableCell beans become the visible action implementations
Representative built-in row-action implementations include:
basic-table:actionViewbasic-table:actionUpdatebasic-table:actionDeletebasic-table:actionOperationsRow
Their responsibilities usually stay small and focused.
actionView
Typical responsibility:
- render a visible link
- call
$performCommand('basic-commands:view', ...)
actionUpdate
Typical responsibility:
- render an edit button
- call
$performCommand('basic-commands:edit', ...)
actionDelete
Typical responsibility:
- render a delete button
- confirm the action
- call
$performCommand('basic-commands:delete', ...)
actionOperationsRow
Typical responsibility:
- inspect the
actionslist - filter actions by permission
- preload nested renders
- render each child action through
$$table.cellRender(...)
A practical reading takeaway is:
- single-action cells adapt render interaction to commands
- operations-row cells orchestrate several action resources together
For deeper cell-authoring detail, continue with TableCell Authoring Cookbook.
Step 7: Bulk actions follow the same contract idea at page level
The same mental model also appears in bulk or page-level actions.
Representative example:
actions: [ZovaRender.tableActionBulk('basic-table:actionCreate')];And the corresponding frontend implementation can render a button that performs:
basic-commands:create
This is the same contract idea at a different UI level:
- backend or shared page metadata chooses the action resource identity
- frontend runtime resolves that identity to a visible action implementation
Step 8: Where the forward chain enters for custom actions
So far, everything could still be handled by built-in commands and built-in resources.
The forward chain becomes important when the action itself depends on a new backend API contract.
Representative examples include actions such as:
summary/:iddeleteForce/:id
In that case, the practical chain becomes:
- backend controller exposes the custom endpoint
- backend DTOs define request/response contracts
- frontend OpenAPI generation produces typed API consumers
- the frontend module model wraps those generated consumers thinly
- a custom
tableCell.actionSummary.tsxortableCell.actionDeleteForce.tsxtriggers the corresponding semantic action path - backend row metadata points the visible row action at that frontend resource identity
This is why Tutorial 5 is a forward-chain tutorial even though the visible result is a row action in a table.
The action semantics are forward-generated; the visible table exposure is reverse-shared.
Step 9: Why the resource-owner model should stay the semantic owner
When a custom action still belongs to the same business resource, do not create a competing cache owner only because the action is custom.
Instead:
- generate the new frontend API contract from backend truth
- wrap it in the existing module model as a thin semantic facade
- keep invalidation and resource ownership coherent
This matters because row actions often affect:
- current row state
- list query invalidation
- related item views
- page refresh expectations
A practical rule is:
- if the action still belongs to the same resource, prefer extending the existing resource owner over inventing a second one
Step 10: A practical end-to-end example matrix
Here is the most useful way to think about common action types.
Built-in view action
Chain:
- backend field metadata uses
ZovaRender.cell('basic-table:actionView') - row schema reaches
ZTable a-tableresolvesbasic-table:actionView- the frontend action cell performs
basic-commands:view
This is mostly a reverse-sharing path.
Built-in operations row
Chain:
- backend row DTO metadata uses
ZovaRender.cell('basic-table:actionOperationsRow', { actions: [...] }) - nested row-action metadata uses
ZovaRender.tableActionRow(...) actionOperationsRowfilters and renders child actions- each child action delegates to its own command-oriented cell resource
This is also mainly a reverse-sharing path.
Custom summary action
Chain:
- backend controller and DTOs define
summary/:id - frontend OpenAPI generation creates the API consumer
- frontend model wraps the consumer thinly
- custom
tableCell.actionSummary.tsxexposes the visible row action - backend row metadata includes that action in the operations row
This uses both forward and reverse directions.
Step 11: How to classify the work before editing anything
Use this quick decision map.
Mostly reverse-chain work
Use this path when:
- you are only changing which built-in or existing frontend render resource a field or row should use
- no new backend endpoint is needed
- the visible change is mostly metadata-driven
Typical examples:
- switch one field to
basic-table:actionView - add an operations row using existing update/delete actions
- add a custom table-cell renderer that backend metadata points to
Mostly forward-chain work
Use this path when:
- the action semantics need a new backend endpoint or changed response contract
- frontend typed consumers must regenerate
- the row action is only the last visible step of a larger API-contract change
Typical examples:
- add summary, archive, approve, or force-delete actions with new backend contracts
Consumer drift
Suspect this when:
- generated artifacts already contain the expected action contracts or resource keys
- but the visible frontend behavior still looks stale
Local dependency drift
Suspect this when:
- generated
.zova-restoutput or SDK output looks correct - but backend or frontend local consumers still do not see the refreshed shared types or resource identities
Common mistakes to avoid
Mistake 1: Treating the visible button as the start of the design
Usually the visible button is the end of the design. Start from the contract and metadata chain first.
Mistake 2: Adding a custom backend endpoint but manually duplicating its frontend contract
Prefer the forward-generation path before hand-writing request code.
Mistake 3: Creating a competing frontend state owner for an action that still belongs to the same resource
Prefer reusing the existing resource-owner model.
Mistake 4: Patching page-local table code when metadata already expresses the action correctly
If the contract is metadata-driven, keep it metadata-driven.
Mistake 5: Mixing up built-in action resources and custom action semantics
Built-in action resources often only adapt UI to commands. Custom action semantics may still need generated API and model work behind them.
A practical authoring order
If you want the shortest path to a correct table-action implementation, use this order:
- decide whether the work is mostly reverse-chain or forward-chain
- if forward-chain, change backend controller/DTO truth first
- regenerate frontend API consumers when backend contract changes
- keep frontend model follow-up thin and semantic
- point backend row metadata to the intended built-in or custom table-action resources
- verify the resource-page block chain still feeds the right schema into
ZTable - verify the visible row action in Admin
Verification checklist
When documenting or implementing this chain, verify in this order:
confirm the backend metadata anchors actually point to the intended
ZovaRender.*(...)resourcesconfirm the page block composition still includes the intended list blocks
confirm the current frontend
tableCellresources exist and match the named identitiesif custom backend actions were added, regenerate the frontend contract surface first
make sure the local dev workflow is running:
bashnpm run dev1open
http://localhost:7102/admin/verify the visible list page behavior:
- bulk create action if relevant
- row operations visibility
- row action execution
- list invalidation or refresh after mutations
if reverse-chain frontend resources changed, run the representative Basic handoff flow when needed:
bashnpm run zova :tools:metadata <module-name> npm run build:zova:admin npm run deps:vona1
2
3if docs changed, build the docs site:
bashnpm run docs:build1
Final takeaway
A frontend table action in Cabloy Basic is often not just a frontend button.
It is the visible result of a contract chain that may include:
- backend field or row metadata
- page block composition metadata
- generated frontend contract consumers
- resource-owner model semantics
tableCellbean-scene resourcesbasic-pagelist runtime
Once you read the system through that chain, row actions stop looking like scattered UI details and start looking like one coherent fullstack workflow.