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
#633 made every record.x / previous.x read in a record-change flow condition total, and pinned it with test/flow-condition-totality.test.ts. That sweep is explicitly filtered to the trigger record:
Conditions over flow-local variables were left alone, because they are a different shape with a different failure mode and measuring them properly is its own job. They are:
The mechanism #633 measured applies unchanged — same AutomationEngine.evaluateCondition, same strict CEL, same consequence (evaluateCondition throws, execute records the run failed, the automation does not happen). Only the source of the sparse shape differs, and there are two distinct ones:
get_record outputs are driver rows. A get_record node resolves through data.findOne, which on driver-memory / driver-mongodb returns the stored document with absent columns genuinely missing — exactly the shape that broke case_escalation. oppRecord.amount and oppRecord.stage are required on crm_opportunity today so they are always written, but campaign_enrollment's vars.campaignRecord.status has not been checked.
Unset flow variables are a different failure.vars.matchedAccount != null reads a variable that a preceding node may never have assigned. Measured on the engine's evaluator, an unset key aborts the same way an absent record column does (No such key: matchedAccount) — but the remedy is not a has() guard, it is declaring the variable with a default so it is always bound. Guarding here would paper over a flow-graph defect rather than fix it.
For each vars.* condition: can the referenced variable be unbound on any path through the graph? Note lead_conversion and campaign_enrollment are screen flows and demo_bootstrap is a schedule flow, so none is reachable through the record-change trigger — the shapes reaching them are different and must be measured, not assumed.
Decide whether test/flow-condition-totality.test.ts's sweep should widen to these scopes, or whether unset variables want a separate structural check ("every variable a condition reads is declared in flow.variables").
Do not inherit #633's conclusion here — that is the mistake #633 itself was opened to prevent. See the house-rule block at the top of test/flow-condition-totality.test.ts for the measured mechanism table, and test/sharing-seeding.test.ts for the surface where has() is actively wrong.
Found while implementing #633, deliberately left out of its scope.
What #633 covered, and what it did not
#633 made every
record.x/previous.xread in a record-change flow condition total, and pinned it withtest/flow-condition-totality.test.ts. That sweep is explicitly filtered to the trigger record:Conditions over flow-local variables were left alone, because they are a different shape with a different failure mode and measuring them properly is its own job. They are:
opportunity_approval(+_on_create)oppRecord.amount > 500000(nodecheck_high_value, edgese5/e6)get_recordoutputquote_generationoppRecord.stage == "prospecting" || …(nodecheck_stage, edgese4a/e4b)get_recordoutputcampaign_enrollmentvars.campaignRecord.status == "planning" || …(nodecheck_campaign_open, edgee4)get_recordoutputlead_conversionvars.matchedAccount != null,vars.matchedContact != null,vars.createOpportunity == true(+ edgese5/e6/e11/e12/e16/e17)demo_bootstrapvars.firstUser != null(+ edgese2/e_nouser)Why they may be exposed
The mechanism #633 measured applies unchanged — same
AutomationEngine.evaluateCondition, same strict CEL, same consequence (evaluateConditionthrows,executerecords the runfailed, the automation does not happen). Only the source of the sparse shape differs, and there are two distinct ones:get_recordoutputs are driver rows. Aget_recordnode resolves throughdata.findOne, which ondriver-memory/driver-mongodbreturns the stored document with absent columns genuinely missing — exactly the shape that brokecase_escalation.oppRecord.amountandoppRecord.stagearerequiredoncrm_opportunitytoday so they are always written, butcampaign_enrollment'svars.campaignRecord.statushas not been checked.Unset flow variables are a different failure.
vars.matchedAccount != nullreads a variable that a preceding node may never have assigned. Measured on the engine's evaluator, an unset key aborts the same way an absent record column does (No such key: matchedAccount) — but the remedy is not ahas()guard, it is declaring the variable with a default so it is always bound. Guarding here would paper over a flow-graph defect rather than fix it.What to establish
get_record-backed condition: is every field it readsrequiredor defaulted on the target object? If not, it has Flow start conditions carry no has() guards — measure whether the abort-and-skip class of #630 reaches them #633's exposure and wants Flow start conditions carry no has() guards — measure whether the abort-and-skip class of #630 reaches them #633's guards.vars.*condition: can the referenced variable be unbound on any path through the graph? Notelead_conversionandcampaign_enrollmentarescreenflows anddemo_bootstrapis ascheduleflow, so none is reachable through the record-change trigger — the shapes reaching them are different and must be measured, not assumed.test/flow-condition-totality.test.ts's sweep should widen to these scopes, or whether unset variables want a separate structural check ("every variable a condition reads is declared inflow.variables").Do not inherit #633's conclusion here — that is the mistake #633 itself was opened to prevent. See the house-rule block at the top of
test/flow-condition-totality.test.tsfor the measured mechanism table, andtest/sharing-seeding.test.tsfor the surface wherehas()is actively wrong.Filed unassigned.