n8n-io/n8n · error · InvalidScheduleError

Unknown IANA timezone: ${JSON.stringify(schedule.timezone)}

Error message

Unknown IANA timezone: ${JSON.stringify(schedule.timezone)}

What it means

Thrown by validateCron when schedule.timezone is not null AND IANAZone.isValidZone returns false. A null timezone is allowed (resolved upstream to the instance default); any non-null string must be a valid IANA zone (e.g. 'Europe/Berlin'), validated via Luxon.

Source

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

export function validateCron(schedule: CronSchedule | RecurringCronSchedule): void {
	// Raw DB rows may reach here untyped, so check the runtime type before use.
	const expression: unknown = schedule.cronExpression;
	if (typeof expression !== 'string') {
		throw new InvalidScheduleError(
			`${schedule.kind}.cronExpression must be a string, got ${JSON.stringify(expression)}`,
		);
	}

	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).
 */

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Use a full IANA zone name from the tz database ('Europe/Berlin', 'America/New_York', 'UTC').
  2. Pass timezone: null (not '') to opt into the instance default.
  3. If you have an offset, convert it to an IANA zone (e.g. UTC+01:00 in winter -> 'Europe/Berlin') or use 'UTC'.
  4. Offer a picker seeded from Intl.supportedValuesOf('timeZone') in the UI.

Example fix

// before
validateCron({ kind: 'cron', cronExpression: '0 9 * * *', timezone: 'CET' }); // not IANA -> throws

// after - use a real IANA zone, or null for the instance default
validateCron({ kind: 'cron', cronExpression: '0 9 * * *', timezone: 'Europe/Berlin' });
// or
validateCron({ kind: 'cron', cronExpression: '0 9 * * *', timezone: null });
Defensive patterns

Strategy: type-guard

Validate before calling

import { DateTime } from 'luxon';

function normalizeTimezone(raw: unknown): string | null {
  if (raw === null || raw === undefined || raw === '') return null;
  if (typeof raw !== 'string' || !DateTime.now().setZone(raw).isValid) {
    throw new Error(`Not a valid IANA timezone: ${JSON.stringify(raw)}`);
  }
  return raw;
}

Type guard

import { IANAZone } from 'luxon';

function isValidIanaTimezone(v: unknown): v is string {
  return typeof v === 'string' && v.length > 0 && IANAZone.isValidZone(v);
}

Prevention

When it happens

Trigger: Passing timezone: 'CET', 'UTC+2', 'EST', 'Berlin', '' (empty string), or a typo'd zone name ('Europ/Berlin'). Abbreviations and fixed offsets are not IANA zones and are rejected.

Common situations: A user enters a timezone abbreviation instead of an IANA name; a legacy system stored 'GMT+1' style offsets; a typo in a zone name; an empty string from a form field treated as 'use default' (must be null instead).

Related errors


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