n8n-io/n8n · error · InvalidScheduleError

Invalid cron expression ${JSON.stringify(expression)}: ${(er

Error message

Invalid cron expression ${JSON.stringify(expression)}: ${(error as Error).message}

What it means

Thrown by validateCron when CronExpressionParser.parse throws after the type, field-count, and timezone checks passed. The original parser error (e.g. 'Cannot use L/W modifiers with recurrent expressions') is wrapped with the offending expression so the user sees what was rejected.

Source

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

		);
	}

	const fieldCount = expression.trim().split(/\s+/).length;
	if (fieldCount < MIN_CRON_FIELD_COUNT || fieldCount > MAX_CRON_FIELD_COUNT) {
		throw new InvalidScheduleError(
			`Cron expression must have ${MIN_CRON_FIELD_COUNT} or ${MAX_CRON_FIELD_COUNT} fields (seconds optional), got ${fieldCount}: ${JSON.stringify(expression)}`,
		);
	}

	// A null timezone is the instance default, resolved by the caller.
	if (schedule.timezone !== null && !IANAZone.isValidZone(schedule.timezone)) {
		throw new InvalidScheduleError(`Unknown IANA timezone: ${JSON.stringify(schedule.timezone)}`);
	}

	try {
		CronExpressionParser.parse(expression, { tz: schedule.timezone ?? 'UTC' });
	} catch (error) {
		throw new InvalidScheduleError(
			`Invalid cron expression ${JSON.stringify(expression)}: ${(error as Error).message}`,
		);
	}
}

/**
 * The schedule's timezone as a concrete IANA zone.
 * @param schedule The cron (or recurring_cron) schedule.
 * @returns The resolved timezone.
 * @throws {InvalidScheduleError} When the timezone is still `null` (the instance
 * default should have been resolved upstream).
 */
export function resolvedTimezone(schedule: CronSchedule | RecurringCronSchedule): string {
	if (schedule.timezone === null) {
		throw new InvalidScheduleError(
			'Cron timezone must be resolved to a concrete zone before computing the next run, got null',
		);
	}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Read the wrapped parser message - it identifies the bad field and value.
  2. Use only standard unix cron syntax: 0-59 minutes, 0-23 hours, 1-31 dom, 1-12 month, 0-6 dow.
  3. For 'last weekday of month' semantics, check the parser's supported modifier set before using L/W/#.
  4. Pre-test expressions with a tool like crontab.guru before persisting.

Example fix

// before
validateCron({ kind: 'cron', cronExpression: '60 0 * * *', timezone: 'UTC' }); // minute 60 -> throws

// after - clamp field into range
validateCron({ kind: 'cron', cronExpression: '0 0 * * *', timezone: 'UTC' }); // every day at midnight
Defensive patterns

Strategy: validation

Validate before calling

import { CronExpressionParser } from 'cron-parser';

function preflightCron(expr: string): void {
  try {
    CronExpressionParser.parse(expr, { tz: 'UTC' });
  } catch (e) {
    throw new Error(`Cron preflight failed for ${JSON.stringify(expr)}: ${(e as Error).message}`);
  }
}

Try / catch

try {
  validateCron(schedule);
} catch (e) {
  if (e instanceof InvalidScheduleError) {
    // Surface to the user with the field/value; do NOT retry with auto-correction.
    return badRequest(`Invalid cron: ${e.message}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: A 5/6-field expression that is syntactically invalid for the parser: out-of-range values ('60 0 * * *'), unsupported modifiers for the parser config ('0 0 * * 5L'), or duplicated/unparseable tokens.

Common situations: User writes a cron variant the underlying parser doesn't support (quartz-only modifiers in a unix cron parser); copy-paste from a different cron dialect; hand-built expressions with off-by-one ranges.

Related errors


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