n8n-io/n8n · critical · CorruptStorageRowError
scheduled_job ${job.id} has unknown misfire policy '${String
Error message
scheduled_job ${job.id} has unknown misfire policy '${String(exhaustive)}' What it means
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.
Source
Thrown at packages/@n8n/scheduler/src/core/materializer/misfire.ts:55
const behind = due.filter((occurrence) => occurrence.getTime() <= now.getTime());
const ahead = due.slice(behind.length);
if (behind.length === 0 || behind[0].getTime() > graceDeadline) {
return { occurrences: due, catchUpAt: null };
}
switch (job.misfirePolicy) {
case ScheduledJobMisfirePolicy.Coalesce: {
if (truncated && ahead.length === 0) return { occurrences: [], catchUpAt: null };
const catchUpAt = behind[behind.length - 1];
return { occurrences: [catchUpAt, ...ahead], catchUpAt };
}
case ScheduledJobMisfirePolicy.Skip:
return { occurrences: ahead, catchUpAt: null };
default: {
// A policy this version does not know, e.g. a row written by a newer instance
// and read back after a rollback.
const exhaustive: never = job.misfirePolicy;
throw new CorruptStorageRowError(
`scheduled_job ${job.id} has unknown misfire policy '${String(exhaustive)}'`,
);
}
}
}
/** Groups a pass's discarded occurrences by the task type and policy that discarded them. */
export function countMisfires(planned: PlannedJob[]): MisfireCount[] {
const grouped = planned
.filter(({ plan }) => plan.skippedOccurrences > 0)
.reduce((groups, { job, plan }) => {
const key = `${job.taskType}:${job.misfirePolicy}`;
return groups.set(key, {
taskType: job.taskType,
policy: job.misfirePolicy,
discarded: (groups.get(key)?.discarded ?? 0) + plan.skippedOccurrences,
});
}, new Map<string, MisfireCount>());View on GitHub (pinned to 5ac6606e81)
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.
Example fix
-- before: row has a policy byte the running build does not know UPDATE scheduled_job SET misfire_policy = 99 WHERE id = 'job-42'; -- after: rewrite to a policy the current enum defines (Coalesce = the safe default) UPDATE scheduled_job SET misfire_policy = 0 WHERE id = 'job-42';
Defensive patterns
Strategy: try-catch
Validate before calling
import { ScheduledJobMisfirePolicy } from '../../types';
const KNOWN_POLICIES = new Set<number>(Object.values(ScheduledJobMisfirePolicy));
function rowHasKnownPolicy(row: { misfirePolicy: number }): boolean {
return KNOWN_POLICIES.has(row.misfirePolicy);
} Type guard
function isKnownMisfirePolicy(v: unknown): v is ScheduledJobMisfirePolicy {
return typeof v === 'number' && KNOWN_POLICIES.has(v);
} Try / catch
try {
applyMisfirePolicy(job);
} catch (e) {
if (e instanceof CorruptStorageRowError && /unknown misfire policy/.test(e.message)) {
// Do NOT silently retry - quarantine the row and alert.
logger.error('Quarantining scheduled_job with unknown misfire policy', { jobId: job.id, policy: job.misfirePolicy });
await quarantineJob(job.id);
return;
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ${schedule.kind}.cronExpression must be a string, got ${JSON
- Supabase upsert failed: ${error.message}
- Supabase query failed: ${error.message}
- Supabase delete failed: ${error.message}
- Database connection timed out
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/28be03d344b505c4.
Report an issue: GitHub.