n8n-io/n8n · error · InvalidScheduleError

Failed to evaluate cron expression ${JSON.stringify(cronExpr

Error message

Failed to evaluate cron expression ${JSON.stringify(cronExpression)} in timezone ${JSON.stringify(timezone)}: ${(error as Error).message}

What it means

Thrown by parseCron when CronExpressionParser.parse fails AT EVALUATION TIME (with currentDate and tz bound), as opposed to error 936 which fires at static validation. parseCron is the cursor factory used by nextRun helpers; wrapping here adds the timezone and expression context that the raw parser error lacks.

Source

Thrown at packages/@n8n/scheduler/src/core/recurrence/kinds/cron.ts:90

 * A cursor over the cron fires strictly after `after`, in the given IANA
 * timezone. `cron-parser` resolves DST via luxon (wall-clock): a nonexistent
 * local time (spring-forward) shifts forward, a repeated one (fall-back) fires
 * once.
 * @param cronExpression The cron expression to evaluate.
 * @param after The instant the cursor starts firing after (strictly).
 * @param timezone IANA zone the expression is evaluated in.
 * @returns A cursor whose `next()` yields successive fires.
 * @throws {InvalidScheduleError} When the expression cannot be parsed.
 */
export function parseCron(
	cronExpression: CronExpression,
	after: Date,
	timezone: string,
): CronCursor {
	try {
		return CronExpressionParser.parse(cronExpression, { currentDate: after, tz: timezone });
	} catch (error) {
		throw new InvalidScheduleError(
			`Failed to evaluate cron expression ${JSON.stringify(cronExpression)} in timezone ${JSON.stringify(timezone)}: ${(error as Error).message}`,
		);
	}
}

/**
 * The next cron fire strictly after `after`.
 * @param schedule The cron schedule. A `recurring_cron` works too: its first
 * fire ignores the "every N" rule (there is no previous fire to count from yet).
 * @param after The instant to fire after.
 * @returns The matching instant; cron is unbounded, so this is never `null`.
 */
export function cronNextRun(schedule: CronSchedule | RecurringCronSchedule, after: Date): Date {
	return parseCron(schedule.cronExpression, after, resolvedTimezone(schedule)).next().toDate();
}

/**
 * Every cron fire starting at `first`, oldest first.

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Read the wrapped error - it carries both expression and timezone.
  2. If the failure is DST-related, pass an `after` that is not exactly on a transition instant, or evaluate in UTC and convert.
  3. Re-validate with validateCron first to surface the cleaner error 936 message before reaching parseCron.
  4. Confirm the timezone string is still a valid IANA zone at evaluation time.

Example fix

// before
const cursor = parseCron(expr, new Date('2024-03-31T02:30:00'), 'Europe/Berlin'); // DST gap -> throws

// after - evaluate in UTC, then convert fires to the target zone
const cursor = parseCron(expr, new Date('2024-03-31T02:30:00'), 'UTC');
const fireUtc = cursor.next().toDate();
const fireLocal = DateTime.fromJSDate(fireUtc, { zone: 'Europe/Berlin' }).toJSDate();
Defensive patterns

Strategy: try-catch

Validate before calling

function safeParseCron(expr: string, after: Date, timezone: string) {
  try {
    return parseCron(expr, after, timezone);
  } catch (e) {
    // Fall back to UTC evaluation, then convert - sidesteps DST-gap failures.
    const utc = parseCron(expr, after, 'UTC');
    return utc; // caller converts fires back to target zone
  }
}

Try / catch

try {
  const cursor = parseCron(cronExpression, after, timezone);
} catch (e) {
  if (e instanceof InvalidScheduleError && /Failed to evaluate/.test(e.message)) {
    // Likely DST or zone-specific; retry in UTC and convert fires.
    const utcCursor = parseCron(cronExpression, after, 'UTC');
    return convertFires(utcCursor, timezone);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling parseCron(cronExpression, after, timezone) where the expression is parseable in isolation but fails when bound to a specific currentDate/timezone - typically because the parser rejects the combination (e.g. a 6-field seconds expression evaluated in a zone with DST transition at exactly `after`).

Common situations: A DST transition coinciding with the requested `after` instant; an expression that is valid syntactically but yields no fires in the configured timezone; a timezone that became invalid between validation and evaluation (rare).

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/b0e64d127fd92ead. Report an issue: GitHub.