n8n-io/n8n · error · InvalidLifecycleOptionsError
concurrencyMode must be 'sequential' or 'concurrent', got ${
Error message
concurrencyMode must be 'sequential' or 'concurrent', got ${String(lifecycleOptions.concurrencyMode)} What it means
Thrown by createScheduler when lifecycleOptions.concurrencyMode is neither 'sequential' nor 'concurrent'. The mode decides whether executor passes run one-at-a-time or overlap; an unknown value would branch into undefined behavior downstream, so the factory rejects it before any pass starts.
Source
Thrown at packages/@n8n/scheduler/src/core/factory.ts:176
'materializerTimeoutSeconds',
'executorTimeoutSeconds',
'reaperTimeoutSeconds',
'retentionTimeoutSeconds',
] as const;
for (const key of durationKeys) {
const value = lifecycleOptions[key];
if (!(Number.isFinite(value) && value > 0)) {
throw new InvalidLifecycleOptionsError(
`${key} must be a positive number of seconds, got ${value}`,
);
}
}
if (
lifecycleOptions.concurrencyMode !== 'sequential' &&
lifecycleOptions.concurrencyMode !== 'concurrent'
) {
throw new InvalidLifecycleOptionsError(
`concurrencyMode must be 'sequential' or 'concurrent', got ${String(lifecycleOptions.concurrencyMode)}`,
);
}
const { maxConcurrentPasses } = lifecycleOptions;
if (!(Number.isInteger(maxConcurrentPasses) && maxConcurrentPasses >= 1)) {
throw new InvalidLifecycleOptionsError(
`maxConcurrentPasses must be a positive integer, got ${maxConcurrentPasses}`,
);
}
const emit = (level: SchedulerEventLevel, message: string, context: Record<string, unknown>) => {
// The sink is the reporting channel itself: if it throws (a broken logger),
// there is nothing left to report through, and the pass that emitted must
// not be broken by its own observability.
try {
onEvent?.({ level, message, context });
} catch {View on GitHub (pinned to 5ac6606e81)
Solutions
- Set lifecycle.concurrencyMode to exactly 'sequential' or 'concurrent'.
- Trim and lowercase config-derived values: (raw ?? '').trim().toLowerCase().
- If the value comes from an env var, validate against an allowlist in your config loader.
- Omit concurrencyMode to accept the DEFAULT_LIFECYCLE_OPTIONS default.
Example fix
// before
createScheduler({ lifecycle: { concurrencyMode: cfg.mode } }); // cfg.mode = 'parallel' -> throws
// after - normalize against an allowlist
const MODES = ['sequential', 'concurrent'] as const;
const concurrencyMode = MODES.includes(cfg.mode as any)
? (cfg.mode as typeof MODES[number])
: 'concurrent';
createScheduler({ lifecycle: { concurrencyMode } }); Defensive patterns
Strategy: type-guard
Validate before calling
const MODES = ['sequential', 'concurrent'] as const;
type ConcurrencyMode = typeof MODES[number];
function normalizeMode(raw: unknown): ConcurrencyMode {
const v = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
return (MODES as readonly string[]).includes(v) ? (v as ConcurrencyMode) : 'concurrent';
} Type guard
function isConcurrencyMode(v: unknown): v is 'sequential' | 'concurrent' {
return v === 'sequential' || v === 'concurrent';
} Prevention
- Constrain the config schema to the enum so invalid values fail at config-load, not at scheduler creation.
- Trim and lowercase env-var-derived values before comparison.
- After a version upgrade that renames modes, add a migration step that rewrites the old value.
- Default to a known mode rather than passing undefined.
When it happens
Trigger: Passing lifecycle.concurrencyMode: 'parallel', 'concurrent-', undefined-after-defaults (withDefaults did not set it), null, or a value with trailing whitespace ('concurrent ').
Common situations: A typo in config (e.g. 'concurrent' vs 'concurent'); a config schema that allowed arbitrary strings; an upgrade that renamed the mode and left stale config; whitespace from an env var not trimmed.
Related errors
- Invalid loadPreviousSession option: ${value}
- jitterRatio must be at least 0 and below 1, got ${lifecycleO
- ${key} must be a positive number of seconds, got ${value}
- maxConcurrentPasses must be a positive integer, got ${maxCon
- recurring_cron.recurrenceUnit must be one of ${RecurringCron
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/2c92e4be009bebe6.
Report an issue: GitHub.