Skip to content

Commit 59a414c

Browse files
committed
feat(spec): D3 entry flow-cel-record-variables-alias-retired, registry regenerated
The changeset's FROM -> TO remedy for flow CEL `record.X` on a run with no record is a prescription for metadata authors, so it lives in the ADR-0087 ledger. Semantic-only: whether a run holds a record depends on the entrances that start the flow, which the flow alone does not show. registry.ts is regenerated by gen:migration-registry, not edited by hand. Claude-Session: https://claude.ai/code/session_013j5gkUCpqQiti4GgPqqmnt Co-authored-by: Claude <noreply@anthropic.com>
1 parent d9ebbee commit 59a414c

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
// A flow CEL expression's record root is the record the run was handed, or
6+
// unbound. The flow engine used to bind record to the run's own variables
7+
// whenever the run held no record, so record.assignee silently read a flow
8+
// variable named assignee. That spelling now fails the run with an unknown-
9+
// variable fault on record. Semantic-only: whether a run holds a record depends
10+
// on the entrances that start the flow (a trigger, an action, a parent flow, a
11+
// map item), which are not visible from the flow alone, so no D2 conversion can
12+
// rewrite the source mechanically.
13+
export const entry: SemanticMigration = {
14+
id: 'flow-cel-record-variables-alias-retired',
15+
// No backticks in `surface` — build-upgrade-guide renders it inside a code
16+
// span already, and a nested backtick would close it.
17+
surface:
18+
'flows[].nodes[].config.condition, flows[].edges[].condition, a decision node\'s conditions[].expression, a '
19+
+ 'screen node\'s fields[].visibleWhen, and the CEL value envelopes of an assignment node\'s assignments and of '
20+
+ 'a create_record or update_record node\'s fields — a CEL source reading record.X, or record bare, on a run '
21+
+ 'that holds no record, where X names a flow variable',
22+
replacement:
23+
'the variable by its name, or through the vars namespace: record.assignee becomes assignee, or vars.assignee. '
24+
+ 'A record.X read on a run that was handed a record is unchanged, and reads that record\'s field X',
25+
reason:
26+
'Flow CEL binds record to the record the run was handed: a record-change trigger\'s row, a time-relative '
27+
+ 'sweep\'s row, the inbound hook\'s request body, a flow action\'s record (the loaded row, or an empty record '
28+
+ 'carrying at most the id it was given), a subflow or map parent\'s record, or a map item that carries an id. '
29+
+ 'A flow can also bind a variable named record itself. A run with none of these, one started through the REST '
30+
+ 'trigger route, a declared endpoint or a cron schedule, or a child of such a run, used to see record bound to '
31+
+ 'its own variables map, so record.X answered a flow variable named X instead of failing: a second spelling of '
32+
+ 'every variable, which hid the author\'s mistake. Such a run now fails the expression with an unknown-variable '
33+
+ 'fault on record, carrying the source, as every other unbound root does. No D2 conversion exists: whether a '
34+
+ 'run holds a record depends on the entrances that start the flow, which the flow alone does not show, so '
35+
+ 'only the author can tell a variable read from a record field read. Measured at the change: 0 flows in this '
36+
+ 'repository\'s example apps, platform objects and dogfood suites, and 0 in objectstack-ai/hotcrm, read record '
37+
+ 'with no record entrance.',
38+
acceptanceCriteria:
39+
'For each flow whose CEL reads record, list the entrances that start it. Where any of them hands no record, '
40+
+ 'replace each record.X that names a flow variable with the variable\'s name or vars.X. Re-run each flow path '
41+
+ 'started with no record and confirm every expression evaluates instead of failing with an unknown-variable '
42+
+ 'fault on record; re-run a path started with a record and confirm record.X still reads that record\'s field.',
43+
};

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13793,6 +13793,45 @@ const step18: MigrationStep = {
1379313793
+ 'number or boolean slot of any other builtin it fails at its first run exactly as it did, so write a '
1379413794
+ 'literal there.',
1379513795
},
13796+
// A flow CEL expression's record root is the record the run was handed, or
13797+
// unbound. The flow engine used to bind record to the run's own variables
13798+
// whenever the run held no record, so record.assignee silently read a flow
13799+
// variable named assignee. That spelling now fails the run with an unknown-
13800+
// variable fault on record. Semantic-only: whether a run holds a record depends
13801+
// on the entrances that start the flow (a trigger, an action, a parent flow, a
13802+
// map item), which are not visible from the flow alone, so no D2 conversion can
13803+
// rewrite the source mechanically.
13804+
{
13805+
id: 'flow-cel-record-variables-alias-retired',
13806+
// No backticks in `surface` — build-upgrade-guide renders it inside a code
13807+
// span already, and a nested backtick would close it.
13808+
surface:
13809+
'flows[].nodes[].config.condition, flows[].edges[].condition, a decision node\'s conditions[].expression, a '
13810+
+ 'screen node\'s fields[].visibleWhen, and the CEL value envelopes of an assignment node\'s assignments and of '
13811+
+ 'a create_record or update_record node\'s fields — a CEL source reading record.X, or record bare, on a run '
13812+
+ 'that holds no record, where X names a flow variable',
13813+
replacement:
13814+
'the variable by its name, or through the vars namespace: record.assignee becomes assignee, or vars.assignee. '
13815+
+ 'A record.X read on a run that was handed a record is unchanged, and reads that record\'s field X',
13816+
reason:
13817+
'Flow CEL binds record to the record the run was handed: a record-change trigger\'s row, a time-relative '
13818+
+ 'sweep\'s row, the inbound hook\'s request body, a flow action\'s record (the loaded row, or an empty record '
13819+
+ 'carrying at most the id it was given), a subflow or map parent\'s record, or a map item that carries an id. '
13820+
+ 'A flow can also bind a variable named record itself. A run with none of these, one started through the REST '
13821+
+ 'trigger route, a declared endpoint or a cron schedule, or a child of such a run, used to see record bound to '
13822+
+ 'its own variables map, so record.X answered a flow variable named X instead of failing: a second spelling of '
13823+
+ 'every variable, which hid the author\'s mistake. Such a run now fails the expression with an unknown-variable '
13824+
+ 'fault on record, carrying the source, as every other unbound root does. No D2 conversion exists: whether a '
13825+
+ 'run holds a record depends on the entrances that start the flow, which the flow alone does not show, so '
13826+
+ 'only the author can tell a variable read from a record field read. Measured at the change: 0 flows in this '
13827+
+ 'repository\'s example apps, platform objects and dogfood suites, and 0 in objectstack-ai/hotcrm, read record '
13828+
+ 'with no record entrance.',
13829+
acceptanceCriteria:
13830+
'For each flow whose CEL reads record, list the entrances that start it. Where any of them hands no record, '
13831+
+ 'replace each record.X that names a flow variable with the variable\'s name or vars.X. Re-run each flow path '
13832+
+ 'started with no record and confirm every expression evaluates instead of failing with an unknown-variable '
13833+
+ 'fault on record; re-run a path started with a record and confirm record.X still reads that record\'s field.',
13834+
},
1379613835
// A flow CEL expression is refused when it reads a root the flow does not bind.
1379713836
// Formulas, row-level security and the client bind the run's user as user,
1379813837
// ctx.user and os.user as well as current_user; flow CEL binds current_user only,

0 commit comments

Comments
 (0)