flowable/flowable-engine · error · FlowableException

Timer expression '${timerEventListener.getTimerExpression()}

Error message

Timer expression '${timerEventListener.getTimerExpression()}' did not resolve to java.util.Date, org.joda.time.DateTime, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime or an ISO8601 date/duration/repetition string or a cron expression for ${planItemInstance}

What it means

A timer event listener's timer expression evaluated, but the resulting value is not a supported date/time type or ISO8601/cron string, so no due date could be derived. Flowable accepts Date, Joda DateTime, Instant, LocalDate, LocalDateTime, ISO8601 date/duration/repetition strings, or cron expressions.

Source

Thrown at modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/behavior/impl/TimerEventListenerActivityBehaviour.java:157

                    } catch (Exception pe) { }

                }

            } else if (timerValue instanceof Instant) {
                timerDueDate = Date.from((Instant) timerValue);

            } else if (timerValue instanceof LocalDate) {
                timerDueDate = Date.from(((LocalDate) timerValue).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());

            } else if (timerValue instanceof LocalDateTime) {
                timerDueDate = Date.from(((LocalDateTime) timerValue).atZone(ZoneId.systemDefault()).toInstant());

            }
        }

        if (timerDueDate == null) {
            throw new FlowableException("Timer expression '" + timerEventListener.getTimerExpression() + "' did not resolve to java.util.Date, org.joda.time.DateTime, "
                    + "java.time.Instant, java.time.LocalDate, java.time.LocalDateTime or "
                    + "an ISO8601 date/duration/repetition string or a cron expression for " + planItemInstance);
        }

        scheduleTimerJob(commandContext, planItemInstance, timerValue, timerDueDate, isRepeating);
    }

    protected boolean timerJobForPlanItemInstanceExists(CommandContext commandContext, PlanItemInstanceEntity planItemInstance) {

        // For the same plan item, only one timer job can ever be active at any given time.
        // Since the DefaultJobManager creates a new timer job on repeat, we need to make sure
        // we're not creating duplicate timers on the create or initiate transition (which does need to happen on the first repeat).
        //
        // The alternative implementation would be to move the repeating timer creation to the onStateTransition on occur,
        // but this would also require similar logic to look up the previous timer job, as the previous repeat value is needed to calculate the next.

        CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext);
        List<TimerJobEntity> jobsByScopeIdAndSubScopeId = cmmnEngineConfiguration.getJobServiceConfiguration().getTimerJobEntityManager()

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Fix the expression/variable so it resolves to one of the supported types, or an ISO8601 duration/date (e.g. PT1H, 2026-09-10T12:00:00Z) or cron string
  2. If using a String variable, parse it in Java to java.util.Date or java.time type and store that as the variable
  3. Log the resolved timerValue at runtime (or evaluate the expression in a test) to see the actual type/value
  4. For repetition, use valid ISO8601 repetition format like R/PT10M or R10/PT10M

Example fix

// before
runtimeService.setVariable(caseInstanceId, "due", "10/09/2026");
// after
runtimeService.setVariable(caseInstanceId, "due", java.time.LocalDateTime.of(2026, 9, 10, 12, 0));
Defensive patterns

Strategy: validation

Validate before calling

const supported = [Date, java.time.Instant, java.time.LocalDateTime, java.time.LocalDate];
const v = runtimeService.getVariable(caseInstanceId, timerVar);
if (typeof v === 'string' && !/^P|^R\d*\/|^\d{4}-\d{2}-\d{2}/.test(v)) throw new Error('timer value not ISO8601/cron: ' + v);

Type guard

function isSupportedTimerValue(v) { return v instanceof Date || v instanceof java.time.Instant || v instanceof java.time.temporal.Temporal || (typeof v === 'string' && /^[PR]/.test(v)); }

Try / catch

try {
  cmmnRuntimeService.startCaseInstance(...);
} catch (e) {
  if (e.getMessage?.().includes("did not resolve to java.util.Date")) { /* fix timer variable, restart */ }
  else throw e;
}

Prevention

When it happens

Trigger: handleCreateTransition runs on plan item creation and the expression (e.g. ${dueDate} variable or literal) resolves to a String not in ISO8601/cron format, or to some other object type (Integer, custom bean).

Common situations: Case variable holding a wrongly formatted date string like 'tomorrow' or '10-11-2026'; expression pointing to the wrong variable; timezone-dependent formats; version change where new types (cron) are expected but the string is invalid.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/f2a127e1a33e6244. Report an issue: GitHub.