Skip to content

fix(spec): prescribe per action type when object-form params is refused — bodyExtra for api, target interpolation + openIn for url (#6828) - #7375

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6828-url-params-guidance
Aug 10, 2026
Merged

fix(spec): prescribe per action type when object-form params is refused — bodyExtra for api, target interpolation + openIn for url (#6828)#7375
os-zhuang merged 1 commit into
mainfrom
claude/issue-6828-url-params-guidance

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes #6828

Executes the maintainer's 2026-08-10 ruling (comment 5237221393), verbatim:

A key with three meanings and no authorized spelling for the third is the de-facto-contract shape AGENTS.md #0.1 forbids; the schema already refuses it and there is no live victim, so retirement costs nothing. Interpolation scope + newTab on a type:'url' inline action either gets its own properly-named key via a spec proposal (if someone demonstrates pull) or stays out of the vocabulary. Document the refusal in the error guidance so authors reaching for it learn the sanctioned spellings.

No new key enters the vocabulary. This is a message-and-docs change.

Premise check (re-verified on main @ f188ed6, not inherited from the card)

Claim Verdict
params is still z.array(ActionParamSchema, …) packages/spec/src/ui/action.zod.ts:907 (the card's :834 had drifted; re-anchored by content)
The refusal message still mis-prescribes for a url action ✅ it named bodyExtra unconditionally — see the before/after below
The inline-action-api-params-to-body-extra conversion still guards on type === 'api' packages/spec/src/conversions/registry.ts — unchanged, and untouched here
Acceptance face moves ❌ nothing moved — see "No acceptance-face movement"

Before / after

Before (post-#5777) — one prescription, applied to every action type:

`params` is the parameter DEFINITION array (fields collected from the user before the action
runs), not the request payload. For a `type:'api'` action's static request body — including
`{{page.<var>}}` tokens — use `bodyExtra: { … }` instead (#5777). Expected an array of
ActionParam, received an object.

A type:'url' author reading that is sent to an api request-body key that is neither an interpolation scope nor a new-tab control. Wrong instruction — and the one the ruling's rationale rests on.

After — the same opening and the same api arm, plus the url arm (the new sentence marked >>):

`params` is the parameter DEFINITION array (fields collected from the user before the action
runs), not a values map. For a `type:'api'` action's static request body — including
`{{page.<var>}}` tokens — use `bodyExtra: { … }` instead (#5777).
>> For a `type:'url'` action there is nowhere to move it to, by decision: put static values
>> straight into the `target` string (`${param.X}` interpolates a value collected by the params
>> dialog, `${ctx.X}` one from the action context), and open a new tab with `openIn: 'new-tab'`.
>> The url-side readings of an object `params` — a static `${param.X}` scope, and
>> `params.newTab` — are RETIRED, not renamed (#6828).
Expected an array of ActionParam, received an object.

Both retired readings have a sanctioned spelling already:

Retired reading Sanctioned spelling
statically authored ${param.X} scope the value goes in the target string itself; ${param.X} interpolates what the params dialog collected, ${ctx.X} the action context
params.newTab openIn: 'new-tab' — declared, and already read with priority by the runner

No acceptance-face movement

params is still z.array(ActionParamSchema); the object form is still refused with invalid_type at path params on both the inline and registered surfaces; the array form still parses on every action type (pinned). Nothing was added to or removed from the vocabulary, and no .describe() text changed — so no generated reference moved and no regen was needed. The shape was always refused; only what the refusal says changed.

Pins (packages/spec/src/ui/inline-action.test.ts)

Both arms, by path + code + message content:

Pin Asserts
url — interpolation scope invalid_type @ params, expected: 'array', message contains `target` string, openIn: 'new-tab', RETIRED, not renamed (#6828)
url — params.newTab invalid_type @ params, message contains params.newTab and openIn: 'new-tab'
api — not displaced invalid_type @ params, message still contains bodyExtra: { … } and (#5777)
registered surface the same url guidance from ActionSchema — the field factory is shared, so a future fix cannot branch on the inline shape alone
acceptance a type:'url' action with params: [ … ] + openIn: 'new-tab' still parses

Reverse verification: reverting only the message (git checkout HEAD -- action.zod.ts) turns the three url-arm pins RED on message content — 3 failed | 25 passed; restored, 28 passed. The api pin is green on both sides by design: its job is that the url arm did not displace #5777's answer, which is a non-regression guard, not a new claim.

Implementation note — the branch is in the text, not in a runtime switch

Zod cannot see a sibling from a property-level error map: it receives only { code, expected, input, inst, path } for the offending value, and an object-level .check() / .superRefine() — which would see type — is skipped once a property has already failed (probed directly on zod 4.4.3, the pinned version). Reading type at the refusal site would mean restructuring ActionSchema behind a z.preprocess, which erases z.input<typeof ActionSchema> (the authoring type defineAction publishes) and would move surfaces this issue must not move. So one message carries both arms, each explicitly addressed to its type, and the constraint is recorded in the JSDoc so the next reader does not re-derive it.

Docs (the ruling's closing sentence)

  • content/docs/ui/actions.mdx — a warn callout under "Collect input and shape the UX": a three-row "you wanted / write this instead" table (api payload → bodyExtra; url interpolation → the target string; new tab → openIn), and the statement that the url meanings were retired, not renamed.
  • content/docs/protocol/objectui/actions.mdx — in URL Actions, next to the ${param.X} / ${ctx.X} sentence that describes the mechanism. Also separates the two new-tab keys, which that page had conflated: openIn for a static url, opensInNewTab for an async handler that redirects.

content/docs/references/** is untouched (generated, and no describe() moved); content/docs/releases/** and docs/adr/** are untouched.

Follow-up filed

objectstack-ai/objectui#4097ActionRunner's interpolateTarget non-array params scope read and the legacy params.newTab escape hatch are dead vocabulary under this ruling. Filed unassigned, contract-first behind this PR; objectui was not edited here. Dedup-searched (interpolateTarget params newTab, ActionRunner params, bodyExtra, 6828) — no existing card; #2043 is the adjacent origin of openIn, not a duplicate.

Changeset

.changeset/action-params-url-refusal-guidance.mdpatch on @objectstack/spec. Precedent: a guidance-only change that moves no acceptance face is a patch (cf. adr-0105-d9-refuse-on-directory-less-types, @objectstack/spec: patch in action-body-type-gate where the behavioural halves took the minor). No key added, no key removed, no accepted input newly refused or newly accepted.

Gates run

vitest on @objectstack/spec (362 files / 9473 tests, green, under flock /tmp/os-heavy-verify.lock) · eslint --no-inline-config on the changed files · tsc --noEmit on packages/spec · check:doc-authoring · check:docs-audit-scope · check:quick-reference-counts · check:empty-changeset · check:adr-0087-registration.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vsi2JQ41Z9ct1z8jC4MjVW

…used (#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
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 8:10am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling labels Aug 10, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 08:59
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit a70358a Aug 10, 2026
28 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6828-url-params-guidance branch August 10, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Object-form params on a type:'url' inline action is a THIRD meaning of the key (interpolation scope + newTab) with no authorized spelling (observation)

2 participants