nexu-io/open-design · error

automation template requires at least one valid stage

Error message

automation template requires at least one valid stage

What it means

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.

Source

Thrown at apps/daemon/src/automation-templates.ts:260

      const stageRaw = stage as Record<string, unknown>;
      const stageId = cleanId(stageRaw.id);
      const kind = stageRaw.kind;
      const title = stageRaw.title;
      if (!stageId || typeof title !== 'string' || !title.trim()) return null;
      if (typeof kind !== 'string' || !STAGE_KINDS.has(kind as AutomationTemplateStageKind)) {
        return null;
      }
      return {
        id: stageId,
        kind: kind as AutomationTemplateStageKind,
        title: title.trim(),
        ...(typeof stageRaw.description === 'string' && stageRaw.description.trim()
          ? { description: stageRaw.description.trim() }
          : {}),
      };
    })
    .filter((stage): stage is AutomationTemplate['stages'][number] => Boolean(stage));
  if (stages.length === 0) throw new Error('automation template requires at least one valid stage');
  return {
    id,
    title: requiredString(raw, 'title'),
    description: requiredString(raw, 'description'),
    purpose: requiredString(raw, 'purpose'),
    triggerKinds: cleanEnumArray(raw.triggerKinds, TRIGGER_KINDS, ['manual']),
    sourceKinds: cleanEnumArray(raw.sourceKinds, SOURCE_KINDS, ['chat']),
    stages,
    outputSinks: cleanEnumArray(raw.outputSinks, OUTPUT_SINKS, ['memory']),
    reviewPolicy: cleanReviewPolicy(raw.reviewPolicy),
    tokenCompression: cleanCompressionMode(raw.tokenCompression),
    ...(cleanStringArray(raw.tags).length > 0 ? { tags: cleanStringArray(raw.tags) } : {}),
  };
}

async function readUserAutomationTemplates(dataDir: string): Promise<AutomationTemplate[]> {
  let parsed: unknown;
  try {

View on GitHub (pinned to 5be4028344)

Solutions

  1. Include at least one stage with a valid id, a non-empty title, and a kind from STAGE_KINDS.
  2. Verify each kind against the current enum (ingest, canonicalize, classify, compress, redact, agent-run, propose) — note the kebab-case spelling.
  3. If your stages array legitimately could be empty, reconsider whether a template is the right shape; the model requires at least one stage.

Example fix

// before
await upsertUserAutomationTemplate(dataDir, {
  id: 'my-template', title: 'T', description: 'D', purpose: 'P',
  stages: [{ id: 'ingest', kind: 'INGEST', title: 'Capture' }], // unknown kind -> filtered out -> throws
});

// after
await upsertUserAutomationTemplate(dataDir, {
  id: 'my-template', title: 'T', description: 'D', purpose: 'P',
  stages: [{ id: 'ingest', kind: 'ingest', title: 'Capture' }],
});
Defensive patterns

Strategy: validation

Validate before calling

const SAFE_ID = /^[a-z0-9][a-z0-9._-]{1,95}$/;
const STAGE_KINDS = new Set(['ingest', 'canonicalize', 'classify', 'compress', 'redact', 'agent-run', 'propose']);
function validStages(stages: unknown): boolean {
  if (!Array.isArray(stages) || stages.length === 0) return false;
  return stages.every(s =>
    s && typeof s === 'object' && !Array.isArray(s)
    && typeof (s as any).id === 'string' && SAFE_ID.test((s as any).id)
    && typeof (s as any).title === 'string' && (s as any).title.trim().length > 0
    && typeof (s as any).kind === 'string' && STAGE_KINDS.has((s as any).kind));
}
if (!validStages(payload.stages)) throw new Error('At least one valid stage is required.');

Type guard

const STAGE_KINDS = new Set(['ingest', 'canonicalize', 'classify', 'compress', 'redact', 'agent-run', 'propose']);
function isValidStage(s: unknown): s is { id: string; kind: string; title: string } {
  if (!s || typeof s !== 'object' || Array.isArray(s)) return false;
  const o = s as Record<string, unknown>;
  return typeof o.id === 'string' && /^[a-z0-9][a-z0-9._-]{1,95}$/.test(o.id)
    && typeof o.title === 'string' && o.title.trim().length > 0
    && typeof o.kind === 'string' && STAGE_KINDS.has(o.kind);
}

Try / catch

try {
  await upsertUserAutomationTemplate(dataDir, payload);
} catch (err) {
  if (err instanceof Error && err.message === 'automation template requires at least one valid stage') {
    return badRequest('Include at least one stage with id, title, and a known kind (ingest, canonicalize, classify, compress, redact, agent-run, propose).');
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/f9a93c01f0a1ee7c. Report an issue: GitHub.