nexu-io/open-design · error

automation template id must be a safe slug

Error message

automation template id must be a safe slug

What it means

Thrown when cleanId(raw.id) returns empty, i.e. raw.id fails SAFE_ID = /^[a-z0-9][a-z0-9._-]{1,95}$/. The id must be lowercase, start with an alphanumeric character, be 2-96 characters long, and contain only [a-z0-9._-]. Uppercase, spaces, slashes, leading punctuation, and over-length ids are rejected.

Source

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

function cleanReviewPolicy(value: unknown): AutomationReviewPolicy {
  return typeof value === 'string' && REVIEW_POLICIES.has(value as AutomationReviewPolicy)
    ? value as AutomationReviewPolicy
    : 'always';
}

function cleanCompressionMode(value: unknown): AutomationTokenCompressionMode {
  return typeof value === 'string' && COMPRESSION_MODES.has(value as AutomationTokenCompressionMode)
    ? value as AutomationTokenCompressionMode
    : 'balanced';
}

export function normalizeAutomationTemplate(input: unknown): AutomationTemplate {
  if (!input || typeof input !== 'object' || Array.isArray(input)) {
    throw new Error('automation template must be an object');
  }
  const raw = input as Record<string, unknown>;
  const id = cleanId(raw.id);
  if (!id) throw new Error('automation template id must be a safe slug');
  const rawStages = Array.isArray(raw.stages) ? raw.stages : [];
  const stages = rawStages
    .map((stage): AutomationTemplate['stages'][number] | null => {
      if (!stage || typeof stage !== 'object' || Array.isArray(stage)) return null;
      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() }

View on GitHub (pinned to 5be4028344)

Solutions

  1. Slugify the id to lowercase kebab-case (e.g. 'my-template-v2').
  2. Ensure the first character is a letter or digit and total length is between 2 and 96.
  3. Strip spaces, slashes, and uppercase before submitting; only [a-z0-9._-] are allowed after the first char.

Example fix

// before
await upsertUserAutomationTemplate(dataDir, { id: 'My Template!', /* ... */ });

// after
await upsertUserAutomationTemplate(dataDir, { id: 'my-template', /* ... */ });
Defensive patterns

Strategy: validation

Validate before calling

const SAFE_ID = /^[a-z0-9][a-z0-9._-]{1,95}$/;
function isValidTemplateId(id: unknown): id is string {
  return typeof id === 'string' && SAFE_ID.test(id);
}
if (!isValidTemplateId(payload.id)) {
  throw new Error('id must be lowercase, 2-96 chars, start alphanumeric, only [a-z0-9._-]');
}

Type guard

const SAFE_ID = /^[a-z0-9][a-z0-9._-]{1,95}$/;
function isSafeTemplateId(id: unknown): id is string {
  return typeof id === 'string' && SAFE_ID.test(id);
}

Try / catch

try {
  await upsertUserAutomationTemplate(dataDir, payload);
} catch (err) {
  if (err instanceof Error && err.message === 'automation template id must be a safe slug') {
    return badRequest('Template id must be a lowercase slug (2-96 chars, [a-z0-9._-]).');
  }
  throw err;
}

Prevention

When it happens

Trigger: Submitting a template whose id contains uppercase letters, spaces, or slashes (e.g. 'My Template' or 'foo/bar'); an id with a leading underscore or dot ('.foo', '_bar'); an id longer than 96 characters; a missing id field.

Common situations: Using a human title as the id; slugify producing Title Case or leaving spaces; auto-generated ids that include UUIDs with uppercase hex (UUIDs are lowercase hex+dash and pass, but any uppercase variant fails); copy-pasting an id with a trailing newline.

Related errors


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