You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec): prescribe per action type when object-form params is refused (#6828)
`params` has always been `z.array(ActionParamSchema)`, so the object form has
always been refused. #5777 replaced the unactionable "expected array, received
object" with a message naming `bodyExtra` — right for a `type:'api'` action's
static request body, and wrong for every other type.
On a `type:'url'` action the object form meant a third thing: objectui's
ActionRunner read a non-array `params` as the `${param.X}` interpolation scope
for `target`, and `params.newTab` as a legacy new-tab flag. Neither is a
request-body key, which is why the `inline-action-api-params-to-body-extra`
conversion guards on `type === 'api'` (rewriting a url action's object `params`
would be lossy; ADR-0087 D2 requires losslessness).
The maintainer's 2026-08-10 ruling retires the url meaning rather than giving
it a key — both halves already have sanctioned spellings: put static values in
the `target` string, and open a new tab with the declared `openIn: 'new-tab'`.
The refusal message now carries both arms, and the authoring docs state the
refusal where inline and url actions are described.
No acceptance-face movement: `params` is still `z.array(ActionParamSchema)`,
the object form is still refused with `invalid_type` at path `params`, and the
array form still parses on every action type. Message and docs only.
Pinned on both arms and on both the inline and registered surfaces
(`inline-action.test.ts`). Reverse-verified: reverting the message turns the
three url-arm pins RED on message content.
Closes#6828
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vsi2JQ41Z9ct1z8jC4MjVW
fix(spec): the object-form `params` refusal prescribes per action type — `bodyExtra` for `api`, `target` interpolation + `openIn` for `url` (#6828)
6
+
7
+
`params` has always been `z.array(ActionParamSchema)`, so writing it as an object
8
+
has always been refused. What changed in #5777 is the *message*: it stopped being
9
+
the unactionable "expected array, received object" and started naming
10
+
`bodyExtra`, the key the maintainer's 2026-08-06 ruling created for a
11
+
`type:'api'` action's static request body.
12
+
13
+
That prescription is right for exactly one action type. On a `type:'url'` action
14
+
the object form meant something else entirely — objectui's `ActionRunner` read a
15
+
non-array `params` as the `${param.X}` interpolation scope for `target`, and
16
+
`params.newTab` as a legacy new-tab flag. Telling that author to use `bodyExtra`
17
+
sent them to an api request-body key that is neither an interpolation scope nor a
18
+
new-tab control. (The same asymmetry is why the
19
+
`inline-action-api-params-to-body-extra` conversion guards on `type === 'api'`:
20
+
rewriting a url action's object `params` into `bodyExtra` would be lossy, and
21
+
ADR-0087 D2 requires losslessness.)
22
+
23
+
The maintainer's 2026-08-10 ruling on #6828**retired the url meaning** rather
24
+
than giving it a key — a key with three meanings and no authorized spelling for
25
+
the third is the de-facto-contract shape AGENTS.md #0.1 forbids, the schema
26
+
already refuses it, and nothing in the reachable corpus authors it. Both halves
27
+
already have sanctioned spellings:
28
+
29
+
| Retired reading | Sanctioned spelling |
30
+
|:---|:---|
31
+
| statically authored `${param.X}` scope | put the value in the `target` string itself (`${param.X}` interpolates what the params **dialog** collected; `${ctx.X}` the action context) |
32
+
|`params.newTab`|`openIn: 'new-tab'` (declared, and already read with priority by the runner) |
33
+
34
+
So the refusal message now carries both arms, and the authoring docs
35
+
(`ui/actions`, `protocol/objectui/actions`) state the refusal where inline and
36
+
url actions are described.
37
+
38
+
**No acceptance-face movement**: `params` is still `z.array(ActionParamSchema)`,
39
+
the object form is still refused with `invalid_type` at path `params`, and the
40
+
array form still parses on every action type. This is a message-and-docs change —
41
+
hence `patch` — pinned on both arms and on both the inline and registered
42
+
surfaces.
43
+
44
+
The two objectui reads this ruling makes dead vocabulary (`interpolateTarget`'s
45
+
non-array `params` scope, and the `params.newTab` escape hatch) are objectui's
To open the result in a new tab, set `opensInNewTab: true` (the renderer pre-opens the tab synchronously so popup blockers don't fire). `newTabUrl` provides a zero-roundtrip new-tab target template supporting the `{recordId}` placeholder.
95
+
For a **static** url, `openIn: 'new-tab'` opens `target` in a new tab and `openIn: 'self'` navigates in place; omitted, absolute URLs open in a new tab and relative ones navigate in place. For an **async handler that redirects**, set `opensInNewTab: true` instead (the renderer pre-opens the tab synchronously so popup blockers don't fire); `newTabUrl` provides a zero-roundtrip new-tab target template supporting the `{recordId}` placeholder.
96
+
97
+
<Callout type="warn">
98
+
**The interpolation scope is not authorable, and `params` is not it.** `params`
99
+
is the parameter DEFINITION array — `ActionParam[]`, the dialog shown before
100
+
the action runs — on every action type, so `params: { id: 'abc' }` on a url
101
+
action is rejected at parse time. `${param.X}` resolves against the values that
102
+
dialog **collected**; a value you already know at authoring time belongs
103
+
literally in the `target` string.
104
+
105
+
Two url-side readings of an object-form `params` existed in the renderer and
106
+
were **retired** by the 2026-08-10 ruling on
107
+
[#6828](https://github.com/objectstack-ai/objectstack/issues/6828) rather than
108
+
given a key: a statically authored `${param.X}` scope (say it in `target`), and
109
+
`params.newTab` (say it with `openIn: 'new-tab'`). The refusal message names
110
+
both replacements. Note the asymmetry with `type: 'api'`, where the object form
111
+
*did* get a key — `bodyExtra`, per
112
+
[#5777](https://github.com/objectstack-ai/objectstack/issues/5777) — because a
113
+
request payload has no other spelling; a url interpolation scope does.
Copy file name to clipboardExpand all lines: content/docs/ui/actions.mdx
+20Lines changed: 20 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -249,6 +249,26 @@ construction — exist at all.
249
249
(generated tokens, export links).
250
250
-**`variant` / `icon` / `order`** — presentation and sort position.
251
251
252
+
<Callouttype="warn">
253
+
**`params` is an array of parameter *definitions*, never a map of values.**
254
+
Writing `params: { … }` is rejected at authoring time, and what to write
255
+
instead depends on the action's `type`:
256
+
257
+
| You wanted | Write this instead |
258
+
|:---|:---|
259
+
| A static request body for `type: 'api'`|`bodyExtra: { name: '{{page.inquiryName}}' }` — merged last, `{{page.<var>}}` tokens resolved by the runtime |
260
+
| A value to interpolate into a `type: 'url'``target`| Put it in the `target` string itself. `${param.X}` interpolates a value the params **dialog** collected; `${ctx.X}` one from the action context |
261
+
| A new tab for `type: 'url'`|`openIn: 'new-tab'` (for an *async* handler that redirects, use `opensInNewTab` instead) |
262
+
263
+
There is no object form of `params` on a url action to migrate to: the two
264
+
things it used to mean in the renderer — a statically authored `${param.X}`
265
+
scope, and a `params.newTab` flag — were **retired**, not renamed
266
+
([#6828](https://github.com/objectstack-ai/objectstack/issues/6828)). Both are
267
+
already expressible with the keys above, so a third meaning of `params` earns
268
+
nothing; if you have a case the `target` string genuinely cannot express, that
269
+
is a spec proposal for a properly named key, not a values map under this one.
270
+
</Callout>
271
+
252
272
## Permissions and visibility
253
273
254
274
-**`requiredPermissions: ['can_close_tickets']`** is a **dual-surface gate**
* **The api prescription is not universal, which #6828 measured and the
908
+
* maintainer's 2026-08-10 ruling closed.** On a `type:'url'` action the
909
+
* object form meant a THIRD thing again — objectui's `ActionRunner` read a
910
+
* non-array `params` as the `${param.X}` interpolation scope for `target`,
911
+
* and `params.newTab` as a legacy new-tab flag. Sending that author to
912
+
* `bodyExtra` is a wrong instruction: an api request-body key is not an
913
+
* interpolation scope (the same asymmetry is why the conversion above guards
914
+
* on `type === 'api'` — rewriting a url action's object `params` would be
915
+
* lossy, and ADR-0087 D2 requires losslessness). The ruling **retired** the
916
+
* url meaning rather than giving it a key: the scope is already expressible
917
+
* as `target`-string interpolation, and the flag is already {@link openIn}.
918
+
* So the refusal below prescribes per action type — `bodyExtra` for `api`,
919
+
* the sanctioned url spellings for `url` — and nothing new enters the
920
+
* vocabulary. A future authorable interpolation-scope key needs a spec
921
+
* proposal that demonstrates pull, not a third arm of this one.
922
+
*
923
+
* The branch is stated IN THE TEXT rather than selected at runtime because
924
+
* zod cannot see a sibling from a property-level error map: the map receives
925
+
* only `{ code, expected, input, inst, path }` for the offending value, and
926
+
* an object-level `.check()`/`.superRefine()` — which would see `type` — is
927
+
* skipped once a property has already failed (probed on zod 4.4.3). Reading
928
+
* `type` here would mean restructuring `ActionSchema` behind a
929
+
* `z.preprocess`, which erases `z.input<typeof ActionSchema>` (the authoring
930
+
* type `defineAction` publishes) — a far larger change than the guidance
931
+
* defect warrants, and one that moves surfaces this issue must not move.
906
932
*/
907
933
params: z.array(ActionParamSchema,{
908
934
error: (iss)=>(
909
935
iss.code==='invalid_type'
910
936
&&iss.input!==null
911
937
&&typeofiss.input==='object'
912
938
&&!Array.isArray(iss.input)
913
-
? "`params` is the parameter DEFINITION array (fields collected from the user before the action runs), not the request payload. "
939
+
? "`params` is the parameter DEFINITION array (fields collected from the user before the action runs), not a values map. "
914
940
+"For a `type:'api'` action's static request body — including `{{page.<var>}}` tokens — use `bodyExtra: { … }` instead (#5777). "
941
+
+"For a `type:'url'` action there is nowhere to move it to, by decision: put static values straight into the `target` string "
942
+
+"(`${param.X}` interpolates a value collected by the params dialog, `${ctx.X}` one from the action context), and open a new tab with "
943
+
+"`openIn: 'new-tab'`. The url-side readings of an object `params` — a static `${param.X}` scope, and `params.newTab` — are RETIRED, not renamed (#6828). "
915
944
+'Expected an array of ActionParam, received an object.'
0 commit comments