Skip to content

Commit 7e3822c

Browse files
feat(spec)!: an action's description, param helpText and outcomeMessages are {{ }} template slots; ${…} refused at publish (#28463)
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D <!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoBCpS3vE7HS7cD6zZzsAPjZ)_ Fixes #27948 Clause-②: yes (narrowing) Metadata-protocol: narrowed Skills-docs: follow-up #28464 (skills/objectstack-ui/rules/actions.md line 23 lists the `{{ }}` copy holes for `confirmText` / `successMessage` / `errorMessage` only; add `description`, param `helpText` and `outcomeMessages`) ## 维护者速读 - 改了什么:动作的 `description`(参数对话框说明)、每个参数的 `helpText`、每条 `outcomeMessages` 现在和 `confirmText` / `successMessage` 一样是 `{{ }}` 模板槽:裸字符串解析成 `{ dialect: 'template', source }`,`objectstack validate` / `compile` / 发布闸(422)拒绝其中的 `${…}`,并给出 `{{ }}` 写法。平台自带文案(邀请、重发邀请、改角色)和四种语言翻译已改成 `{{ }}`。 - 为什么改:裁决 A(「同意」)定五个文案键统一用 `{{ }}`;第 1 步(#28436)让控制台先能读 `{{ }}`,这是第 2 步:规范收口 + 发布拒绝 + 迁移。 - 风险与回滚:收窄(破坏性):写 `${…}` 的这三个键在保存/发布时被拒;已存的仍能加载,控制台对没有 `{{ }}` 的句子继续按 `${…}` 填,直到下次保存。解析后的值变成信封对象,所以第一个提交先让所有读取方(core、react、runtime、cli、Studio 预览)接受信封。回滚即 revert 本 PR。 - 先看:packages/spec/src/ui/action.zod.ts(三个槽的类型)、packages/lint/src/validate-action-copy-templates.ts(发布闸)、ui/packages/core/src/actions/ActionRunner.ts(`dialogCopyOf`)、迁移条目 packages/spec/src/migrations/entries/semantic/18.action-dialog-outcome-copy-template-holes.ts。 - 两分钟验证:`pnpm --filter @objectstack/lint exec vitest run src/validate-action-copy-templates.test.ts`,预期 23/23 通过;`cd ui && pnpm exec vitest run packages/core/src/actions/__tests__/ActionRunner.copyEnvelope-27948.test.ts`,预期 4/4。 - 需要你做的:无,契约复审后由席位落地;HotCRM 的同类文案另开为 objectstack-ai/hotcrm#2384。 ## What changes Step 2 of #27948, ruling 6545229148, letter A. Four commits. **1. Readers first** (`fcd2e654`). Typing the three keys makes a parsed action carry `{ dialect: 'template', source }` where it carried a string, so every reader that took a string now takes the envelope (claim 6546254270, amendments 6546313079 and 6546830184): - `ui/packages/core/src/actions/ActionRunner.ts`: `dialogCopyOf` hands the param-collection handler the action and params with `description` / each `helpText` reduced to their text (`actionCopyText`, new in `actionCopyTemplate.ts`); a locale map is handed on for the host to localize. `composeSuccessMessage` reads an outcome envelope's `source`. - `ui/packages/react/src/hooks/useActionTextLocalizer.ts`: an `outcomeMessages` envelope keeps its shape (a bundle entry replaces its `source`) instead of collapsing to the word "template". - `packages/runtime/src/action-execution.ts` `summarizeActionParams`: the MCP param description reads `helpText`'s text, never "[object Object]". - `packages/cli/src/utils/i18n-extract.ts`: `description`, param `helpText` and `outcomeMessages` go through `actionCopy` (`templateSlotSource`), so the envelope seeds exactly as the bare string. - `ui/packages/app-shell/src/views/metadata-admin/previews/ActionPreview.tsx`: `localize` reads the envelope's `source` and `helpText` goes through it (it was drawn raw, an object as a React child). **2. Spec and the publish refusal** (`45d5e284`). - `packages/spec/src/ui/action.zod.ts`: `description` and each `outcomeMessages` value are `actionCopy(...)` slots with the inline locale map; `ActionParamSchema.helpText` is `actionParamHelpText()`, the same slot without a map (it was a plain string). Describes teach `{{ }}`. - `packages/spec/src/ui/action-copy-template.ts`: `ActionCopyKey` widened to the three keys, each with its own refusal wording and example; `ActionRowCopyKey` keeps the page rows at their three; three describe constants; module notes on the roots and the `${…}` transition. - `packages/lint/src/validate-action-copy-templates.ts`: a registered action is judged on `description`, each `outcomeMessages.OUTCOME` and each `params[N].helpText`; an inline `properties.action` on its params' `helpText`. Roots: `actionConfirmTemplateRoots(action)` for the dialog copy, `ACTION_SUCCESS_TEMPLATE_ROOTS` for an outcome. Every finding is an error, so the publish gate answers 422. - `packages/spec/src/system/i18n-resolver.ts`: `ActionLike` types the envelope on the three keys. - Expression-conformance ledger: rows `template-action-dialog-copy` and `template-action-outcome-message`. `liveness/action.json` outcomeMessages evidence names `{{ }}`. Reference docs regenerated. - Eight platform-objects identity tests read the parsed copy through `templateSlotSource` (`copyAsWritten`), as their `confirmText` reads already did. **3. Migration and producers** (`a8f9f1c7`). - Semantic entry `action-dialog-outcome-copy-template-holes`. No conversion: there is no copy-hole conversion family in `os migrate meta`, and a `${…}` token held an expression (`${record.user_id?.name ?? 'this member'}`), which has no mechanical `{{ }}` spelling. - Producers: `invite-outcome-messages.ts` (`sent` / `link_only`, invite and resend), `sys_invitation.resend_invitation` and `sys_member.update_member_role` descriptions (`{{ record.user_id.name | default:'this member' }}`), both `permission_set_granted` outcomes, the four `objects.generated` bundles (en regenerates identically; `check-i18n-bundles` in sync), the action form's `outcomeMessages` help (`action.form.ts` + four `metadata-forms.generated` bundles), and `content/docs/ui/actions.mdx`. - Changesets: `27948-action-copy-template-slots.md` (`@objectstack/spec` and `@objectstack/lint` minor; platform-objects, runtime, cli patch; ADR-0087 `registered`, FROM → TO list) and `27948-action-copy-envelope-readers.md` (`@object-ui/core`, `@object-ui/react`, `@object-ui/fields` patch). **4. Fields reader** (`408466db`). `ui/packages/fields/src/resolveActionParams.ts` reads `helpText` through `actionCopySource`; pin `ui/packages/app-shell/src/utils/resolveActionParams.helpTextEnvelope-27948.test.ts` (3 tests; reverse 2 failed, the type pin passed). ## Tests - New pins, each reverse-checked (source restored from the merge base, test kept, then restored): - `ActionRunner.copyEnvelope-27948.test.ts` (core): 4; reverse 2 failed, 2 passed (the passing two are controls). - `useActionTextLocalizer.outcomeEnvelope-27948.test.tsx` (react): 3; reverse 2 failed, 1 control passed. - `action-param-help-envelope-27948.test.ts` (runtime): 2; reverse 2 failed. - `i18n-extract-action-copy-envelope-27948.test.ts` (cli): 2; reverse 1 failed, 1 control passed. - `ActionPreview.copyEnvelope-27948.test.tsx` (app-shell): 2; reverse 1 failed, 1 control passed. - `validate-action-copy-templates.test.ts` (lint): 6 new; reverse all 5 refusal pins failed, the accept pin passed. - `action-copy-template.test.ts` (spec): 7 new (parse arms, locale map per slot, refusal names, describes teach `{{ }}`). - Updated: `action-description.test.ts` and `action-outcome-messages.test.ts` expect the envelope from a parsed bare string. ## Acceptance notes - **Readers, all five groups** (seat ruling A, amendments 6546313079, 6546830184 and 6547728288): `@object-ui/react` `useActionTextLocalizer`; runtime `summarizeActionParams`; cli `i18n-extract`; app-shell `ActionPreview.tsx`; `@object-ui/fields` `resolveActionParams` (commit 4, `408466db`, named by CI "Type Check UI": `RawActionParam.helpText` is `unknown` and read through `actionCopySource`; a field-backed param whose envelope has no text falls back to the field's help). Plus core's own `ActionRunner` (`dialogCopyOf`, `composeSuccessMessage`). App-shell's `useConsoleActionRuntime` / `RecordDetailView` / `ActionParamDialog` read `description` / `helpText` from what the runner hands the handler, so they get strings and do not move. - **Left as is: `action-icon.tsx` tooltip** (`ui/packages/components/src/renderers/action/action-icon.tsx` lines 319 and 332, `schema.label || schema.description`). It would draw an envelope only with an empty label, and no parsed slot reaches it that way: `useActionTextLocalizer` always fills `label` (falling back to the name), a page row's `description` is not a slot, and `UIActionSchema` still types `description` as a string. Seat ruling: leave it. - **Reader sweep** at `408466db` over non-test sources in `ui/packages/*/src`, `packages/*/src`, plugins, services, mcp and cli: no other reader of the three keys takes the parsed value as a string. The renderers (`action:button` / `icon` / `group` / `menu`, `element:button`) forward `description` and `outcomeMessages` into `execute()`, and the runner reduces them. - **HotCRM producers** (carried by objectstack-ai/hotcrm#2384): `src/service/actions/knowledge_article.actions.ts` lines 121-123 and 144-146 (six `outcomeMessages` entries with `${result.helpful}` / `${result.notHelpful}`), and `src/sales/translations/{en,es-ES,ja-JP,zh-CN}/objects.service.ts`. Roots are `result`, so each converts to `{{ result.helpful }}` / `{{ result.notHelpful }}`. - **`default` vs `??`.** `{{ record.user_id.name | default:'this member' }}` also fires on an empty name, where `??` fired only on null/undefined. For a member name that is the intended reading. - **Census beyond the stop report.** Three more platform holes than `out/27948-step2-surface-stop.md` listed: the `link_only` outcomes and both `permission_set_granted` outcomes (`${result.grantedPermissionSet.label}`). All on `record` / `result`, all converted. - **The family's other refusals now reach these keys**: a blank sentence, a single-brace `{x}`, a hole over an unbound root, logic or an unknown formatter. The in-repo census found none; full dogfood is the check on the platform's own copy. - **Not changed.** `packages/plugins/plugin-auth/src/role-change-granted-set.ts` line 29 still names `${result.grantedPermissionSet.label}` in a comment (outside the surface). The ui console pin `ActionParamDialog.invitationScreens-27521.test.tsx` carries its own `${…}` literal, which the console still reads. - **`ActionLike` / `ActionCopyKey` widening** is a type-level change for TypeScript readers of a parsed `ActionSchema`; the changeset names `templateSlotSource` as the reader. ## Verification Readings at head `a8f9f1c7` (merge base `776c1d87`). <details><summary>Tests</summary> - `pnpm --filter @objectstack/spec test`: Test Files 773 passed, 1 skipped; Tests 22871 passed. - `packages/lint` vitest: Test Files 183/183, Tests 7699/7699. - `packages/platform-objects` vitest: Test Files 104/104, Tests 1474/1474. - `packages/cli` vitest `test/i18n`: 44/44 files, 419 tests. `packages/runtime` vitest `src/action*`: 29/29 files, 521 tests. - `cd ui && pnpm exec vitest run packages/core/src/actions packages/react/src/hooks packages/react/src/context`: 108/108 files, 1263 tests. `packages/app-shell/src/views/metadata-admin/previews`: 157/157 files, 1713 tests. - Type-check: spec `tsc --noEmit`, runtime and cli `typecheck`, `@object-ui/core` and `@object-ui/react` `type-check`: clean. - `node scripts/check-i18n-bundles.mjs`: all bundles in sync. - Full dogfood: see the os-dev-report on the card. </details> <details><summary>Gates</summary> - Build: `env -u OS_SKIP_DTS bash scripts/pm/os-verify-lock.sh -c "pnpm exec turbo run build --filter='./packages/**' --filter='@objectstack/dogfood^...'"`, exit 0. - `dispatch-gates --commands` at `a8f9f1c7`: 116 families, stdin from `/dev/null`, exit codes recorded before any pipe; all 116 exit 0. `--ran`: 116 derived, 116 run, 0 NOT-MEASURED. Record: `/mnt/project-files/spec-seat-2/r2026-10-30/out/27948-step2-gates-ran-a8f9f1c7.list`. - The full CI farm is CI's. </details> --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) _Generated by [Claude Code](https://claude.ai/code/session_01L9qibwQ8tWwY7TiHQPMCHR)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 52306d4 commit 7e3822c

