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
- Read the wrapped parser message - it identifies the bad field and value.
- Use only standard unix cron syntax: 0-59 minutes, 0-23 hours, 1-31 dom, 1-12 month, 0-6 dow.
- For 'last weekday of month' semantics, check the parser's supported modifier set before using L/W/#.
- 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
- Pre-test expressions with cron-parser in a UI playground before submission.
- Restrict to standard unix cron ranges per field (0-59, 0-23, 1-31, 1-12, 0-6).
- Document which modifiers (L, W, #) the parser supports before users reach for them.
- Treat InvalidScheduleError as a user-input error, not a transient retry candidate.
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
- Cron expression must have ${MIN_CRON_FIELD_COUNT} or ${MAX_C
- Unknown IANA timezone: ${JSON.stringify(schedule.timezone)}
- 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 ${
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/2854d830aec050e6.
Report an issue: GitHub.