Skip to content

fix(app-shell): a record-triggered Start node is judged against the scope the engine binds, with one verdict (objectui#11789) - #11849

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11789-cel-scope-verdict
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11789-cel-scope-verdict

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11789

Clause-②: no

What was wrong, measured on main (87f7b6c)

A record-triggered flow's Start node read "Valid CEL" and, right under it, "record is not a reference in scope at this step." for the same entry condition. Two defects were behind that one screen.

  1. The Start node's scope had no record. resolveFlowScope in flow-scope.ts pushed the whole-record record ref only if (!onStart). The engine binds it there. seedRunVariables in @objectstack/service-automation sets record, $record, the record's flattened fields and previous, and the start-condition gate then evaluates against that same variables map. Every shipped spelling resolves at run time: record.status and bare status.
  2. Two verdicts for one expression. The raw CEL editor (CelPredicateField, mounted by ConditionBuilder) says "Valid CEL" from its lint. Its lint knows the CEL scope roots, not the flow's scope at the node. The scope note comes from somewhere else, so a root the lint accepts and the flow scope rejects got both lines.

This PR's new pins were first run with the source unchanged: 3 failed, 3 passed (6). The card's expression failed on the scope note. The trigger.status pin and the editor pin failed on "Valid CEL" shown beside the note. The 3 that passed are the controls.

The change

record is in scope on a record-triggered Start node

In flow-scope.ts, the record ref is pushed at every node of a record-triggered flow, the Start node included. What still differs on the Start node is the per-field prefix only: bare there, record. downstream. A schedule, manual or API Start node gains nothing. The existing record-trigger gate (RECORD_TRIGGER_TYPES plus an objectName) is untouched.

flow-ref-check.ts is not touched. Adding record / previous to its RUNTIME_GLOBALS would accept them on schedule and manual flows, where the engine binds no record.

previous follows the engine's pre-image

The engine binds previous on every run: to the pre-image the record-change trigger hands it, or to null when there is none. So Studio scopes it where a pre-image exists.

Trigger record previous What the engine binds
record-after-update, record-before-update in scope in scope the prior row
record-after-write, record-before-write in scope in scope null on the create leg, the prior row on the update leg (previous == null picks the create leg)
record-after-delete in scope in scope (new) the deleted row: the data engine binds the pre-image before the delete runs, and the trigger reads record from it too
record-after-create in scope out (unchanged) always null: there is no prior row
schedule, manual, api out out no record is handed to the run

Why create stays out (the seat's answer A to this PR's open question): a member read on null, such as previous.status, is a CEL evaluation error. The engine's evaluateCondition throws on it, so the run fails. And previous == null is constantly true there. Showing the scope note is the useful verdict. The table is pinned row by row in flow-scope.test.ts, at the Start node and downstream.

One verdict: the scope note replaces "Valid CEL"

Zone 2 #3's location is falsified. The scope line under the entry condition is not FlowExprIssue's. It is FlowNodeConfigField's own describeUnknownRefs note, rendered under the control it mounts. FlowExprIssue never renders beside a CelPredicateField: only the Start node's condition descriptor opts into conditionBuilder. So the rule lands where both verdicts render:

  • FlowNodeConfigField computes its scope verdict before it builds the control, and hands scopeIssue to the ConditionBuilder it mounts;
  • ConditionBuilder forwards the new optional scopeIssue to its raw editor;
  • CelPredicateField withholds "Valid CEL" when scopeIssue is set. The lint, its findings and onLintChange are unchanged. With no scopeIssue, nothing changes. That covers the permission set's row-level security clauses, pinned as a control.

The note's wording is unchanged and no catalogue row was added. FlowNodeConfigField's FLOW_TRIGGER_CONTEXT_SUBJECTS doc comment said flow-scope.ts withholds record on the Start node, which this change makes false, so it was reworded. The builder still offers only the bare spelling, because record.FIELD is the same value. Two sibling test files carried the same false reason in a test name and a comment, and were reworded the same way. No assertion changed.

Side effect: an edge leaving the Start node

An edge's guard is judged against the scope at its source (resolveEdgeScope / useEdgeScope). So an edge leaving the Start node now accepts record too. That matches the engine: traverseNext evaluates those guards against the same run variables. The Problems panel's expression scan skips the Start node and its out-edges (flowExpressionProblems, by design: there the trigger fields are not expanded), so its output does not move.

Verification

All at b097f9a (this branch merged with main 455c646) unless stated.

  • pnpm --filter @object-ui/app-shell type-check (echoed tsc --noEmit && tsc -p tsconfig.test.json): exit 0. The dependency closure was rebuilt first with pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, exit 0. --listFiles on tsconfig.test.json lists the new pin file.
  • The targeted set (the new pin file, CelPredicateField.test.tsx, flow-scope.test.ts, both entryCondition suites, the ConditionBuilder.* suites): Test Files 14 passed (14), Tests 227 passed (227), exit 0.
  • At 1bbfc4b (before the merge of main): pnpm exec vitest run packages/app-shell/ gave Test Files 1078 passed | 1 skipped (1079), Tests 10616 passed | 9 skipped (10625), exit 0. main's app-shell changes since then (objectui#11783, objectui#11811) touch none of these files. The package-wide run on the merged head is CI's.
  • pnpm exec eslint on the 8 touched source and test files: 0 errors, 8 warnings, all on lines outside this diff.
  • pnpm check:control-bytes, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check:new-line-citations, plus scripts/check-changeset-presence.mjs and scripts/check-changeset-no-major.mjs: all exit 0 on b097f9a. The i18n gates are not applicable: no locale pack or catalogue row changed.

Ablation, run at 1bbfc4b through ablation-replace (the anchor must hit, and the restore is proven against HEAD). The tests import the subjects by relative source path, so there is no dist leg.

  • scope: put if (!onStart) back on the record push in flow-scope.ts. Anchor 1 → 0, blob 3180f147cab2 → 65c6870b91c0. Result 8 failed / 51 passed: the card's pin, the Start-node pin and the 6 record-trigger rows of the table. Restored: blob == HEAD 3180f147cab2, git diff HEAD empty.
  • one verdict: changed issues.length === 0 && !scopeIssue; to issues.length === 0; in CelPredicateField.tsx. Blob 51ef56396d7d → e2187dea09eb. Result 2 failed / 57 passed: the trigger.status pin and the editor-contract pin. Restored: blob == HEAD 51ef56396d7d, diff empty.

Both went red, as predicted.

Clause-② (no), measured on the built package. A transitive walk of dist/index.d.ts's relative imports reaches 172 declaration files. None of CelPredicateField, ConditionBuilder, FlowNodeConfigField, flow-scope, flow-ref-check, FlowExprIssue or useFlowScope is among them. The positive control DirectoryPage.d.ts is reached. scopeIssue is in the emitted CelPredicateField.d.ts and ConditionBuilder.d.ts, and in none of the reachable files. exports declares only . and ./styles.css.

Overlap

Per the seat's claim amendment, objectui#11788 may edit FlowNodeConfigField.tsx in another region (the notify Recipients and field-mapping rows). This PR's edit there is the entry condition's scope verdict, the scopeIssue handed to its ConditionBuilder, and the FLOW_TRIGGER_CONTEXT_SUBJECTS comment. Whoever lands second merges main.

Acceptance notes

Noted, not filed. Neither one gives a wrong verdict at a public door today.

  • time_relative Start nodes get no trigger scope. The engine's time-relative sweep hands each matched row to the run as record. flow-scope.ts gives time_relative no trigger scope: the type is not in RECORD_TRIGGER_TYPES, and its object is at config.timeRelative.object, not config.objectName. The effect is silent today. With no declared variable, the ref check has no roots and says nothing. The shipped producer (app-showcase's showcase_task_due_reminder, with {record.title} templates) declares none. A flow that also declared a variable would see record flagged. Carrier: none.
  • Four record-trigger tokens are not in Studio's set. The record-change trigger accepts record-(before|after)-(create|insert|update|delete|write), and RECORD_TRIGGER_TYPES lists 6 of those 10 tokens. A grep over objectstack main (15ec50e5) examples and package sources finds no producer of the other four (before-create, before-insert, after-insert, before-delete). The control, the same grep for record-after-update, hits 15 times in 3 example files. Studio's trigger select does not offer them either. Carrier: none.

