{"record":{"id":"28be03d344b505c4","repo":"n8n-io/n8n","slug":"scheduled-job-job-id-has-unknown-misfire-policy","errorCode":null,"errorMessage":"scheduled_job ${job.id} has unknown misfire policy '${String(exhaustive)}'","messagePattern":"scheduled_job (.+?) has unknown misfire policy '(.+?)'","errorType":"exception","errorClass":"CorruptStorageRowError","httpStatus":null,"severity":"critical","filePath":"packages/@n8n/scheduler/src/core/materializer/misfire.ts","lineNumber":55,"sourceCode":"\tconst behind = due.filter((occurrence) => occurrence.getTime() <= now.getTime());\n\tconst ahead = due.slice(behind.length);\n\tif (behind.length === 0 || behind[0].getTime() > graceDeadline) {\n\t\treturn { occurrences: due, catchUpAt: null };\n\t}\n\n\tswitch (job.misfirePolicy) {\n\t\tcase ScheduledJobMisfirePolicy.Coalesce: {\n\t\t\tif (truncated && ahead.length === 0) return { occurrences: [], catchUpAt: null };\n\t\t\tconst catchUpAt = behind[behind.length - 1];\n\t\t\treturn { occurrences: [catchUpAt, ...ahead], catchUpAt };\n\t\t}\n\t\tcase ScheduledJobMisfirePolicy.Skip:\n\t\t\treturn { occurrences: ahead, catchUpAt: null };\n\t\tdefault: {\n\t\t\t// A policy this version does not know, e.g. a row written by a newer instance\n\t\t\t// and read back after a rollback.\n\t\t\tconst exhaustive: never = job.misfirePolicy;\n\t\t\tthrow new CorruptStorageRowError(\n\t\t\t\t`scheduled_job ${job.id} has unknown misfire policy '${String(exhaustive)}'`,\n\t\t\t);\n\t\t}\n\t}\n}\n\n/** Groups a pass's discarded occurrences by the task type and policy that discarded them. */\nexport function countMisfires(planned: PlannedJob[]): MisfireCount[] {\n\tconst grouped = planned\n\t\t.filter(({ plan }) => plan.skippedOccurrences > 0)\n\t\t.reduce((groups, { job, plan }) => {\n\t\t\tconst key = `${job.taskType}:${job.misfirePolicy}`;\n\t\t\treturn groups.set(key, {\n\t\t\t\ttaskType: job.taskType,\n\t\t\t\tpolicy: job.misfirePolicy,\n\t\t\t\tdiscarded: (groups.get(key)?.discarded ?? 0) + plan.skippedOccurrences,\n\t\t\t});\n\t\t}, new Map<string, MisfireCount>());","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/scheduler/src/core/materializer/misfire.ts#L37-L73","documentation":"Thrown by the misfire-policy switch when job.misfirePolicy is neither Coalesce nor Skip. The `const exhaustive: never = job.misfirePolicy` pattern turns unhandled enum members into a compile-time error AND a runtime CorruptStorageRowError. The comment names the real cause: a row written by a newer scheduler version (with a new policy) read back after a rollback.","triggerScenarios":"A scheduled_job row in the DB carries a misfire-policy byte/string that this build's ScheduledJobMisfirePolicy enum does not define. Happens after a forward-then-backward version migration, a manual DB edit, or a corrupt row.","commonSituations":"Rolling back a scheduler release that introduced a new policy (e.g. 'FireAndForget'); restoring a DB backup from a newer version onto an older instance; concurrent instances running different scheduler versions against the same DB.","solutions":["Restore all scheduler instances to the same version so the enum matches the rows.","If the row is genuinely corrupt, update the misfire_policy column to a known value (Coalesce or Skip) for the offending job id.","Forward-migrate the schema rather than rolling back, so new policy values stay understood.","Pin a single scheduler version across the fleet; never run mixed versions against one DB."],"exampleFix":"-- before: row has a policy byte the running build does not know\nUPDATE scheduled_job SET misfire_policy = 99 WHERE id = 'job-42';\n\n-- after: rewrite to a policy the current enum defines (Coalesce = the safe default)\nUPDATE scheduled_job SET misfire_policy = 0 WHERE id = 'job-42';","handlingStrategy":"try-catch","validationCode":"import { ScheduledJobMisfirePolicy } from '../../types';\n\nconst KNOWN_POLICIES = new Set<number>(Object.values(ScheduledJobMisfirePolicy));\n\nfunction rowHasKnownPolicy(row: { misfirePolicy: number }): boolean {\n  return KNOWN_POLICIES.has(row.misfirePolicy);\n}","typeGuard":"function isKnownMisfirePolicy(v: unknown): v is ScheduledJobMisfirePolicy {\n  return typeof v === 'number' && KNOWN_POLICIES.has(v);\n}","tryCatchPattern":"try {\n  applyMisfirePolicy(job);\n} catch (e) {\n  if (e instanceof CorruptStorageRowError && /unknown misfire policy/.test(e.message)) {\n    // Do NOT silently retry - quarantine the row and alert.\n    logger.error('Quarantining scheduled_job with unknown misfire policy', { jobId: job.id, policy: job.misfirePolicy });\n    await quarantineJob(job.id);\n    return;\n  }\n  throw e;\n}","preventionTips":["Run a single scheduler version across all instances hitting the same DB - mixed versions are the dominant cause.","Before rolling back a scheduler release, scan for rows whose policy the older enum does not know and rewrite them.","Treat CorruptStorageRowError as a data-integrity incident: quarantine the row, alert, and do not auto-retry.","Forward-migrate rather than rollback whenever a new policy value exists."],"tags":["scheduler","database","version-mismatch","corruption"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}