Skip to content

Flow start conditions carry no has() guards — measure whether the abort-and-skip class of #630 reaches them #633

Description

@os-zhuang

Found while implementing #630. Narrowed by the PM on 2026-08-02: the sharing-rule half of this issue is closed by measurement (see below) and only the flow-start-condition half remains open.

Settled: sharing rules must NOT get has() guards

The original suggested work said "if either surface is exposed, add has(...) guards". For sharing rules that is now known to be actively harmful. Measured in #621 / PR #637 with the platform's own compileCelToFilter — the function plugin-sharing calls at seed time:

has(record.type) && record.type == "customer"   →   unsupported operator "call"

Sharing conditions are compiled to pushdown filters stored in sys_sharing_rule.criteria_json, not interpreted per record. Function calls as a class are outside the translatable subset. So adding a has() guard to a sharing rule makes it untranslatable, which makes the seeder skip it — turning seeded: 9, skipped: 0 into seeded: 0, skipped: 9, with the same quiet signature as the bug #621 was filed for, amplified from 2 rules to 9.

Further, sharing rules likely need no totality guard at all: a missing column in a where clause is NULL-compared by the database, not an abort. The abort behaviour belongs to the CEL interpreter, and sharing conditions never reach it.

This conclusion is pinned as an assertion in test/sharing-seeding.test.ts, so an attempt to add guards there turns CI red rather than shipping inert rules.

Still open: flow start conditions (9)

case_escalation, case_escalation_on_create, opportunity_approval, opportunity_approval_on_create, lead_assignment.check_hot, case_csat_followup, contact_welcome, opportunity_won_alert, task_urgent_alert — all read record fields with no has(...).

Whether that matters is unmeasured, and must not be inherited from either of the other two surfaces. The validation path aborts and skips; the sharing path compiles to SQL and never interprets. Flow start conditions are a third mechanism and could behave like either, or neither.

What to establish:

  1. What record shape does a record-change trigger hand the start condition — the driver's post-write row (sparse on driver-memory / driver-mongodb), a re-read, or a merged {...previous, ...data}?
  2. Are start conditions interpreted or compiled? If compiled to a filter like sharing rules, guards are wrong here too, for the same reason.
  3. What happens on abort — does the flow silently not run (the same declared-≠-enforced shape), or does it error visibly?

Suggested work

  1. Measure (1)–(3) against driver-memory, which is already sparse and reproduces the condition without any setup.
  2. If flows are exposed: add has(...) guards in the two shapes Script validations are silently SKIPPED when the merged record lacks the field key (update path) #630 settled on, and extend the sweep in test/object-validation-predicates.test.ts to cover flow start conditions.
  3. If they are not exposed: change nothing, and record why — a comment naming the record shape and evaluation mechanism the flow path actually uses — so the next person does not re-measure it. A correct "no action needed, here is the evidence" outcome is a complete result for this issue, not a failure.

Background (from #630)

Strict CEL aborts the whole predicate on a key that is not present. Note record.f != null aborts too — the != null guards already in the repo protect against dyn< null > < int, not against an absent key.

predicate key absent key present as null
isBlank(record.f) abort true
record.f != null abort false
record.f == "v" abort false
has(record.f) false true
driver absent column comes back as
@objectstack/driver-sql key present, null
@objectstack/driver-sqlite-wasm key present, null
@objectstack/driver-memory key absent
@objectstack/driver-mongodb key absent

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingmetadataDeclarative metadata — schema, security posture, UI surfacespm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loopprio:p1Next in line once P0s clear

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions