Moved here from objectstack-ai/objectstack#14145 after measuring: the platform side is correct at every layer, and the defect is in the Console.
Measured against a real ObjectStack application (objectstack-ai/duly) on published @objectstack/* 17.2.0, admin session, seeded data.
The claim
An object-bound action does not render at any of list_toolbar, list_item, record_header or record_more, although the Console has already fetched its complete declaration with the correct locations.
What the Console actually receives
GET /api/v1/meta/object/duly_catalog_item → item.actions:
[{ "name": "duly_catalog_apply_to_people",
"objectName": "duly_catalog_item",
"type": "script",
"label": "Apply to people",
"locations": ["list_toolbar"] }]
On /_console/apps/<app>/duly_catalog_item/view/list — the object's real list view, 20 rows, full grid chrome (New · Import · Filter · Group · Sort):
"Apply to people" buttons: 0 page errors: 0 4xx: 0
Same for three actions on duly_task at list_item, record_header and record_more.
What I ruled out, so the next person does not repeat it
| hypothesis |
measurement |
verdict |
| metadata store does not have actions |
GET /api/v1/meta/action → 6, all with label, type, objectName, locations |
❌ ruled out |
| the object payload omits them |
meta/object/<name> → item.actions present, correct locations |
❌ ruled out |
| the shipped console predates the feature |
all 40 served chunks grepped: list_toolbar 11, record_header 22, record_more 12, list_item 10 |
❌ ruled out |
the visible predicate hides them |
duly_catalog_apply_to_people declares no visible at all and still renders nothing |
❌ ruled out |
| a JS error swallows the render |
zero page errors, zero 4xx on every screen |
❌ ruled out |
The predicate row is the sharpest: an action with no predicate, at a valid location, present in the payload the page already holds, is not drawn.
Control that still works: bulkActionDefs declared on the view renders and executes end to end, and its per-record visible filtering is correct — the confirm dialog even reports "3 selected record(s) are not eligible for this action and will be skipped". So the Console renders view-declared actions and not object-declared ones.
Where to start
packages/app-shell/src/views/ObjectView.tsx does read them — line ~2314 gates the toolbar on objectDef.actions?.some(a => a.locations?.includes('list_toolbar')), and line ~1968 derives rowActionDefs from objectDef.actions. So the wiring exists in source; what I could not determine from outside is whether objectDef as ObjectView receives it still carries actions, or whether it is normalized away before it gets there.
That is the same shape as objectui#7199 (a list view's description is served but never copied onto the ListView schema by the same relay), which is why I would look at the relay first.
Why this is worth prioritising
declared ≠ enforced is normally a silent-wrong-answer problem; here it is a silent missing-feature problem, and every signal an author has says shipped: the schema accepts the action, validate passes, build passes, the handler registers, the REST route dispatches it, and an engine-level integration test that dispatches it passes. The button does not exist and nothing anywhere says so.
In the app that found this, the affected actions are one-click task completion — the interaction the product is designed around. It shipped with 35 passing tests and no button, and was only found by opening a browser.
Moved here from objectstack-ai/objectstack#14145 after measuring: the platform side is correct at every layer, and the defect is in the Console.
Measured against a real ObjectStack application (
objectstack-ai/duly) on published@objectstack/*17.2.0, admin session, seeded data.The claim
An object-bound action does not render at any of
list_toolbar,list_item,record_headerorrecord_more, although the Console has already fetched its complete declaration with the correctlocations.What the Console actually receives
GET /api/v1/meta/object/duly_catalog_item→item.actions:[{ "name": "duly_catalog_apply_to_people", "objectName": "duly_catalog_item", "type": "script", "label": "Apply to people", "locations": ["list_toolbar"] }]On
/_console/apps/<app>/duly_catalog_item/view/list— the object's real list view, 20 rows, full grid chrome (New · Import · Filter · Group · Sort):Same for three actions on
duly_taskatlist_item,record_headerandrecord_more.What I ruled out, so the next person does not repeat it
GET /api/v1/meta/action→ 6, all withlabel,type,objectName,locationsmeta/object/<name>→item.actionspresent, correctlocationslist_toolbar11,record_header22,record_more12,list_item10visiblepredicate hides themduly_catalog_apply_to_peopledeclares novisibleat all and still renders nothingThe predicate row is the sharpest: an action with no predicate, at a valid location, present in the payload the page already holds, is not drawn.
Control that still works:
bulkActionDefsdeclared on the view renders and executes end to end, and its per-recordvisiblefiltering is correct — the confirm dialog even reports "3 selected record(s) are not eligible for this action and will be skipped". So the Console renders view-declared actions and not object-declared ones.Where to start
packages/app-shell/src/views/ObjectView.tsxdoes read them — line ~2314 gates the toolbar onobjectDef.actions?.some(a => a.locations?.includes('list_toolbar')), and line ~1968 derivesrowActionDefsfromobjectDef.actions. So the wiring exists in source; what I could not determine from outside is whetherobjectDefas ObjectView receives it still carriesactions, or whether it is normalized away before it gets there.That is the same shape as objectui#7199 (a list view's
descriptionis served but never copied onto the ListView schema by the same relay), which is why I would look at the relay first.Why this is worth prioritising
declared ≠ enforcedis normally a silent-wrong-answer problem; here it is a silent missing-feature problem, and every signal an author has says shipped: the schema accepts the action,validatepasses,buildpasses, the handler registers, the REST route dispatches it, and an engine-level integration test that dispatches it passes. The button does not exist and nothing anywhere says so.In the app that found this, the affected actions are one-click task completion — the interaction the product is designed around. It shipped with 35 passing tests and no button, and was only found by opening a browser.