Skip to content

Shipped template formula fields silently evaluate to null on @objectstack 15.1.1 — daysBetween / Timestamp−Timestamp / floor in stored formulas (hr tenure_years, time_off days) #3306

Description

@os-zhuang

Summary

While converting the templates repo's roll-ups (framework#1867 follow-up) I boot-verified several stored Field.formula fields that silently evaluate to null on the pinned runtime (@objectstack/formula@15.1.1). No parse error, no runtime error, no objectstack build warning — the field just returns null. This is the exact silent-miss class ADR-0053 / ADR-0032 exist to kill, but for function calls / arithmetic, not the == today() equality case that #3183 just fixed.

Two of these are real, user-facing fields in the shipped hr template:

Field Expression (origin/main) Runtime value (15.1.1)
hr_time_off_request.days (record.end_date - record.start_date) + 1 null for every request
hr_employee.tenure_years floor((today() - record.hire_date) / 365) null for every employee

hr_time_off_request.days is the leave-duration in the core time-off workflow — every seeded request shows a blank "Calendar Days".

Verified repro

Fresh objectstack dev --fresh on hr (@objectstack 15.1.1), signed in, GET /api/v1/data/...:

  • hr_time_off_request.daysnull (all 6 seeded rows).
  • hr_employee.tenure_yearsnull (all 7 rows), and I could not get a non-null result with any of:
    • floor((today() - hire_date) / 365) (original)
    • daysBetween(record.hire_date, today()) ← bare, still null
    • daysBetween(date(record.hire_date), today()) ← explicit coercion, still null
    • daysBetween(...) / 365, daysBetween(...) / round(365) ← still null

By contrast, today(), daysFromNow(n), addDays(date, n), round, abs, and date ordering comparisons all work in 15.1.1 (I used daysFromNow/addDays to fix two sibling formulas — see below).

What appears to be going on

daysBetween is correct in framework HEAD — implemented in packages/formula/src/stdlib.ts:176, listed in CEL_STDLIB_FUNCTIONS (validate.ts:446), and engine-tested (cel-engine.test.ts:449,456, including a date-string field arg). But calling it from a stored formula field on the published 15.1.1 returns null, and templates pin 15.1.1 (the latest published — there is nothing newer to bump to). So either the working daysBetween stored-formula path never shipped in 15.1.1, or the end-to-end stored-formula path has a gap the engine-level unit tests don't cover. Two related operators have no support at all and also null out silently: Timestamp − Timestamp (→ a number) and a floor builtin — both things template authors reach for naturally.

Related prior work: #1302 (original "missing daysBetween/date-arithmetic", closed not_planned then implemented via ADR-0053), #1979 (thread ctx into applyFormulaPlan), #3183 (silent dateField == today(), fixed 2026-07-18). This is the same silent-null family, one layer over: function/arith calls in stored formulas rather than equality.

Ask

  1. Confirm end-to-end + release: add a test that a stored formula field (via engine.find()applyFormulaPlan, real driver YYYY-MM-DD round-trip) using daysBetween returns the right value, and make sure it ships in the next published @objectstack/formula. Then templates can restore time_off.days / tenure_years.
  2. Fail loudly, don't null: an unresolved function / unsupported operator in a formula should be a build-time error (like Bare dateField == today() silently returns false — fix via AST temporal-comparison rewrite in the CEL engine #3183's philosophy — cel-to-filter already fails closed), not a silent runtime null. Silent null is the trap here.
  3. Template regression coverage: nothing currently catches that a shipped template's formula field evaluates to null on the pinned release — a boot-and-assert-non-null smoke over template formula fields would have caught all of these.

What I already shipped template-side (using functions that DO work in 15.1.1)

Same root class, fixable without daysBetween, so done in templates:

  • compliance_control.is_overdue_for_review: last_assessed_at + freq*86400000addDays(last_assessed_at, freq) < today() (templates#92, merged).
  • hr_document.is_expiring_soon / expiry_status: today() + 30/60daysFromNow(30/60) (templates#93).

time_off.days and tenure_years are left for this framework fix — they need a working daysBetween (or Timestamp subtraction), which 15.1.1 doesn't provide in stored formulas.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions