n8n-io/n8n · critical · CorruptStorageRowError

scheduled_job has unknown misfire policy

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.

Solutions

  1. Restore all scheduler instances to the same version so the enum matches the rows.
  2. If the row is genuinely corrupt, update the misfire_policy column to a known value (Coalesce or Skip) for the offending job id.
  3. Forward-migrate the schema rather than rolling back, so new policy values stay understood.
  4. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/28be03d344b505c4. Report an issue: GitHub.

Appendix: 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)