{"record":{"id":"f9a93c01f0a1ee7c","repo":"nexu-io/open-design","slug":"automation-template-requires-at-least-one-valid-st","errorCode":null,"errorMessage":"automation template requires at least one valid stage","messagePattern":"automation template requires at least one valid stage","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/daemon/src/automation-templates.ts","lineNumber":260,"sourceCode":"      const stageRaw = stage as Record<string, unknown>;\n      const stageId = cleanId(stageRaw.id);\n      const kind = stageRaw.kind;\n      const title = stageRaw.title;\n      if (!stageId || typeof title !== 'string' || !title.trim()) return null;\n      if (typeof kind !== 'string' || !STAGE_KINDS.has(kind as AutomationTemplateStageKind)) {\n        return null;\n      }\n      return {\n        id: stageId,\n        kind: kind as AutomationTemplateStageKind,\n        title: title.trim(),\n        ...(typeof stageRaw.description === 'string' && stageRaw.description.trim()\n          ? { description: stageRaw.description.trim() }\n          : {}),\n      };\n    })\n    .filter((stage): stage is AutomationTemplate['stages'][number] => Boolean(stage));\n  if (stages.length === 0) throw new Error('automation template requires at least one valid stage');\n  return {\n    id,\n    title: requiredString(raw, 'title'),\n    description: requiredString(raw, 'description'),\n    purpose: requiredString(raw, 'purpose'),\n    triggerKinds: cleanEnumArray(raw.triggerKinds, TRIGGER_KINDS, ['manual']),\n    sourceKinds: cleanEnumArray(raw.sourceKinds, SOURCE_KINDS, ['chat']),\n    stages,\n    outputSinks: cleanEnumArray(raw.outputSinks, OUTPUT_SINKS, ['memory']),\n    reviewPolicy: cleanReviewPolicy(raw.reviewPolicy),\n    tokenCompression: cleanCompressionMode(raw.tokenCompression),\n    ...(cleanStringArray(raw.tags).length > 0 ? { tags: cleanStringArray(raw.tags) } : {}),\n  };\n}\n\nasync function readUserAutomationTemplates(dataDir: string): Promise<AutomationTemplate[]> {\n  let parsed: unknown;\n  try {","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/automation-templates.ts#L242-L278","documentation":"Thrown after the stages array is mapped and filtered. Each stage survives only if it has a valid id (passes SAFE_ID), a non-empty string title, and a kind in STAGE_KINDS (ingest, canonicalize, classify, compress, redact, agent-run, propose per the built-in templates). If zero stages survive, the template is rejected.","triggerScenarios":"Passing stages: []; passing stages whose kind is unknown or misspelled (e.g. 'agent_run' vs 'agent-run', or uppercased 'INGEST'); stages missing title or with an invalid id; stages that are not plain objects.","commonSituations":"Renaming a stage kind in the payload that no longer matches the enum; copy-pasting a stage and forgetting to update its kind; version drift where an older template used a kind that has since been removed.","solutions":["Include at least one stage with a valid id, a non-empty title, and a kind from STAGE_KINDS.","Verify each kind against the current enum (ingest, canonicalize, classify, compress, redact, agent-run, propose) — note the kebab-case spelling.","If your stages array legitimately could be empty, reconsider whether a template is the right shape; the model requires at least one stage."],"exampleFix":"// before\nawait upsertUserAutomationTemplate(dataDir, {\n  id: 'my-template', title: 'T', description: 'D', purpose: 'P',\n  stages: [{ id: 'ingest', kind: 'INGEST', title: 'Capture' }], // unknown kind -> filtered out -> throws\n});\n\n// after\nawait upsertUserAutomationTemplate(dataDir, {\n  id: 'my-template', title: 'T', description: 'D', purpose: 'P',\n  stages: [{ id: 'ingest', kind: 'ingest', title: 'Capture' }],\n});","handlingStrategy":"validation","validationCode":"const SAFE_ID = /^[a-z0-9][a-z0-9._-]{1,95}$/;\nconst STAGE_KINDS = new Set(['ingest', 'canonicalize', 'classify', 'compress', 'redact', 'agent-run', 'propose']);\nfunction validStages(stages: unknown): boolean {\n  if (!Array.isArray(stages) || stages.length === 0) return false;\n  return stages.every(s =>\n    s && typeof s === 'object' && !Array.isArray(s)\n    && typeof (s as any).id === 'string' && SAFE_ID.test((s as any).id)\n    && typeof (s as any).title === 'string' && (s as any).title.trim().length > 0\n    && typeof (s as any).kind === 'string' && STAGE_KINDS.has((s as any).kind));\n}\nif (!validStages(payload.stages)) throw new Error('At least one valid stage is required.');","typeGuard":"const STAGE_KINDS = new Set(['ingest', 'canonicalize', 'classify', 'compress', 'redact', 'agent-run', 'propose']);\nfunction isValidStage(s: unknown): s is { id: string; kind: string; title: string } {\n  if (!s || typeof s !== 'object' || Array.isArray(s)) return false;\n  const o = s as Record<string, unknown>;\n  return typeof o.id === 'string' && /^[a-z0-9][a-z0-9._-]{1,95}$/.test(o.id)\n    && typeof o.title === 'string' && o.title.trim().length > 0\n    && typeof o.kind === 'string' && STAGE_KINDS.has(o.kind);\n}","tryCatchPattern":"try {\n  await upsertUserAutomationTemplate(dataDir, payload);\n} catch (err) {\n  if (err instanceof Error && err.message === 'automation template requires at least one valid stage') {\n    return badRequest('Include at least one stage with id, title, and a known kind (ingest, canonicalize, classify, compress, redact, agent-run, propose).');\n  }\n  throw err;\n}","preventionTips":["Always include at least one fully-formed stage.","Use kebab-case kinds spelled exactly as in STAGE_KINDS (note 'agent-run', not 'agent_run').","Validate stage kind spelling against the current enum before submitting — kinds can change across versions."],"tags":["automation","validation","templates","stages"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}