{"record":{"id":"6deb07c0b609c532","repo":"n8n-io/n8n","slug":"unhandled-seed-mode-json-stringify-unhandled","errorCode":null,"errorMessage":"Unhandled seed mode: ${JSON.stringify(unhandled)}","messagePattern":"Unhandled seed mode: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/@n8n/instance-ai/evaluations/harness/build-workflow.ts","lineNumber":395,"sourceCode":"\t\t\t\t\t\t: reconstructed.sourceProject;\n\t\t\t\t\tlogger.info(\n\t\t\t\t\t\t`  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 ?? ''}`,\n\t\t\t\t\t);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase 'inline':\n\t\t\t\t\t// The arm is a superset of the restore payload — `mode` is the case\n\t\t\t\t\t// schema's discriminant and never reaches restore-thread.\n\t\t\t\t\tseed = config.seed;\n\t\t\t\t\tbreak;\n\t\t\t\tcase undefined:\n\t\t\t\t\tbreak;\n\t\t\t\tdefault: {\n\t\t\t\t\t// A new arm must decide what to restore here; without this the case\n\t\t\t\t\t// would silently run UNSEEDED, which is the failure this slot exists\n\t\t\t\t\t// to make impossible.\n\t\t\t\t\tconst unhandled: never = config.seed;\n\t\t\t\t\tthrow new Error(`Unhandled seed mode: ${JSON.stringify(unhandled)}`);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\t// A seed that can't be resolved is a harness/framework problem, not an\n\t\t\t// agent build failure — tag it and fail before spending a live turn.\n\t\t\tseedingFailed = true;\n\t\t\tthrow new Error(`Seeding failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t}\n\n\t\tconst openingMessage = conversation[0]?.text ?? '';\n\t\tconst isMultiTurn = isMultiTurnConversation(conversation);\n\t\tlogger.info(\n\t\t\t`  Running case${isMultiTurn ? ' [multi-turn]' : ''}: \"${truncate(openingMessage, 60)}\"${config.laneTag ?? ''}`,\n\t\t);\n\n\t\tconst projectId = await client.getPersonalProjectId();\n\t\tawait client.ensureThread(threadId, projectId);\n","sourceCodeStart":377,"sourceCodeEnd":413,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/instance-ai/evaluations/harness/build-workflow.ts#L377-L413","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Add a `case` for the new seed mode in the switch and resolve `seed` from it before the default.","Ensure the Zod schema strictly rejects unknown modes (no `.passthrough()` on the discriminant).","Keep the `never` exhaustiveness assignment so the compiler flags the missing case at build time."],"exampleFix":"// before\ncase 'inline': seed = config.seed; break;\ncase undefined: break;\ndefault: { const unhandled: never = config.seed; throw new Error(`Unhandled seed mode: ...`); }\n// after (new 'snapshot' arm added to the union)\ncase 'snapshot': seed = await loadSnapshotSeed(config.seed); break;\ncase 'inline': seed = config.seed; break;\ncase undefined: break;\ndefault: { const unhandled: never = config.seed; throw new Error(`Unhandled seed mode: ...`); }","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// The existing exhaustiveness guard IS the type-level defense. Keep `never`.\nfunction resolveSeed(config: BuildConfig): ConversationSeed | undefined {\n  switch (config.seed?.mode) {\n    case 'thread': /* ... */ return seed;\n    case 'inline': return config.seed;\n    case undefined: return undefined;\n    default: {\n      const _exhaustive: never = config.seed;\n      throw new Error(`Unhandled seed mode: ${JSON.stringify(_exhaustive)}`);\n    }\n  }\n}","tryCatchPattern":null,"preventionTips":["Always case the `never` default so the compiler flags a missing arm when the union widens.","Make the Zod schema strict (no `.passthrough()` on the discriminant) so unknown modes fail at parse.","Land handler arms and schema arms in the same commit to avoid the window where this throw is reachable."],"tags":["discriminated-union","exhaustiveness","framework","seeding"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}