Changeset: .changeset/11789-cel-scope-verdict.md, patch on @object-ui/app-shell.

Implemented by the os-dev run under session https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8, dispatched by the domain:ui seat 3 claim on the card.


Generated by Claude Code

claude added 2 commits October 8, 2026 00:10
…cope the engine binds, with one verdict (objectui#11789)

The flow designer's Start node left the whole `record` out of the scope it
checks an entry condition against, while the engine binds `record` beside the
flattened fields before it runs the start-condition gate. So a valid
`record.status == 'done' && previous.status != 'done'` read "Valid CEL" and
"`record` is not a reference in scope at this step." at once.

- flow-scope: `record` is in scope at every node of a record-triggered flow,
  the Start node included; `previous` follows the engine's pre-image (update,
  create-or-update, and now delete; not create, where it is only `null`).
- one verdict: when the field's scope check names an out-of-scope reference,
  the raw CEL editor withholds "Valid CEL" (CelPredicateField `scopeIssue`,
  forwarded by ConditionBuilder from FlowNodeConfigField).

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3526.0 KB 3551.8 KB
Main entry chunk (gzip) 157.0 KB 350 KB
Entry file index-BaMXY_w3.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 582.41KB 140.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
fields (index.js) 266.88KB 67.46KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 38.37KB 10.31KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.82KB 39.17KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 239.75KB 65.92KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 117.42KB 29.32KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 01:36
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 01:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 5aa7f55 Oct 8, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11789-cel-scope-verdict branch October 8, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

studio(cel editor): an entry condition using record.x shows "Valid CEL" and "record is not a reference in scope" at the same time

2 participants