Found by booting a real ObjectStack application (objectstack-ai/duly) against published @objectstack/* 17.2.0 and driving the Console in Chromium. Filed here rather than in the app because nothing in the app can fix it.
The claim
An action declared in defineStack({ actions }) does not render in the Console at any location. Measured across six actions and four locations, with objectstack validate / build green and the actions registered under the keys executeAction dispatches on.
| Location declared |
Surface |
Renders |
list_toolbar — object-bound type: 'script' action |
object list toolbar |
❌ New · Import · Edit inline · Filter · Group · Sort only |
list_item |
grid row ⋯ menu |
❌ Edit · Delete only |
record_header |
record detail header |
❌ Edit + ⋯ only |
record_more |
record detail ⋯ overflow |
❌ Share · Delete only |
bulkActionDefs on the view (not an action) |
selection toolbar |
✅ renders and works end to end |
All four are members of ACTION_LOCATIONS in @objectstack/spec 17.2.0. The visible predicates match the records under test. Zero console errors, zero page errors, zero 4xx on every screen.
The last row is the control: a declarative bulk write on the view renders, executes, and persists. So the Console is not broken — it does not see actions metadata.
Likely root cause, measured separately
While investigating an unrelated startup warning in the same app, a developer measured that meta.loadMany('action') returns an empty array, and meta.load('action', <name>) is undefined for actions that are demonstrably live — the REST route resolves the very same actions through ql.registry.getItem('action', name) and dispatches them fine.
So there appear to be two stores: the runtime registry, which has the actions and executes them, and the metadata store the Console reads, which does not. That is consistent with every row of the table above.
(That measurement is on this repo's issue #14123, which reports the startup audit symptom of the same split. This report is the user-visible half: the buttons.)
Why this is worth prioritising
declared ≠ enforced is normally a silent-wrong-answer problem. Here it is a silent missing-feature problem, and the failure mode is unusually bad for a metadata platform:
- the author writes the action, the schema accepts it,
validate passes, build passes;
- the handler registers, and an integration test that dispatches through the engine 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. The team only found out by opening a browser.
Reproduce
npx create-objectstack@latest scratch && cd scratch
# add an object-bound action:
# { name: 'x_ping', objectName: '<obj>', type: 'script', target: 'x_ping',
# locations: ['list_toolbar', 'list_item', 'record_header', 'record_more'],
# label: 'Ping' }
# register a handler for it in defineStack({ onEnable })
pnpm validate && pnpm build && pnpm dev
# open /_console/, navigate to the object, look for "Ping" in: the list toolbar,
# a row's ⋯ menu, the record header, the record's ⋯ overflow. It is in none of them.
Suggested direction
Whatever the resolution, the gap between "an action is dispatchable" and "an action is reachable" should not be silent. Either the Console reads the same registry the dispatcher does, or validate refuses an action whose declared locations no surface will render — the second is cheaper and is the ADR-0049 shape.
Related from the same app: #14092 (no declarative row-level field write, which is why these were hand-written script actions), #14123 (the startup-audit half of the registry split).
Unassigned and untriaged, per the single-producer rule for domain:*.
Found by booting a real ObjectStack application (
objectstack-ai/duly) against published@objectstack/*17.2.0 and driving the Console in Chromium. Filed here rather than in the app because nothing in the app can fix it.The claim
An action declared in
defineStack({ actions })does not render in the Console at any location. Measured across six actions and four locations, withobjectstack validate/buildgreen and the actions registered under the keysexecuteActiondispatches on.list_toolbar— object-boundtype: 'script'actionlist_itemrecord_headerrecord_morebulkActionDefson the view (not anaction)All four are members of
ACTION_LOCATIONSin@objectstack/spec17.2.0. Thevisiblepredicates match the records under test. Zero console errors, zero page errors, zero 4xx on every screen.The last row is the control: a declarative bulk write on the view renders, executes, and persists. So the Console is not broken — it does not see
actionsmetadata.Likely root cause, measured separately
While investigating an unrelated startup warning in the same app, a developer measured that
meta.loadMany('action')returns an empty array, andmeta.load('action', <name>)isundefinedfor actions that are demonstrably live — the REST route resolves the very same actions throughql.registry.getItem('action', name)and dispatches them fine.So there appear to be two stores: the runtime registry, which has the actions and executes them, and the metadata store the Console reads, which does not. That is consistent with every row of the table above.
(That measurement is on this repo's issue #14123, which reports the startup audit symptom of the same split. This report is the user-visible half: the buttons.)
Why this is worth prioritising
declared ≠ enforcedis normally a silent-wrong-answer problem. Here it is a silent missing-feature problem, and the failure mode is unusually bad for a metadata platform:validatepasses,buildpasses;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. The team only found out by opening a browser.
Reproduce
Suggested direction
Whatever the resolution, the gap between "an action is dispatchable" and "an action is reachable" should not be silent. Either the Console reads the same registry the dispatcher does, or
validaterefuses an action whose declared locations no surface will render — the second is cheaper and is the ADR-0049 shape.Related from the same app: #14092 (no declarative row-level field write, which is why these were hand-written script actions), #14123 (the startup-audit half of the registry split).
Unassigned and untriaged, per the single-producer rule for
domain:*.