57 files changed

Lines changed: 1360 additions & 211 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@object-ui/core': patch
3+
'@object-ui/react': patch
4+
'@object-ui/fields': patch
5+
---
6+
7+
fix(core,react): an action's `description`, a param's `helpText` and an `outcomeMessages` entry read the `template` envelope
8+
9+
Clause-②: no
10+
Metadata-protocol: none
11+
12+
objectstack#27948 step 2 types these three as `template` slots, so a served action can carry `{ dialect: 'template', source }` where it carried a string.
13+
14+
- **Param dialog.** The runner hands the param-collection handler the action and params with `description` and each `helpText` reduced to their text (`actionCopyText`), so the dialog draws the sentence, never `[object Object]`; a locale map is handed on unchanged for the host to localize.
15+
- **Outcome toast.** `useActionTextLocalizer` keeps an `outcomeMessages` envelope's shape (a bundle entry replaces its `source`) instead of collapsing it to the word "template", and the runner fills the envelope's holes as it fills the string's.
16+
17+
- **Param fields.** `@object-ui/fields`' `resolveActionParams` reads a param's `helpText` through `actionCopySource`, so an envelope yields its sentence (a field-backed param with no text falls back to the field's own help) and `ActionParam` stays assignable to its `RawActionParam`.
18+
19+
- **Studio's action preview** (`@object-ui/app-shell`, unpublished) draws a param's `helpText`, `confirmText` and `successMessage` from the envelope's `source`: it drew `helpText` raw (an object as a React child) and read the other two as a locale map, which showed the word "template".
20+
21+
What to do: nothing. `${…}` copy written before the migration keeps reading.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
'@objectstack/platform-objects': patch
5+
'@objectstack/runtime': patch
6+
'@objectstack/cli': patch
7+
---
8+
9+
feat(spec)!: an action's `description`, each param's `helpText` and each `outcomeMessages` entry are `template` slots; the doors refuse `${…}` there
10+
11+
Clause-②: yes (narrowing)
12+
Metadata-protocol: narrowed
13+
14+
<!-- adr-0087: registered action-dialog-outcome-copy-template-holes -->
15+
16+
Step 2 of #27948 (ruling A): the five action copy keys speak one hole syntax, ADR-0032's `{{ }}`. `confirmText` and `successMessage` already did (#24814); the param dialog's `description`, each param's `helpText` and each `outcomeMessages` entry now join them.
17+
18+
- **The parse.** Each of the three is typed as `confirmText` is: a bare string is shorthand for `{ dialect: 'template', source }` and parses to that envelope; the envelope is taken; `description` and an `outcomeMessages` entry keep the inline locale map they took as an `I18nLabel`, and `helpText` stays without one (it was a plain string). A blank string and a `${…}` sentence still parse, so a stored action loads.
19+
- **The roots.** `description` and `helpText` read `confirmText`'s roots: `record`, the record the action runs against (a registered action's `params` is the input list the dialog collects, so it binds nothing). An `outcomeMessages` entry reads `successMessage`'s: `result`, the handler's answer, and `record`.
20+
- **The doors.** `objectstack validate`, `compile` and the publish gate (422) judge each sentence through the same judge as `confirmText`: a `${…}` token, a single-brace `{x}` token, a hole over a root the runner does not bind, a hole holding logic or an unknown formatter, and a blank sentence are errors, each named at its own path (`description`, `params[N].helpText`, `outcomeMessages.OUTCOME`) with its `{{ }}` spelling. An inline `properties.action` is judged on its params' `helpText`.
21+
- **The readers.** The CLI's i18n extraction seeds the envelope's text exactly as the bare string's, and the MCP `list_actions` param description reads `helpText`'s text, never the object.
22+
- **The platform's own copy** is rewritten: the invite actions' outcomes (`Invitation sent to {{ result.email }}`), `resend_invitation`'s and `update_member_role`'s descriptions, the role-change `permission_set_granted` outcomes, and their four translation bundles.
23+
24+
What to do (FROM → TO, on `description`, `params[].helpText` and `outcomeMessages` entries):
25+
26+
- `Resend the invitation to ${record.email}?` → `Resend the invitation to {{ record.email }}?`
27+
- `Invitation sent to ${result.email}` → `Invitation sent to {{ result.email }}`
28+
- `${record.owner?.name ?? 'nobody'}` → `{{ record.owner.name | default:'nobody' }}` (the `default` formatter also covers an empty value)
29+
- other logic in a `${…}` token moves into the handler's answer and is read from `result`.
30+
31+
The console keeps filling `${…}` in a stored sentence with no `{{ }}` hole, so stored copy reads as before until its next save, which is judged. `os migrate meta` lists the change (`action-dialog-outcome-copy-template-holes`); no conversion is registered, since a `${…}` token held an expression and a hole holds only a path and formatters.
32+
33+
A TypeScript reader of a parsed `ActionSchema` now meets the envelope on these three keys: read its text with `templateSlotSource` (`@objectstack/spec/automation`).

