Found while implementing #643, deliberately left out of its scope.
What was measured
@objectstack/service-automation@17.0.0-rc.1 evaluates a flow condition in exactly three places:
- the start node's
config.condition — AutomationEngine.execute;
- a
decision node's config.conditions — the PLURAL, an array of { label, expression }, read by the decision node executor as const conditions = config?.conditions ?? [];
- every out-edge's
condition — AutomationEngine.traverseNext.
Every decision node in this repo authors the singular config.condition instead:
{
id: 'check_campaign_open', type: 'decision', label: 'Campaign Open?',
config: { condition: P`vars.campaignRecord.status == "planning"` },
}
That key is never read. The executor finds conditions empty, returns branchLabel: 'default' immediately, and the branch is decided entirely by the edge copies of the same predicate.
Affected sites (grep type: 'decision' under src/flows/): campaign_enrollment (check_campaign_open, check_not_enrolled), lead_conversion (decision_account, decision_contact, decision_opportunity), quote_generation (check_stage), opportunity_approval + _on_create (check_high_value), contract_renewal (check_notice_window, check_not_reminded, check_auto_renewal, check_no_open_renewal), demo_bootstrap (has_user), opportunity_stagnation (check_not_nudged), forecast_snapshot (has_deals, check_missing), and the record-change decision nodes lead_assignment (check_hot) / case_escalation / etc.
Why it matters even though behaviour is correct today
Behaviour is correct only because every one of these predicates is duplicated onto the out-edges. Nothing enforces that duplication:
The has() guards added in #643 were applied to both copies for this reason, and test/flow-variable-conditions.test.ts sweeps node and edge conditions alike. But nothing yet asserts the two copies agree.
What to establish
- Is the singular
config.condition on a decision node meant to be authorable at all? FlowNodeSchema.config is an open Record<string, unknown>, so it neither validates nor rejects the key — check whether Studio's designer form emits it, and whether the plural form is the only supported shape.
- If the singular form is legacy: either (a) convert every
decision node to config.conditions: [{ label, expression }] so the node is the live site and the edges follow its labels, or (b) drop the node-level condition entirely and let the edges be the single source of truth. These are different architectures — (a) makes the node authoritative, (b) makes it edge-only — and the choice affects every flow in the repo.
- Whichever way it goes, add the assertion that is missing today: for a
decision node, its condition and its out-edge conditions must not drift.
Do not assume the answer from the other three surfaces — that is the mistake #633 was opened to prevent.
Filed unassigned.
Found while implementing #643, deliberately left out of its scope.
What was measured
@objectstack/service-automation@17.0.0-rc.1evaluates a flow condition in exactly three places:config.condition—AutomationEngine.execute;decisionnode'sconfig.conditions— the PLURAL, an array of{ label, expression }, read by thedecisionnode executor asconst conditions = config?.conditions ?? [];condition—AutomationEngine.traverseNext.Every
decisionnode in this repo authors the singularconfig.conditioninstead:That key is never read. The executor finds
conditionsempty, returnsbranchLabel: 'default'immediately, and the branch is decided entirely by the edge copies of the same predicate.Affected sites (grep
type: 'decision'undersrc/flows/):campaign_enrollment(check_campaign_open,check_not_enrolled),lead_conversion(decision_account,decision_contact,decision_opportunity),quote_generation(check_stage),opportunity_approval+_on_create(check_high_value),contract_renewal(check_notice_window,check_not_reminded,check_auto_renewal,check_no_open_renewal),demo_bootstrap(has_user),opportunity_stagnation(check_not_nudged),forecast_snapshot(has_deals,check_missing), and the record-change decision nodeslead_assignment(check_hot) /case_escalation/ etc.Why it matters even though behaviour is correct today
Behaviour is correct only because every one of these predicates is duplicated onto the out-edges. Nothing enforces that duplication:
in [...]conditions are untranslatable, sona_sales_team/eu_sales_teamget no access at all #621 (sharing) / Flow start conditions carry no has() guards — measure whether the abort-and-skip class of #630 reaches them #633 (record-change conditions) each turned out to be. The mechanism differs again — this one is not a runtime abort or a silent skip, it is inert metadata — which is one more reason not to reason about it by analogy.The
has()guards added in #643 were applied to both copies for this reason, andtest/flow-variable-conditions.test.tssweeps node and edge conditions alike. But nothing yet asserts the two copies agree.What to establish
config.conditionon adecisionnode meant to be authorable at all?FlowNodeSchema.configis an openRecord<string, unknown>, so it neither validates nor rejects the key — check whether Studio's designer form emits it, and whether the plural form is the only supported shape.decisionnode toconfig.conditions: [{ label, expression }]so the node is the live site and the edges follow its labels, or (b) drop the node-level condition entirely and let the edges be the single source of truth. These are different architectures — (a) makes the node authoritative, (b) makes it edge-only — and the choice affects every flow in the repo.decisionnode, its condition and its out-edge conditions must not drift.Do not assume the answer from the other three surfaces — that is the mistake #633 was opened to prevent.
Filed unassigned.