Skip to content

Commit eab4bba

Browse files
feat(console): a greyed action says the reason its author declared (#26869)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBAQyBVaDQuUj1ZDR9GVydAG)_ Fixes #26619 — the console half. The spec half, `ActionSchema.disabledReason`, landed in #26786. Clause-②: no Metadata-protocol: none Skills-docs: #26790 ## 维护者速读 - **改了什么:** 置灰的动作按钮现在显示作者写的 `disabledReason`(悬停提示、读屏描述、菜单项第二行),没写或写了空白时仍显示「对此记录不可用」。覆盖记录页头(含 ⋯ 溢出菜单)、记录区块按钮栏、声明动作栏,以及 `action:button` / `action:icon` / `action:group` / `action:menu` 四个渲染器。原因文案走和动作其他文案相同的翻译路径:先取翻译包 `objects.对象._actions.动作.disabledReason`,再取作者的内联多语言映射,最后取作者原文。 - **为什么改:** 裁决 A(6442737825)的第二步。规格已在 #26786 落地,这个 PR 让控制台真正读它,hotclm 的送审 / 发起签署 / 生效能说清缺什么。 - **风险与回滚:** 不写这个键的动作显示不变。只有声明的 `disabled` 判断为真时才显示原因;动作执行中、宿主禁用的按钮仍不给原因。纯展示,不参与服务端准入。回滚就是 revert 本 PR。 - **先看:** `ui/packages/react/src/hooks/useActionTextLocalizer.ts`(解析原因文案),`ui/packages/components/src/renderers/action/disabled-reason.tsx`(四个渲染器的共用钩子),`packages/spec/liveness/action.json`(存活账本改为 live)。 - **两分钟验证:** `cd ui && pnpm exec vitest run packages/components/src/renderers/action/__tests__/action-disabled-reason-26619.test.tsx`,预期全部通过:写了原因显示原因,中文环境取内联映射的中文,不写时仍是默认句。 - **需要你做的:** 无,席位落地。 ## What changes **The readers.** While an action's DECLARED `disabled` predicate is TRUE, each surface shows its declared reason where it showed `actions.notAvailableForRecord`. That generic sentence stays the answer when the action declares none, or a blank one. The surfaces where nothing earns a reason are unchanged: a button greyed while its own action runs, the host's forwarded `disabled`, the host's Edit / Delete, and the legacy `enabled` leg. - `ui/packages/components/src/renderers/layout/containers.tsx#disabledReasonFor`: the record header, inline and in the ⋯ overflow. - `ui/packages/plugin-detail/src/renderers/record-quick-actions.tsx` (`QuickActionButton`): the record section bar. - `ui/packages/app-shell/src/views/DeclaredActionsBar.tsx` (`DeclaredActionButton`): the declared actions bar, button and override-menu item. The reason comes off the same localized copy as the label. - `ui/packages/components/src/renderers/action/disabled-reason.tsx#useDisabledReason`: the four `action:*` renderers. The hook takes the declared reason as a second argument, and `action-button.tsx`, `action-icon.tsx`, `action-group.tsx` (inline and dropdown) and `action-menu.tsx` pass it. **Translation.** The reason is an `I18nLabel` and is resolved like the action's other copy: - `useObjectLabel` (`@object-ui/i18n`) gains `actionDisabledReason(objectName, actionName, fallback?)`, the `actionSuffixes` pattern: `objects.<o>._actions.<a>.disabledReason`, or `globalActions.<a>.disabledReason` with no key object. It reads the same two addresses `translateAction` and `os i18n extract` use. - `useActionTextLocalizer` (`@object-ui/react`) collapses a declared reason's locale map to the active language and asks the bundle, but only when the action declares the key. That is the `confirmText` rule: a bundle cannot give a reason to an action whose author wrote none, so that action keeps the default. The record header, the section bar and the declared actions bar already run their actions through it. - The four `action:*` renderers read the action as authored (they do not localize `label` through the bundle either), so the inline locale map is the translation they honour. `useDisabledReason` collapses it with `pickLocalized`. **Types.** `UIActionSchema.disabledReason` (`@object-ui/types`), derived from the spec (`SpecAction['disabledReason']`), beside `disabled`. **Spec side.** - `packages/spec/liveness/action.json` `disabledReason`: `planned` → `live`, `evidenceScope: in-repo`, with the readers above as evidence. `authorWarn` and `authorHint` are dropped in the same edit, so a warned `live` row cannot red `check:liveness`. `liveness/state-counts/action.md` was regenerated. - `packages/lint/src/lint-liveness-properties.test.ts`: the #26619 block becomes a silence pin. The key is authored beside a property that still warns (`object.externalSharingModel`, `dead`) in the same call, so the silence is a reading. The block also asserts the shipped row is `live`, carries no warning, and cites `containers.tsx#disabledReasonFor`. - The two `.describe()` texts (`action.zod.ts`, `translation.zod.ts`) and `content/docs/protocol/objectui/actions.mdx` no longer say that nothing shows the key. Four `content/docs/references/**` pages were regenerated by `check:generated --fix`. **Changeset.** `.changeset/26619-console-disabled-reason.md`, every moved package at `patch`: `Metadata-protocol: none`, because nothing an author can write changes (AGENTS.md step 3). `@objectstack/lint` changes a test only. ## Tests - `ui/packages/react/src/hooks/__tests__/useActionTextLocalizer.disabledReason-26619.test.tsx` (6): authored string, bundle outranks authored, locale map collapsed (zh), the `globalActions` address, no key added when the action declares none even with a bundle entry, a nameless action's map collapsed. - `ui/packages/components/src/renderers/action/__tests__/action-disabled-reason-26619.test.tsx`: for the three button renderings, the declared string as description and tooltip, the zh map, the default (en and zh), a blank map value falling back to the default, and the predicate-false control. For the two menu renderings, the declared second line, the default, and the control. - `ui/packages/components/src/__tests__/page-header-disabledReason-26619.test.tsx`: inline, overflow, the zh bundle outranking the authored text, the default, and the control. - `ui/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.disabledReason-26619.test.tsx` and `ui/packages/app-shell/src/views/__tests__/DeclaredActionsBar.disabledReason-26619.test.tsx`: the declared string, the zh bundle, the default, and the control. - The `-11811` / `-11839` sibling suites, which pin the mechanics and the no-reason controls, pass unchanged. <details><summary>Test readings (local, on the pushed head)</summary> - Head `d96cc5bbb` (`f6a479a30` plus the i18n pin), branch restarted from `main` at `aeb9cf0dd`. - `pnpm --filter @objectstack/spec test`: Test Files 735 passed (735), Tests 22041 passed, 1 todo. - `pnpm --filter @objectstack/lint test`: Test Files 168 passed (168), Tests 7432 passed. - `pnpm --filter @objectstack/spec check:liveness`: OK. `check:generated --fix`: 2 of 14 stale on the first run (`check:docs`, `check:liveness`), regenerated and committed. `node scripts/check-doc-authoring.mjs`: exit 0. - `pnpm --filter @object-ui/{types,i18n,react,components,plugin-detail,app-shell} type-check`: all exit 0, after building `@object-ui/app-shell^...`. - `cd ui && pnpm exec vitest run packages/types/ packages/i18n/ packages/react/ packages/components/ packages/plugin-detail/ packages/app-shell/src/views/__tests__/`: Test Files 1442 passed, 1 failed, 2 skipped; Tests 21406 passed, 1 failed. The one failure was a pin that moves with the new resolver: `useObjectLabel-identity-5564.test.tsx` counts `useObjectLabel`'s members (27 → 28). It is updated in `d96cc5bbb` and passes (5/5). - `ui/packages/core` was not run locally. On `main`, its `actionKeys.pin.test.ts` reds on the new spec key (SPEC_ACTION_KEYS lacks `disabledReason`). The seat's hotfix #26859 fixes that, and this branch takes it by a `main` merge once it lands. This PR does not add the row. - eslint over the 17 changed ui files: 0 errors (warnings are pre-existing `no-explicit-any`). </details> ## Acceptance notes - **The `action:*` renderers honour the inline map, not the bundle.** They render `label` as authored too. Giving them a bundle lookup would also need a key object they do not carry, and that is a wider change than this card. A registered action that reaches them through a surface that localizes (the record header, the section bar, the declared actions bar) already arrives with the bundle's sentence. - **A blank reason falls back to the default** on every surface, including a locale map whose value for the active language is whitespace. The spec refuses a blank string but not a blank map value (#26786's acceptance note). ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; eslint over the 17 changed ui files: 0 errors. - [x] 4. One issue, one branch restarted from `main` after #26786 merged, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzfHuAwrecfjUDtkwcm --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzfHuAwrecfjUDtkwcm)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6090bb7 commit eab4bba

28 files changed

Lines changed: 829 additions & 60 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@object-ui/types': patch
4+
'@object-ui/i18n': patch
5+
'@object-ui/react': patch
6+
'@object-ui/components': patch
7+
'@object-ui/plugin-detail': patch
8+
'@object-ui/app-shell': patch
9+
---
10+
11+
feat(console): a greyed action says the reason its author declared
12+
13+
Clause-②: no
14+
Metadata-protocol: none
15+
16+
An action greyed out by its `disabled` predicate now shows its declared `disabledReason` as the button's hover title and accessible description, and as a menu item's second line. The console used to show "Not available for this record" for every such action. That sentence stays the default when the action declares no reason, or a blank one.
17+
18+
```ts
19+
defineAction({
20+
name: 'submit_for_approval',
21+
label: 'Submit for approval',
22+
objectName: 'clm_contract',
23+
disabled: "record.legal_review_status != 'approved'",
24+
disabledReason: 'An approved legal review is required before the contract enters approval.',
25+
});
26+
```
27+
28+
- **Where it shows.** The record header (inline and in the ⋯ overflow), the record section bar, the declared actions bar, and the four `action:*` renderers (`action:button`, `action:icon`, `action:group`, `action:menu`). On every one, only the declared `disabled` predicate evaluated true earns a reason. A button greyed while its own action runs, or by the host, still gives none.
29+
- **Translated like the other action copy.** `useActionTextLocalizer` (`@object-ui/react`) resolves the key when the action declares it, through the new `actionDisabledReason` resolver on `useObjectLabel` (`@object-ui/i18n`). The bundle's sentence at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an action with no `objectName`) comes first. After it comes the authored inline locale map, collapsed to the active language, then the authored string. A bundle entry never gives a reason to an action whose author wrote none. The four `action:*` renderers read the action as authored, so for them the inline locale map is the translation.
30+
- **Typed.** `UIActionSchema` (`@object-ui/types`) declares `disabledReason`, derived from the spec's `ActionSchema`.
31+
- **Liveness.** `disabledReason` is now `live` in `@objectstack/spec`'s ledger, with the console readers as evidence. The planned-property warning an author got for declaring it is gone. The key's description no longer says that nothing shows it.

‎content/docs/protocol/objectui/actions.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ Predicates are **bare CEL** — `record.status == 'paid'`, not `${record.status
336336

337337
#### Saying why: `disabledReason`
338338

339-
A greyed action says **why** with `disabledReason`, the sentence beside the `disabled` predicate. It is meant for the button's hover title and its accessible description (a menu item, under its label). The console does not show it yet: until its greyed-action readers ship, every greyed action shows the console's default reason, "Not available for this record", and `os validate` warns an author who declares the key.
339+
A greyed action says **why** with `disabledReason`, the sentence beside the `disabled` predicate. The console shows it while the predicate is true, as the button's hover title and its accessible description (a menu item shows it under its label). Without it the reason is the console's default, "Not available for this record".
340340

341341
```yaml
342342
name: submit_for_approval

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ const result = ApiMethod.parse(data);
499499
| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. |
500500
| **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| …>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions at parse time. |
501501
| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. |
502-
| **disabledReason** | `string \| Record<string, string>` | optional | Display-only reason for a greyed action, for its hover title and accessible description while `disabled` evaluates TRUE (declared ahead of its console reader: no renderer shows it yet) — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, `requiredPermissions`), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused. |
502+
| **disabledReason** | `string \| Record<string, string>` | optional | Display-only reason a greyed action shows (its hover title and accessible description, or a menu item's second line) while `disabled` evaluates TRUE — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, `requiredPermissions`), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused. |
503503
| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. |
504504
| **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record<string, object>; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. |
505505
| **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. |

‎content/docs/references/kernel/metadata-plugin.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ const result = MetadataBulkResultSchema.parse(data);
346346
| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| …>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. |
347347
| **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| …>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions at parse time. |
348348
| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. |
349-
| **disabledReason** | `string \| Record<string, string>` | optional | Display-only reason for a greyed action, for its hover title and accessible description while `disabled` evaluates TRUE (declared ahead of its console reader: no renderer shows it yet) — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, `requiredPermissions`), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused. |
349+
| **disabledReason** | `string \| Record<string, string>` | optional | Display-only reason a greyed action shows (its hover title and accessible description, or a menu item's second line) while `disabled` evaluates TRUE — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, `requiredPermissions`), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused. |
350350
| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. |
351351
| **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record<string, object>; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. |
352352
| **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. |

‎content/docs/references/system/translation.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Translation data for a single field
128128
| **description** | `string` | optional | Translated action description — the explanatory line under the title in the action's param dialog |
129129
| **confirmText** | `string` | optional | Translated confirmation prompt |
130130
| **successMessage** | `string` | optional | Translated success toast/message |
131-
| **disabledReason** | `string` | optional | Translated reason for a greyed action, for its hover title and accessible description while its `disabled` predicate is TRUE (no renderer shows it yet) |
131+
| **disabledReason** | `string` | optional | Translated reason a greyed action shows (hover title and accessible description) while its `disabled` predicate is TRUE |
132132
| **outcomeMessages** | `Record<string, string>` | optional | Translated success copy per handler outcome, keyed by the snake_case outcome name the action's `outcomeMessages` declares |
133133
| **params** | `Record<string, { label?: string; helpText?: string; placeholder?: string; options?: Record<string, string> }>` | optional | Action parameter translations keyed by parameter name |
134134
| **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; fields?: Record<string, string> }` | optional | Translations for the action result dialog |
@@ -228,7 +228,7 @@ Translation data for a single object
228228
| **description** | `string` | optional | Translated action description — the explanatory line under the title in the action's param dialog |
229229
| **confirmText** | `string` | optional | Translated confirmation prompt |
230230
| **successMessage** | `string` | optional | Translated success toast/message |
231-
| **disabledReason** | `string` | optional | Translated reason for a greyed action, for its hover title and accessible description while its `disabled` predicate is TRUE (no renderer shows it yet) |
231+
| **disabledReason** | `string` | optional | Translated reason a greyed action shows (hover title and accessible description) while its `disabled` predicate is TRUE |
232232
| **outcomeMessages** | `Record<string, string>` | optional | Translated success copy per handler outcome, keyed by the snake_case outcome name the action's `outcomeMessages` declares |
233233
| **params** | `Record<string, { label?: string; helpText?: string; placeholder?: string; options?: Record<string, string> }>` | optional | Action parameter translations keyed by parameter name |
234234
| **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; fields?: Record<string, string> }` | optional | Translations for the action result dialog |
@@ -386,7 +386,7 @@ Translation data for a single object
386386
| **description** | `string` | optional | Translated action description — the explanatory line under the title in the action's param dialog |
387387
| **confirmText** | `string` | optional | Translated confirmation prompt |
388388
| **successMessage** | `string` | optional | Translated success toast/message |
389-
| **disabledReason** | `string` | optional | Translated reason for a greyed action, for its hover title and accessible description while its `disabled` predicate is TRUE (no renderer shows it yet) |
389+
| **disabledReason** | `string` | optional | Translated reason a greyed action shows (hover title and accessible description) while its `disabled` predicate is TRUE |
390390
| **outcomeMessages** | `Record<string, string>` | optional | Translated success copy per handler outcome, keyed by the snake_case outcome name the action's `outcomeMessages` declares |
391391
| **params** | `Record<string, { label?: string; helpText?: string; placeholder?: string; options?: Record<string, string> }>` | optional | Action parameter translations keyed by parameter name |
392392
| **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; fields?: Record<string, string> }` | optional | Translations for the action result dialog |
@@ -606,7 +606,7 @@ Translation data for a single object
606606
| **description** | `string` | optional | Translated action description — the explanatory line under the title in the action's param dialog |
607607
| **confirmText** | `string` | optional | Translated confirmation prompt |
608608
| **successMessage** | `string` | optional | Translated success toast/message |
609-
| **disabledReason** | `string` | optional | Translated reason for a greyed action, for its hover title and accessible description while its `disabled` predicate is TRUE (no renderer shows it yet) |
609+
| **disabledReason** | `string` | optional | Translated reason a greyed action shows (hover title and accessible description) while its `disabled` predicate is TRUE |
610610
| **outcomeMessages** | `Record<string, string>` | optional | Translated success copy per handler outcome, keyed by the snake_case outcome name the action's `outcomeMessages` declares |
611611
| **params** | `Record<string, { label?: string; helpText?: string; placeholder?: string; options?: Record<string, string> }>` | optional | Action parameter translations keyed by parameter name |
612612
| **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; fields?: Record<string, string> }` | optional | Translations for the action result dialog |
@@ -795,7 +795,7 @@ Translation data for a single object
795795
| **description** | `string` | optional | Translated action description — the explanatory line under the title in the action's param dialog |
796796
| **confirmText** | `string` | optional | Translated confirmation prompt |
797797
| **successMessage** | `string` | optional | Translated success toast/message |
798-
| **disabledReason** | `string` | optional | Translated reason for a greyed action, for its hover title and accessible description while its `disabled` predicate is TRUE (no renderer shows it yet) |
798+
| **disabledReason** | `string` | optional | Translated reason a greyed action shows (hover title and accessible description) while its `disabled` predicate is TRUE |
799799
| **outcomeMessages** | `Record<string, string>` | optional | Translated success copy per handler outcome, keyed by the snake_case outcome name the action's `outcomeMessages` declares |
800800
| **params** | `Record<string, { label?: string; helpText?: string; placeholder?: string; options?: Record<string, string> }>` | optional | Action parameter translations keyed by parameter name |
801801
| **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; fields?: Record<string, string> }` | optional | Translations for the action result dialog |

‎content/docs/references/ui/action.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const result = ActionSchema.parse(data);
5656
| **requiresFeature** | `Enum<'twoFactor' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| 'oidcProvider' \| 'sso' \| 'ssoEnforced' \| 'deviceAuthorization' \| 'admin' \| 'phoneNumber' \| … +2 more>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. |
5757
| **requiresMembershipReach** | `Enum<'invite_member' \| 'cancel_invitation' \| 'update_member_role' \| 'transfer_ownership' \| 'remove_member' \| 'create_team' \| 'update_team' \| 'remove_team' \| … +4 more>` | optional | Organization endpoint (a MEMBERSHIP_REACH row) whose membership-grade gate this action follows; lowered into `visible` over current_user.positions at parse time. |
5858
| **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Disabled predicate — `true`/`false` literal, CEL string, or `{dialect, source}` envelope. The action is shown but refused when it evaluates TRUE. Omit = never disabled. |
59-
| **disabledReason** | `string \| Record<string, string>` | optional | Display-only reason for a greyed action, for its hover title and accessible description while `disabled` evaluates TRUE (declared ahead of its console reader: no renderer shows it yet) — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, `requiredPermissions`), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused. |
59+
| **disabledReason** | `string \| Record<string, string>` | optional | Display-only reason a greyed action shows (its hover title and accessible description, or a menu item's second line) while `disabled` evaluates TRUE — the default-language string, or an inline locale map; translated at `objects.OBJECT._actions.ACTION.disabledReason` (`globalActions.ACTION.disabledReason` for an object-less action). Not a gate: the server refuses a call on its own rules (hooks, `requiredPermissions`), and nothing reads this as a condition. Omit = the default reason, "Not available for this record". A blank reason is refused. |
6060
| **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. |
6161
| **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record<string, object>; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. |
6262
| **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. |

0 commit comments

Comments
 (0)