{"record":{"id":"b7b16db1c70cfd8e","repo":"n8n-io/n8n","slug":"recurring-cron-recurrenceunit-must-be-one-of-rec","errorCode":null,"errorMessage":"recurring_cron.recurrenceUnit must be one of ${RecurringCronUnitList.join(', ')}, got ${JSON.stringify(schedule.recurrenceUnit)}","messagePattern":"recurring_cron\\.recurrenceUnit must be one of (.+?), got (.+?)","errorType":"validation","errorClass":"InvalidScheduleError","httpStatus":null,"severity":"error","filePath":"packages/@n8n/scheduler/src/core/recurrence/kinds/recurring-cron.ts","lineNumber":29,"sourceCode":" * How many cron fires an \"every N periods\" scan may reject before giving up.\n * Real schedules stay far below this (a weekly cron kept every N weeks rejects\n * at most 7×N candidates); hitting the bound means the cron fires so much more\n * often than the rule keeps that the schedule is judged malformed.\n */\nconst MAX_RECURRENCE_CANDIDATES = 10_000;\n\n/**\n * Checks an \"every N periods\" cron schedule: a valid cron expression, a known\n * period unit, and N of at least 2 (N = 1 keeps every fire, which is a plain\n * cron).\n * @param schedule The recurring_cron schedule to check.\n * @throws {InvalidScheduleError} When the cron, unit, or N is invalid.\n */\nexport function validateRecurringCron(schedule: RecurringCronSchedule): void {\n\tvalidateCron(schedule);\n\n\tif (!RecurringCronUnitList.includes(schedule.recurrenceUnit)) {\n\t\tthrow new InvalidScheduleError(\n\t\t\t`recurring_cron.recurrenceUnit must be one of ${RecurringCronUnitList.join(', ')}, got ${JSON.stringify(schedule.recurrenceUnit)}`,\n\t\t);\n\t}\n\n\tif (!Number.isInteger(schedule.recurrenceSize) || schedule.recurrenceSize < 2) {\n\t\tthrow new InvalidScheduleError(\n\t\t\t`recurring_cron.recurrenceSize must be an integer of at least 2 (a stride of 1 is a plain cron), got ${JSON.stringify(schedule.recurrenceSize)}`,\n\t\t);\n\t}\n}\n\n/**\n * Whether a candidate fire time should actually fire, for an \"every N periods\"\n * schedule. It only compares the candidate to the previous fire — there is no\n * hidden counter — so it gives the same answer on any machine and after a\n * restart.\n *\n * The candidate fires when either:","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/scheduler/src/core/recurrence/kinds/recurring-cron.ts#L11-L47","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst schedule = {\n  kind: 'recurring_cron',\n  cronExpression: '0 9 * * 1',\n  recurrenceUnit: 'Minutes', // wrong casing\n  recurrenceSize: 2,\n};\n// after\nconst schedule = {\n  kind: 'recurring_cron',\n  cronExpression: '0 9 * * 1',\n  recurrenceUnit: 'weeks',\n  recurrenceSize: 2,\n};","handlingStrategy":"validation","validationCode":"import { RecurringCronUnitList } from '@n8n/constants';\n\nfunction isValidRecurringCronUnit(unit: unknown): unit is string {\n  return typeof unit === 'string' && RecurringCronUnitList.includes(unit as any);\n}\n\n// before registering the job:\nif (!isValidRecurringCronUnit(schedule.recurrenceUnit)) {\n  throw new Error(`Invalid recurrenceUnit: ${schedule.recurrenceUnit}`);\n}","typeGuard":"import { RecurringCronUnitList, type RecurringCronUnit } from '@n8n/constants';\n\nfunction isRecurringCronUnit(value: unknown): value is RecurringCronUnit {\n  return typeof value === 'string'\n    && (RecurringCronUnitList as readonly string[]).includes(value);\n}","tryCatchPattern":"import { InvalidScheduleError } from '@n8n/scheduler';\n\ntry {\n  validateRecurringCron(schedule);\n} catch (e) {\n  if (e instanceof InvalidScheduleError) {\n    // log and surface to user: invalid recurrenceUnit\n    logger.error('Invalid schedule configuration', { error: e.message });\n    throw new UserError('Please select a valid recurrence unit');\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["scheduler","validation","recurrence","configuration"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}