Repository navigation
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 intoOct 8, 2026
Conversation
…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>
Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
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
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, "
recordis not a reference in scope at this step." for the same entry condition. Two defects were behind that one screen.record.resolveFlowScopeinflow-scope.tspushed the whole-recordrecordref onlyif (!onStart). The engine binds it there.seedRunVariablesin@objectstack/service-automationsetsrecord,$record, the record's flattened fields andprevious, and the start-condition gate then evaluates against that same variables map. Every shipped spelling resolves at run time:record.statusand barestatus.CelPredicateField, mounted byConditionBuilder) 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.statuspin and the editor pin failed on "Valid CEL" shown beside the note. The 3 that passed are the controls.The change
recordis in scope on a record-triggered Start nodeIn
flow-scope.ts, therecordref 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_TYPESplus anobjectName) is untouched.flow-ref-check.tsis not touched. Addingrecord/previousto itsRUNTIME_GLOBALSwould accept them on schedule and manual flows, where the engine binds no record.previousfollows the engine's pre-imageThe engine binds
previouson every run: to the pre-image the record-change trigger hands it, or tonullwhen there is none. So Studio scopes it where a pre-image exists.recordpreviousrecord-after-update,record-before-updaterecord-after-write,record-before-writenullon the create leg, the prior row on the update leg (previous == nullpicks the create leg)record-after-deleterecordfrom it toorecord-after-createnull: there is no prior rowschedule,manual,apiWhy create stays out (the seat's answer A to this PR's open question): a member read on
null, such asprevious.status, is a CEL evaluation error. The engine'sevaluateConditionthrows on it, so the run fails. Andprevious == nullis constantly true there. Showing the scope note is the useful verdict. The table is pinned row by row inflow-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 isFlowNodeConfigField's owndescribeUnknownRefsnote, rendered under the control it mounts.FlowExprIssuenever renders beside aCelPredicateField: only the Start node'sconditiondescriptor opts intoconditionBuilder. So the rule lands where both verdicts render:FlowNodeConfigFieldcomputes its scope verdict before it builds the control, and handsscopeIssueto theConditionBuilderit mounts;ConditionBuilderforwards the new optionalscopeIssueto its raw editor;CelPredicateFieldwithholds "Valid CEL" whenscopeIssueis set. The lint, its findings andonLintChangeare unchanged. With noscopeIssue, 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'sFLOW_TRIGGER_CONTEXT_SUBJECTSdoc comment saidflow-scope.tswithholdsrecordon the Start node, which this change makes false, so it was reworded. The builder still offers only the bare spelling, becauserecord.FIELDis 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 acceptsrecordtoo. That matches the engine:traverseNextevaluates 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 withmain455c646) unless stated.pnpm --filter @object-ui/app-shell type-check(echoedtsc --noEmit && tsc -p tsconfig.test.json): exit 0. The dependency closure was rebuilt first withpnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, exit 0.--listFilesontsconfig.test.jsonlists the new pin file.CelPredicateField.test.tsx,flow-scope.test.ts, bothentryConditionsuites, theConditionBuilder.*suites):Test Files 14 passed (14),Tests 227 passed (227), exit 0.1bbfc4b(before the merge ofmain):pnpm exec vitest run packages/app-shell/gaveTest 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 eslinton 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, plusscripts/check-changeset-presence.mjsandscripts/check-changeset-no-major.mjs: all exit 0 onb097f9a. The i18n gates are not applicable: no locale pack or catalogue row changed.Ablation, run at
1bbfc4bthroughablation-replace(the anchor must hit, and the restore is proven against HEAD). The tests import the subjects by relative source path, so there is nodistleg.if (!onStart)back on therecordpush inflow-scope.ts. Anchor 1 → 0, blob3180f147cab2→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 == HEAD3180f147cab2,git diff HEADempty.issues.length === 0 && !scopeIssue;toissues.length === 0;inCelPredicateField.tsx. Blob51ef56396d7d→e2187dea09eb. Result 2 failed / 57 passed: thetrigger.statuspin and the editor-contract pin. Restored: blob == HEAD51ef56396d7d, 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 ofCelPredicateField,ConditionBuilder,FlowNodeConfigField,flow-scope,flow-ref-check,FlowExprIssueoruseFlowScopeis among them. The positive controlDirectoryPage.d.tsis reached.scopeIssueis in the emittedCelPredicateField.d.tsandConditionBuilder.d.ts, and in none of the reachable files.exportsdeclares only.and./styles.css.Overlap
Per the seat's claim amendment, objectui#11788 may edit
FlowNodeConfigField.tsxin another region (the notify Recipients and field-mapping rows). This PR's edit there is the entry condition's scope verdict, thescopeIssuehanded to itsConditionBuilder, and theFLOW_TRIGGER_CONTEXT_SUBJECTScomment. Whoever lands second mergesmain.Acceptance notes
Noted, not filed. Neither one gives a wrong verdict at a public door today.
time_relativeStart nodes get no trigger scope. The engine's time-relative sweep hands each matched row to the run asrecord.flow-scope.tsgivestime_relativeno trigger scope: the type is not inRECORD_TRIGGER_TYPES, and its object is atconfig.timeRelative.object, notconfig.objectName. The effect is silent today. With no declared variable, the ref check has no roots and says nothing. The shipped producer (app-showcase'sshowcase_task_due_reminder, with{record.title}templates) declares none. A flow that also declared a variable would seerecordflagged. Carrier: none.record-(before|after)-(create|insert|update|delete|write), andRECORD_TRIGGER_TYPESlists 6 of those 10 tokens. A grep over objectstackmain(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 forrecord-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,patchon@object-ui/app-shell.Implemented by the os-dev run under session
https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8, dispatched by thedomain:uiseat 3 claim on the card.Generated by Claude Code