From 59d708e3244f72a68b5db92ac283ce7931d7ff8c Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:01:25 +0530 Subject: [PATCH] fix(workflow): stop relative date filter from crashing on empty/invalid date values (#22384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem A workflow **Filter** step on a `DATE`/`DATE_TIME` field using `IS_RELATIVE` crashes with a `RangeError` when the referenced step output is empty or invalid. The empty value is coerced into an `Invalid Date` (`new Date("undefined")`), whose `.getTime()` is `NaN`, and `Temporal.Instant.fromEpochMilliseconds(NaN)` throws, failing the affected workflow runs. This is a latent regression from the Date → Temporal migration (#16544): the previous `date-fns` implementation silently returned `false` on an invalid date, but Temporal is strict and throws. The guard was never carried over. ## Fix Validate the coerced date once at the boundary in `evaluateDateFilter` — the single place arbitrary/empty step output is turned into a `Date`. An unparseable date now resolves to "does not match" for every comparison operand (`IS`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`, `IS_AFTER`, `IS_RELATIVE`), restoring the pre-migration contract. `IS_EMPTY` / `IS_NOT_EMPTY` are intentionally excluded so emptiness is still evaluated on the raw operand. ## Tests Added a parameterized regression test covering every date comparison operand with empty and missing step output, asserting no throw and a `false` result. Review in cubic --- .../evaluate-filter-conditions.util.spec.ts | 39 +++++++++++++++++++ .../utils/evaluate-filter-conditions.util.ts | 18 ++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts index e5ce98f4a9..ad7adc1acf 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/__tests__/evaluate-filter-conditions.util.spec.ts @@ -1042,6 +1042,45 @@ describe('evaluateFilterConditions', () => { true, ); }); + + it.each([ + ViewFilterOperand.IS, + ViewFilterOperand.IS_IN_PAST, + ViewFilterOperand.IS_IN_FUTURE, + ViewFilterOperand.IS_TODAY, + ViewFilterOperand.IS_BEFORE, + ViewFilterOperand.IS_AFTER, + ViewFilterOperand.IS_RELATIVE, + ])( + 'should not match and not throw when the left operand is an empty/invalid date (%s)', + (operand) => { + const relativeValue = JSON.stringify({ + direction: 'PAST', + amount: 1, + unit: 'DAY', + }); + + const emptyStepOutputFilter = createFilter( + operand, + '', + relativeValue, + 'DATE', + ); + const missingStepOutputFilter = createFilter( + operand, + undefined, + relativeValue, + 'DATE_TIME', + ); + + expect( + evaluateFilterConditions({ filters: [emptyStepOutputFilter] }), + ).toBe(false); + expect( + evaluateFilterConditions({ filters: [missingStepOutputFilter] }), + ).toBe(false); + }, + ); }); describe('currency operands', () => { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts index 05ded6949f..566ea37773 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/filter/utils/evaluate-filter-conditions.util.ts @@ -216,8 +216,20 @@ function evaluateBooleanFilter(filter: ResolvedFilter): boolean { function evaluateDateFilter(filter: ResolvedFilter): boolean { // TODO: refactor this with Temporal + if (filter.operand === ViewFilterOperand.IS_EMPTY) { + return !isDefined(filter.leftOperand) || filter.leftOperand === ''; + } + + if (filter.operand === ViewFilterOperand.IS_NOT_EMPTY) { + return isDefined(filter.leftOperand) && filter.leftOperand !== ''; + } + const dateLeftValue = new Date(String(filter.leftOperand)); + if (Number.isNaN(dateLeftValue.getTime())) { + return false; + } + switch (filter.operand) { case ViewFilterOperand.IS: return ( @@ -245,12 +257,6 @@ function evaluateDateFilter(filter: ResolvedFilter): boolean { new Date(String(filter.rightOperand)).getTime() ); - case ViewFilterOperand.IS_EMPTY: - return !isDefined(filter.leftOperand) || filter.leftOperand === ''; - - case ViewFilterOperand.IS_NOT_EMPTY: - return isDefined(filter.leftOperand) && filter.leftOperand !== ''; - case ViewFilterOperand.IS_RELATIVE: return parseAndEvaluateRelativeDateFilter({ dateToCheck: dateLeftValue,