Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/25800-flow-action-on-success.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'@objectstack/spec': minor
'@object-ui/types': minor
'@object-ui/core': minor
'@object-ui/app-shell': minor
---

feat(spec,console): a `type: 'flow'` action may declare `onSuccess`, and the console navigates after a run that completes

Clause-②: yes
Metadata-protocol: added

A flow action that creates a record could not take the user to it. `ActionSchema` refused `onSuccess` beside `type: 'flow'` (and the page action rows refused it beside a `flow` executor), so after the run the user stayed on the page they started from.

- **`@objectstack/spec`:** `onSuccess` is now accepted on a `type: 'flow'` action and beside a page action row's `flow` executor, next to `api` and `script`. On a flow action, `${result.*}` reads the completed run's answer (`AutomationResult`). Its `output` holds the flow's `isOutput` variables, so a `create_record` node that writes an `isOutput` variable `renewal` is reached as `${result.output.renewal.id}`. A variable the flow does not declare `isOutput: true` is not in `output`. The refusal text for `url`, `navigation`, `modal` and `form` now names the three executors. The Studio action form shows the `onSuccess` row for a flow action.
- **`@object-ui/types`:** `ActionResult` gains `handedOff`: the success is a hand-off to a follow-up surface, not a completion.
- **`@object-ui/app-shell`:** the flow launch judge marks a run that pauses at a screen, or ends refused, `handedOff`. A run that completes is not marked, including one whose toast is the flow's own `successMessage`.
- **`@object-ui/core`:** the action runner performs no `onSuccess` hop for a `handedOff` success. A flow run that pauses at a screen or ends refused does not navigate, neither when the screen or the refusal notice opens nor when the flow runner later finishes the run.

Example:

```ts
{
name: 'start_renewal',
type: 'flow',
target: 'renewal_start',
onSuccess: { navigate: '/apps/clm/clm_contract/record/${result.output.renewal.id}' },
}
```

with the flow declaring `variables: [{ name: 'renewal', type: 'object', isOutput: true }]` and its `create_record` node writing `outputVariable: 'renewal'`.
2 changes: 1 addition & 1 deletion content/docs/references/data/object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ const result = ApiMethod.parse(data);
| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. |
| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api' and type:'script' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response); `openIn` defaults 'self'. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api', type:'script' and type:'flow' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response; for a flow, the completed run's answer); `openIn` defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. 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. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/kernel/metadata-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ const result = MetadataBulkResultSchema.parse(data);
| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. |
| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api' and type:'script' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response); `openIn` defaults 'self'. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api', type:'script' and type:'flow' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response; for a flow, the completed run's answer); `openIn` defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. 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. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/ui/action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const result = ActionSchema.parse(data);
| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. |
| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api' and type:'script' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response); `openIn` defaults 'self'. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api', type:'script' and type:'flow' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response; for a flow, the completed run's answer); `openIn` defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. 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. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
Expand Down Expand Up @@ -178,7 +178,7 @@ L2 sandboxed JS body — runs inside an isolated VM with declared capabilities

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **navigate** | `string` | ✅ | Route/URL template navigated to after the action succeeds. Interpolates $`{param.*}` (params-dialog values), $`{ctx.*}` (origin/apiBase/user/org/recordId/selection) and $`{result.*}` (the action's server response payload — NEW with this key, e.g. $`{result.id}`). An http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ (an SPA route hop); a template that opens with $&#123; is judged after interpolation. Every other scheme and a protocol-relative //host are refused. Renderers MUST encodeURIComponent values in query positions. |
| **navigate** | `string` | ✅ | Route/URL template navigated to after the action succeeds. Interpolates $`{param.*}` (params-dialog values), $`{ctx.*}` (origin/apiBase/user/org/recordId/selection) and $`{result.*}` (the action's server response payload — NEW with this key, e.g. $`{result.id}`; on a type:'flow' action the completed run's answer, whose `output` carries the flow's isOutput variables, e.g. $`{result.output.renewal.id}`). An http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ (an SPA route hop); a template that opens with $&#123; is judged after interpolation. Every other scheme and a protocol-relative //host are refused. Renderers MUST encodeURIComponent values in query positions. |
| **openIn** | `Enum<'self' \| 'newTab'>` | optional (default: `"self"`) | Where to perform the post-success navigation: 'self' (default — in-place SPA navigation, immune to popup blocking) or 'newTab'. Closed enum — no general navigation DSL. |


Expand Down
Loading
Loading