n8n-io/n8n · error · InvalidScheduleError
${schedule.kind}.cronExpression must be a string, got ${JSON
Error message
${schedule.kind}.cronExpression must be a string, got ${JSON.stringify(expression)} What it means
Thrown by validateCron when schedule.cronExpression is not a string. Because raw DB rows may reach the validator untyped, the validator defensively narrows `unknown` before any cron parsing. A null, number, or object expression is rejected before it can crash CronExpressionParser.
Source
Thrown at packages/@n8n/scheduler/src/core/recurrence/kinds/cron.ts:29
* - 5 fields (standard cron, seconds = 0)
* - 6 fields (the first is seconds)
*/
const MIN_CRON_FIELD_COUNT = 5;
const MAX_CRON_FIELD_COUNT = 6;
export type CronCursor = ReturnType<typeof CronExpressionParser.parse>;
/**
* Checks that a cron schedule is usable: a 5- or 6-field expression in a real
* timezone. Also used for `recurring_cron`, which reuses the cron expression.
* @param schedule The cron (or recurring_cron) schedule to check.
* @throws {InvalidScheduleError} When the expression or timezone is invalid.
*/
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' });View on GitHub (pinned to 5ac6606e81)
Solutions
- Ensure cron_expression is stored as a non-null VARCHAR/TEXT in the DB.
- Coerce request input: if (typeof raw !== 'string') reject before validateCron.
- Backfill null cron_expression rows with a default or delete them.
- Type the schedule literal with cronExpression: string and let TS catch the mistake at compile time.
Example fix
// before
validateCron({ kind: 'cron', cronExpression: row.cron_expression, timezone: row.tz }); // row.cron_expression is null -> throws
// after - guard at the boundary
if (typeof row.cron_expression !== 'string') {
throw new Error(`row ${row.id} has non-string cron_expression`);
}
validateCron({ kind: 'cron', cronExpression: row.cron_expression, timezone: row.tz }); Defensive patterns
Strategy: type-guard
Validate before calling
function coerceCronExpression(raw: unknown): string {
if (typeof raw !== 'string' || raw.length === 0) {
throw new Error('cronExpression must be a non-empty string');
}
return raw;
}
validateCron({ kind: 'cron', cronExpression: coerceCronExpression(row.cron_expression), timezone: row.tz }); Type guard
function isStringExpression(v: unknown): v is string {
return typeof v === 'string' && v.length > 0;
} Prevention
- Map DB columns with the right type in the ORM so cron_expression comes back as a string, not null or an object.
- Coerce request input at the controller boundary before it reaches the scheduler.
- Backfill or delete rows with null cron_expression.
- Type schedule literals with cronExpression: string so TS catches the mistake at compile time.
When it happens
Trigger: Loading a cron or recurring_cron schedule from a DB row where cron_expression is NULL, an integer, an empty object, or a JSON-parsed array; passing a schedule literal with cronExpression typo'd as expression.
Common situations: A migration that left cron_expression null for legacy rows; an ORM that returned the column as a parsed JSON object; constructing a schedule from request body without coercion; a test fixture that set cronExpression: undefined.
Related errors
- scheduled_job ${job.id} has unknown misfire policy '${String
- Cron expression must have ${MIN_CRON_FIELD_COUNT} or ${MAX_C
- Unknown IANA timezone: ${JSON.stringify(schedule.timezone)}
- Invalid cron expression ${JSON.stringify(expression)}: ${(er
- Failed to evaluate cron expression ${JSON.stringify(cronExpr
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/605f94da8ad91a9f.
Report an issue: GitHub.