nexu-io/open-design · error

automation template must be an object

Error message

automation template must be an object

What it means

Thrown at the very top of normalizeAutomationTemplate when the input is not a plain object. null, undefined, arrays, strings, numbers, and booleans are all rejected before any field is read. This guards the entire shape so downstream field access (raw.id, raw.stages, ...) is safe.

Source

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

  }
  return out.length > 0 ? out : fallback;
}

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,

View on GitHub (pinned to 5be4028344)

Solutions

  1. Pass a single plain object, not an array and not a string.
  2. Confirm the request uses Content-Type: application/json and that the daemon parsed the body (not received as a raw string).
  3. If you intended to upsert several templates, loop and call upsert once per item rather than passing the array.

Example fix

// before
await upsertUserAutomationTemplate(dataDir, [{ id: 'x', title: 'X', /* ... */ }]);

// after
await upsertUserAutomationTemplate(dataDir, { id: 'x', title: 'X', /* ... */ });
Defensive patterns

Strategy: type-guard

Validate before calling

function isPlainObject(v: unknown): v is Record<string, unknown> {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}
if (!isPlainObject(payload)) throw new Error('Template payload must be a single object, not an array or primitive.');

Type guard

function isAutomationTemplateInput(v: unknown): v is Record<string, unknown> {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

Try / catch

try {
  await upsertUserAutomationTemplate(dataDir, payload);
} catch (err) {
  if (err instanceof Error && err.message === 'automation template must be an object') {
    return badRequest('Send a single template object, not an array or string.');
  }
  throw err;
}

Prevention

When it happens

Trigger: POSTing a template as a JSON array ([{...}]) instead of a single object; double-encoding the body so it arrives as a JSON string; passing undefined from a CLI flag that took no value; sending a top-level array of stages.

Common situations: A CLI wrapping the payload in an extra set of brackets; a fetch client sending JSON.stringify([{...}]) by mistake; the consumer iterating a list and accidentally passing the list instead of an element.

Related errors


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