‎content/docs/references/api/metadata.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,7 @@ Metadata query with filtering, sorting, and pagination
794794
| **filePatterns** | `string[]` | ✅ | File glob patterns |
795795
| **supportsOverlay** | `boolean` | ✅ | Whether a stored overlay row of an item a managed package ships may be removed through the ordinary delete door without the type's environment-overlay permission: the removal removes no managed definition, and changes what is served for permission alone; it opens no save and it decides no read |
796796
| **domain** | `string` | ✅ | Protocol domain |
797-
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; objectName?: string; … }[]` | optional | Declarative type-level actions (buttons) the metadata-admin UI renders for this type |
797+
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| object \| string \| Record<string, string>; objectName?: string; … }[]` | optional | Declarative type-level actions (buttons) the metadata-admin UI renders for this type |
798798

799799

800800
---
@@ -971,7 +971,7 @@ Metadata query with filtering, sorting, and pagination
971971
| **membershipKeys** | `Record<string, { userField: string; valueField: string; filter?: string; label?: string; … }>` | optional | Membership keys this object defines, by name: each makes `current_user.KEY` the set of `valueField` values on this object's rows whose `userField` is the signed-in user and on which the optional `filter` holds, for a row-level security policy to read with `in` (e.g. `supplier in current_user.portal_supplier_ids`, declared on a supplier-contact object as `portal_supplier_ids: { userField: 'user', valueField: 'supplier', filter: 'active == true' }`). Declare a key on the one object whose rows define it; a key name is one request-wide name, so declaring it on two objects is refused, and the kernel's own names (`id`, `organization_id`, `positions`, `org_user_ids`, `accessible_org_ids`, `email`, `queues`) are refused. The set is read once per request, only when a policy reads the key, as a system read inside the caller's organization and regardless of what the caller may read on this object, so the caller needs no read on it; it holds at most 1,000 values, and a larger set, a failed read, a `filter` that does not compile for this object, or a key a registered membership resolver also owns makes every policy reading it match no rows. Whoever can write this object's rows can grant the key. |
972972
| **externalSharingModel** | `never` | optional | [REMOVED] `object.externalSharingModel` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — it never had an effect: no request resolves an external principal, and the external rung of the authorization kernel (ADR-0095) admits explicitly shared rows only, never an org-wide default, so the value changed no answer for anyone. Delete the key. `sharingModel` is the one org-wide default; a principal reaches a row it does not own through an explicit share, and an anonymous reader through a share link (`publicSharing`). Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
973973
| **publicSharing** | `{ enabled?: boolean; allowedAudiences?: Enum<'public' \| 'link_only' \| 'signed_in' \| 'email'>[]; allowedPermissions?: Enum<'view' \| 'comment' \| 'edit'>[]; maxExpiryDays?: integer; … }` | optional | Public share-link policy (Notion/Figma-style link sharing) |
974-
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; objectName?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) |
974+
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| object \| string \| Record<string, string>; objectName?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) |
975975
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this object. |
976976
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
977977
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |

