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
- Set recurrenceUnit to one of 'hours', 'days', 'weeks', or 'months' (exact lowercase match).
- If you need sub-hour cadence, use a plain interval schedule (kind='interval') or a stepped cron expression (kind='cron') instead of recurring_cron.
- If the value comes from user input or an API, validate it against RecurringCronUnitList before persisting the job.
- 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
- Always validate recurrenceUnit against RecurringCronUnitList before persisting a job.
- Use TypeScript types (RecurringCronUnit) rather than string at the API boundary to catch typos at compile time.
- Add a CHECK constraint at the database level to enforce valid recurrenceUnit values.
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
- recurring_cron.recurrenceSize must be an integer of at least
- jitterRatio must be at least 0 and below 1, got ${lifecycleO
- ${key} must be a positive number of seconds, got ${value}
- concurrencyMode must be 'sequential' or 'concurrent', got ${
- maxConcurrentPasses must be a positive integer, got ${maxCon
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/b7b16db1c70cfd8e.
Report an issue: GitHub.