n8n-io/n8n · error · InvalidScheduleError

recurring_cron.recurrenceUnit must be one of ${RecurringCron

Error message

recurring_cron.recurrenceUnit must be one of ${RecurringCronUnitList.join(', ')}, got ${JSON.stringify(schedule.recurrenceUnit)}

What it means

Thrown by validateRecurringCron when the recurrenceUnit field of a recurring_cron schedule is not one of the allowed values. The allowed values come from RecurringCronUnitList, which is defined in @n8n/constants as 'hours', 'days', 'weeks', 'months'. This is a validation gate before the scheduler computes fire times, ensuring the period-counting logic has a known unit to operate on.

Source

Thrown at packages/@n8n/scheduler/src/core/recurrence/kinds/recurring-cron.ts:29

 * How many cron fires an "every N periods" scan may reject before giving up.
 * Real schedules stay far below this (a weekly cron kept every N weeks rejects
 * at most 7×N candidates); hitting the bound means the cron fires so much more
 * often than the rule keeps that the schedule is judged malformed.
 */
const MAX_RECURRENCE_CANDIDATES = 10_000;

/**
 * Checks an "every N periods" cron schedule: a valid cron expression, a known
 * period unit, and N of at least 2 (N = 1 keeps every fire, which is a plain
 * cron).
 * @param schedule The recurring_cron schedule to check.
 * @throws {InvalidScheduleError} When the cron, unit, or N is invalid.
 */
export function validateRecurringCron(schedule: RecurringCronSchedule): void {
	validateCron(schedule);

	if (!RecurringCronUnitList.includes(schedule.recurrenceUnit)) {
		throw new InvalidScheduleError(
			`recurring_cron.recurrenceUnit must be one of ${RecurringCronUnitList.join(', ')}, got ${JSON.stringify(schedule.recurrenceUnit)}`,
		);
	}

	if (!Number.isInteger(schedule.recurrenceSize) || schedule.recurrenceSize < 2) {
		throw new InvalidScheduleError(
			`recurring_cron.recurrenceSize must be an integer of at least 2 (a stride of 1 is a plain cron), got ${JSON.stringify(schedule.recurrenceSize)}`,
		);
	}
}

/**
 * Whether a candidate fire time should actually fire, for an "every N periods"
 * schedule. It only compares the candidate to the previous fire — there is no
 * hidden counter — so it gives the same answer on any machine and after a
 * restart.
 *
 * The candidate fires when either:

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Set recurrenceUnit to one of 'hours', 'days', 'weeks', or 'months' (exact lowercase match).
  2. If you need sub-hour cadence, use a plain interval schedule (kind='interval') or a stepped cron expression (kind='cron') instead of recurring_cron.
  3. If the value comes from user input or an API, validate it against RecurringCronUnitList before persisting the job.
  4. Inspect the scheduled job row in the database to find which record has the invalid recurrenceUnit and fix or delete it.

Example fix

// before
const schedule = {
  kind: 'recurring_cron',
  cronExpression: '0 9 * * 1',
  recurrenceUnit: 'Minutes', // wrong casing
  recurrenceSize: 2,
};
// after
const schedule = {
  kind: 'recurring_cron',
  cronExpression: '0 9 * * 1',
  recurrenceUnit: 'weeks',
  recurrenceSize: 2,
};
Defensive patterns

Strategy: validation

Validate before calling

import { RecurringCronUnitList } from '@n8n/constants';

function isValidRecurringCronUnit(unit: unknown): unit is string {
  return typeof unit === 'string' && RecurringCronUnitList.includes(unit as any);
}

// before registering the job:
if (!isValidRecurringCronUnit(schedule.recurrenceUnit)) {
  throw new Error(`Invalid recurrenceUnit: ${schedule.recurrenceUnit}`);
}

Type guard

import { RecurringCronUnitList, type RecurringCronUnit } from '@n8n/constants';

function isRecurringCronUnit(value: unknown): value is RecurringCronUnit {
  return typeof value === 'string'
    && (RecurringCronUnitList as readonly string[]).includes(value);
}

Try / catch

import { InvalidScheduleError } from '@n8n/scheduler';

try {
  validateRecurringCron(schedule);
} catch (e) {
  if (e instanceof InvalidScheduleError) {
    // log and surface to user: invalid recurrenceUnit
    logger.error('Invalid schedule configuration', { error: e.message });
    throw new UserError('Please select a valid recurrence unit');
  }
  throw e;
}

Prevention

When it happens

Trigger: A scheduled job with kind='recurring_cron' is registered or resolved where job.recurrenceUnit is undefined, null, an empty string, or any value outside {'hours','days','weeks','months'} (e.g. 'minutes', 'seconds', 'years', 'Minutes'). Most commonly triggered when resolveRecurringCron reads a job row whose recurrenceUnit column was set incorrectly or left null.

Common situations: Writing a migration or seed that inserts a recurring_cron job row with a typo or wrong casing in recurrenceUnit. Deserializing a schedule from an API payload or JSON where the unit was omitted. Database rows corrupted by an older schema version that didn't enforce the CHECK constraint.

Related errors


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