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

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions