{"record":{"id":"605f94da8ad91a9f","repo":"n8n-io/n8n","slug":"schedule-kind-cronexpression-must-be-a-string","errorCode":null,"errorMessage":"${schedule.kind}.cronExpression must be a string, got ${JSON.stringify(expression)}","messagePattern":"(.+?)\\.cronExpression must be a string, got (.+?)","errorType":"validation","errorClass":"InvalidScheduleError","httpStatus":null,"severity":"error","filePath":"packages/@n8n/scheduler/src/core/recurrence/kinds/cron.ts","lineNumber":29,"sourceCode":" * - 5 fields (standard cron, seconds = 0)\n * - 6 fields (the first is seconds)\n */\nconst MIN_CRON_FIELD_COUNT = 5;\nconst MAX_CRON_FIELD_COUNT = 6;\n\nexport type CronCursor = ReturnType<typeof CronExpressionParser.parse>;\n\n/**\n * Checks that a cron schedule is usable: a 5- or 6-field expression in a real\n * timezone. Also used for `recurring_cron`, which reuses the cron expression.\n * @param schedule The cron (or recurring_cron) schedule to check.\n * @throws {InvalidScheduleError} When the expression or timezone is invalid.\n */\nexport function validateCron(schedule: CronSchedule | RecurringCronSchedule): void {\n\t// Raw DB rows may reach here untyped, so check the runtime type before use.\n\tconst expression: unknown = schedule.cronExpression;\n\tif (typeof expression !== 'string') {\n\t\tthrow new InvalidScheduleError(\n\t\t\t`${schedule.kind}.cronExpression must be a string, got ${JSON.stringify(expression)}`,\n\t\t);\n\t}\n\n\tconst fieldCount = expression.trim().split(/\\s+/).length;\n\tif (fieldCount < MIN_CRON_FIELD_COUNT || fieldCount > MAX_CRON_FIELD_COUNT) {\n\t\tthrow new InvalidScheduleError(\n\t\t\t`Cron expression must have ${MIN_CRON_FIELD_COUNT} or ${MAX_CRON_FIELD_COUNT} fields (seconds optional), got ${fieldCount}: ${JSON.stringify(expression)}`,\n\t\t);\n\t}\n\n\t// A null timezone is the instance default, resolved by the caller.\n\tif (schedule.timezone !== null && !IANAZone.isValidZone(schedule.timezone)) {\n\t\tthrow new InvalidScheduleError(`Unknown IANA timezone: ${JSON.stringify(schedule.timezone)}`);\n\t}\n\n\ttry {\n\t\tCronExpressionParser.parse(expression, { tz: schedule.timezone ?? 'UTC' });","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/scheduler/src/core/recurrence/kinds/cron.ts#L11-L47","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nvalidateCron({ kind: 'cron', cronExpression: row.cron_expression, timezone: row.tz }); // row.cron_expression is null -> throws\n\n// after - guard at the boundary\nif (typeof row.cron_expression !== 'string') {\n  throw new Error(`row ${row.id} has non-string cron_expression`);\n}\nvalidateCron({ kind: 'cron', cronExpression: row.cron_expression, timezone: row.tz });","handlingStrategy":"type-guard","validationCode":"function coerceCronExpression(raw: unknown): string {\n  if (typeof raw !== 'string' || raw.length === 0) {\n    throw new Error('cronExpression must be a non-empty string');\n  }\n  return raw;\n}\n\nvalidateCron({ kind: 'cron', cronExpression: coerceCronExpression(row.cron_expression), timezone: row.tz });","typeGuard":"function isStringExpression(v: unknown): v is string {\n  return typeof v === 'string' && v.length > 0;\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["scheduler","cron","type-narrowing","database"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}