‎content/docs/references/api/package-api-assembled.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ Installed package row whose manifest is the assembled package body
122122
| **dashboards** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; header?: object; … }[]` | optional | Dashboards |
123123
| **reports** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; type?: Enum<'tabular' \| 'summary' \| 'matrix' \| 'joined'>; … }[]` | optional | Analytics Reports |
124124
| **datasets** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; object: string; … }[]` | optional | Analytics semantic-layer datasets (ADR-0021) |
125-
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
125+
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| object \| string \| Record<string, string>; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
126126
| **flows** | `{ name: string; label: string; description?: string; successMessage?: string; … }[]` | optional | Screen Flows |
127127
| **jobs** | `{ name: string; label?: string; description?: string; schedule: object \| object \| object; … }[]` | optional | Background / Scheduled Jobs (run by IJobService on cron/interval/once schedules) |
128128
| **emailTemplates** | `{ name: string; label: string; locale?: string; subject: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |
@@ -372,7 +372,7 @@ This schema accepts one of the following structures:
372372
| **dashboards** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; header?: object; … }[]` | optional | Dashboards |
373373
| **reports** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; type?: Enum<'tabular' \| 'summary' \| 'matrix' \| 'joined'>; … }[]` | optional | Analytics Reports |
374374
| **datasets** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; object: string; … }[]` | optional | Analytics semantic-layer datasets (ADR-0021) |
375-
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
375+
| **actions** | `{ name: string; label: string \| Record<string, string>; description?: string \| object \| string \| Record<string, string>; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
376376
| **flows** | `{ name: string; label: string; description?: string; successMessage?: string; … }[]` | optional | Screen Flows |
377377
| **jobs** | `{ name: string; label?: string; description?: string; schedule: object \| object \| object; … }[]` | optional | Background / Scheduled Jobs (run by IJobService on cron/interval/once schedules) |
378378
| **emailTemplates** | `{ name: string; label: string; locale?: string; subject: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |

0 commit comments

Comments
 (0)