n8n-io/n8n · critical · Error

Unhandled seed mode: ${JSON.stringify(unhandled)}

Error message

Unhandled seed mode: ${JSON.stringify(unhandled)}

What it means

Thrown in the seed-resolution switch when config.seed matches none of the handled arms ('thread', 'inline', undefined). The `const unhandled: never = config.seed` assignment is an exhaustiveness check: it compiles only if every member of the seed discriminated union is cased above. At runtime it means a new seed mode was added to the union (or the schema) without a matching case — a harness/framework bug, never a fixture or config mistake. The whole block is wrapped so the throw is re-tagged as 'Seeding failed'.

Source

Thrown at packages/@n8n/instance-ai/evaluations/harness/build-workflow.ts:395

						: reconstructed.sourceProject;
					logger.info(
						`  Reconstructed seed from thread ${config.seed.threadId}: ${String(reconstructed.runCount)} runs → ${String(seed.messages.length)} message(s), ${String(seed.workflows.length)} workflow(s)${contSuffix} [${wsLabel}]${config.laneTag ?? ''}`,
					);
					break;
				}
				case 'inline':
					// The arm is a superset of the restore payload — `mode` is the case
					// schema's discriminant and never reaches restore-thread.
					seed = config.seed;
					break;
				case undefined:
					break;
				default: {
					// A new arm must decide what to restore here; without this the case
					// would silently run UNSEEDED, which is the failure this slot exists
					// to make impossible.
					const unhandled: never = config.seed;
					throw new Error(`Unhandled seed mode: ${JSON.stringify(unhandled)}`);
				}
			}
		} catch (error: unknown) {
			// A seed that can't be resolved is a harness/framework problem, not an
			// agent build failure — tag it and fail before spending a live turn.
			seedingFailed = true;
			throw new Error(`Seeding failed: ${error instanceof Error ? error.message : String(error)}`);
		}

		const openingMessage = conversation[0]?.text ?? '';
		const isMultiTurn = isMultiTurnConversation(conversation);
		logger.info(
			`  Running case${isMultiTurn ? ' [multi-turn]' : ''}: "${truncate(openingMessage, 60)}"${config.laneTag ?? ''}`,
		);

		const projectId = await client.getPersonalProjectId();
		await client.ensureThread(threadId, projectId);

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Add a `case` for the new seed mode in the switch and resolve `seed` from it before the default.
  2. Ensure the Zod schema strictly rejects unknown modes (no `.passthrough()` on the discriminant).
  3. Keep the `never` exhaustiveness assignment so the compiler flags the missing case at build time.

Example fix

// before
case 'inline': seed = config.seed; break;
case undefined: break;
default: { const unhandled: never = config.seed; throw new Error(`Unhandled seed mode: ...`); }
// after (new 'snapshot' arm added to the union)
case 'snapshot': seed = await loadSnapshotSeed(config.seed); break;
case 'inline': seed = config.seed; break;
case undefined: break;
default: { const unhandled: never = config.seed; throw new Error(`Unhandled seed mode: ...`); }
Defensive patterns

Strategy: type-guard

Type guard

// The existing exhaustiveness guard IS the type-level defense. Keep `never`.
function resolveSeed(config: BuildConfig): ConversationSeed | undefined {
  switch (config.seed?.mode) {
    case 'thread': /* ... */ return seed;
    case 'inline': return config.seed;
    case undefined: return undefined;
    default: {
      const _exhaustive: never = config.seed;
      throw new Error(`Unhandled seed mode: ${JSON.stringify(_exhaustive)}`);
    }
  }
}

Prevention

When it happens

Trigger: A developer extends the seed config schema with a new discriminant arm (e.g. 'snapshot') and adds it to the union type without adding a case here. The exhaustiveness `never` is the compile-time guard; reaching the throw means the type was bypassed (e.g. `as`, loose schema) or the case was omitted.

Common situations: Mid-migration: the schema/type arm lands before the handler arm; a discriminated union widened without updating all switches; Zod `.passthrough()` letting an unmodeled mode through.

Related errors


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