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
runtime: execute the declarative row-level operation: 'update' action — the platform action route performs one data-plane update of the current record as the caller (runtime half of #14092) #15079
Filed by the domain:spec seat (session_0174WZTU6XcFcS7g2kykC53i, seat post #6017) at the contract-review ACCEPT of PR #15077 (the spec half of #14092), per the contract-first split the maintainer ruling on #14092 prescribes (ruling comment 5494341350, director batch 22, 2026-09-01, verbatim 「同意」). Filed with pm:blocked; the domain:* label is triage's (landing package packages/runtime, the domain:cli table row). Dispatches when PR #15077 is MERGED. Until this half lands, operation / patch stay planned in packages/spec/liveness/action.json and an authored update action reaches this route and gets the registry's loud not-registered answer — never a silent no-op.
Spec shape this executes (PR #15077, head 44e26d67)
ActionSchema gains operation: 'update' (one member, a parallel key beside type; type keeps its materialized default 'script') and patch (a record of static field values, optional). The parsed shape is always { type: 'script', operation: 'update', patch }. Beside operation: 'update' the spec refuses target, body, method, bodyExtra, bodyShape, recordIdParam, recordIdField, onSuccess, opensInNewTab, newTabUrl, list_toolbar in locations, and any explicit type other than 'script'; patch without operation and operation with neither patch nor params are refused. A standalone action with operation: 'update' and no objectName is refused by defineStack's cross-reference walk.
Executor contract (pinned in PR #15077's body, section "Executor contract")
Read operation before type. An action with operation: 'update' is the declarative single-record field write; its type is 'script' (the route) and it carries no handler, no target, no body.
The write is { ...patch, ...collectedParams } — static values UNDER the dialog's values; nothing else from the action is merged.
undoable: true — the result carries the prior values of exactly the fields written (shape pinned in the PR body for review) so the existing Undo readers can restore.
visible is a UI gate the client evaluates with the record bound; point 3 is the authorization.
No current record (no recordId on the route, or a standalone action without objectName) ⇒ a located refusal, not a silent no-op.
Landing seams (measured on origin/mainbe416187, 2026-09-03T20:32Z; re-verify at dispatch)
packages/runtime/src/action-execution.ts — invokeBusinessAction (line 1300) loads the subject record (loadActionSubjectRecord :1245 through callData('get', …) :1383) and then dispatches on type: flow (:1406) or the registered handler (executeRegisteredAction :1650 → ql.executeAction :1659, whose miss surfaces as isActionNotRegisteredError :1572). The update branch goes BEFORE that switch (contract point 1) and writes through callData('update', …) (:126) with the caller's execution context — not buildActionExecutionContext (:1126), which forces isSystem: true for script bodies.
Same file — the headless predicates read type only today: isHeadlessInvokableAction :525-528 (script ⇒ needs target or body, so an update action currently reads as NOT invokable), SERVER_DISPATCHED_ACTION_TYPES :537, headlessActionTypeError :553, summarizeAction :917. Each gains the operation branch so the MCP run_action bridge (packages/runtime/src/domains/mcp.ts:622) and the HTTP door agree.
packages/runtime/src/domains/actions.ts — handleActionsRequest (:381), the door POST /actions/:object/:action and /:recordId; its docblock (:357-380, "Dispatch follows the DECLARED action type") gains the update row.
packages/runtime/src/dispatcher-plugin.ts:1635 and :1643 — the two route registrations (no change expected; listed so the implementer reads them).
Pins (assert code + status + path wherever an error is asserted — the ADR-0112 envelope; a bare toThrow does not count)
an operation: 'update' action with patch performs exactly one data-plane update of the current record, as the caller (the driver call carries the caller's context, not isSystem);
{ ...patch, ...params } precedence — a param of the same name wins;
a caller without write permission on the row is refused, located; a validation failure surfaces; a before-update hook runs;
no recordId ⇒ refusal; a standalone action with objectName works; undoable: true ⇒ the prior values of exactly the written fields are in the result;
the headless / MCP predicate treats the action as invokable;
reverse pin: a handler-less type: 'script' action WITHOUT operation keeps today's not-registered answer (no widening of the script path).
Not this card
packages/spec/** (the ledger flip of operation / patch to live is the spec seat's follow-up card, filed beside this one); objectui (its executor half is filed on objectstack-ai/objectui); packages/objectql / packages/rest unless the write needs a seam there — report it, do not widen silently.
Refs
#14092 · PR #15077 · #14010 (runAs: 'user') · #14143 (a swallowed load is not a grant) · #3915 (dispatch by declared type) · #5519 (anonymous baseline on the actions door)
Blocked-by: #14092
Filed by the
domain:specseat (session_0174WZTU6XcFcS7g2kykC53i, seat post #6017) at the contract-review ACCEPT of PR #15077 (the spec half of #14092), per the contract-first split the maintainer ruling on #14092 prescribes (ruling comment 5494341350, director batch 22, 2026-09-01, verbatim 「同意」). Filed withpm:blocked; thedomain:*label is triage's (landing packagepackages/runtime, thedomain:clitable row). Dispatches when PR #15077 is MERGED. Until this half lands,operation/patchstayplannedinpackages/spec/liveness/action.jsonand an authored update action reaches this route and gets the registry's loud not-registered answer — never a silent no-op.Ruling (quoted from 5494341350, untranslated)
Spec shape this executes (PR #15077, head
44e26d67)ActionSchemagainsoperation: 'update'(one member, a parallel key besidetype;typekeeps its materialized default'script') andpatch(a record of static field values, optional). The parsed shape is always{ type: 'script', operation: 'update', patch }. Besideoperation: 'update'the spec refusestarget,body,method,bodyExtra,bodyShape,recordIdParam,recordIdField,onSuccess,opensInNewTab,newTabUrl,list_toolbarinlocations, and any explicittypeother than'script';patchwithoutoperationandoperationwith neitherpatchnorparamsare refused. A standalone action withoperation: 'update'and noobjectNameis refused bydefineStack's cross-reference walk.Executor contract (pinned in PR #15077's body, section "Executor contract")
operationbeforetype. An action withoperation: 'update'is the declarative single-record field write; itstypeis'script'(the route) and it carries no handler, notarget, nobody.recordIdthe route already receives) on the object the action belongs to (its embedding object, orobjectNameon a standalone action), through the data plane under the caller's own identity — never theisSystem-elevated script-body context. This consumes the A hook cannot elevate, so a hook-written computed column cannot be protected by field-leveleditable: false— the guard and the writer are the same door #14010 direction (runAs: 'user'pinned to the triggering user) and adds norunAskey.ctx.record.idafter a failed caller-scope load, so the natural authorization guard (if (!ctx.record?.id) refuse()) is always true on a row the caller cannot read #14143 class: a swallowed load must never become an implicit grant.requiredPermissionsstays enforced on the route.{ ...patch, ...collectedParams }— static values UNDER the dialog's values; nothing else from the action is merged.undoable: true— the result carries the prior values of exactly the fields written (shape pinned in the PR body for review) so the existing Undo readers can restore.visibleis a UI gate the client evaluates with the record bound; point 3 is the authorization.recordIdon the route, or a standalone action withoutobjectName) ⇒ a located refusal, not a silent no-op.Landing seams (measured on
origin/mainbe416187, 2026-09-03T20:32Z; re-verify at dispatch)packages/runtime/src/action-execution.ts—invokeBusinessAction(line 1300) loads the subject record (loadActionSubjectRecord:1245 throughcallData('get', …):1383) and then dispatches ontype:flow(:1406) or the registered handler (executeRegisteredAction:1650 →ql.executeAction:1659, whose miss surfaces asisActionNotRegisteredError:1572). The update branch goes BEFORE that switch (contract point 1) and writes throughcallData('update', …)(:126) with the caller's execution context — notbuildActionExecutionContext(:1126), which forcesisSystem: truefor script bodies.typeonly today:isHeadlessInvokableAction:525-528 (script⇒ needstargetorbody, so an update action currently reads as NOT invokable),SERVER_DISPATCHED_ACTION_TYPES:537,headlessActionTypeError:553,summarizeAction:917. Each gains theoperationbranch so the MCPrun_actionbridge (packages/runtime/src/domains/mcp.ts:622) and the HTTP door agree.packages/runtime/src/domains/actions.ts—handleActionsRequest(:381), the doorPOST /actions/:object/:actionand/:recordId; its docblock (:357-380, "Dispatch follows the DECLARED action type") gains the update row.packages/runtime/src/dispatcher-plugin.ts:1635and:1643— the two route registrations (no change expected; listed so the implementer reads them).Pins (assert
code+status+pathwherever an error is asserted — the ADR-0112 envelope; a baretoThrowdoes not count)operation: 'update'action withpatchperforms exactly one data-plane update of the current record, as the caller (the driver call carries the caller's context, notisSystem);{ ...patch, ...params }precedence — a param of the same name wins;recordId⇒ refusal; a standalone action withobjectNameworks;undoable: true⇒ the prior values of exactly the written fields are in the result;type: 'script'action WITHOUToperationkeeps today's not-registered answer (no widening of the script path).Not this card
packages/spec/**(the ledger flip ofoperation/patchtoliveis the spec seat's follow-up card, filed beside this one); objectui (its executor half is filed on objectstack-ai/objectui);packages/objectql/packages/restunless the write needs a seam there — report it, do not widen silently.Refs
#14092 · PR #15077 · #14010 (
runAs: 'user') · #14143 (a swallowed load is not a grant) · #3915 (dispatch by declared type) · #5519 (anonymous baseline on the actions door)