n8n-io/n8n · error · InvalidRuntimeSkillError

Invalid skill at

Error message

Invalid skill at ${sourceDirectory}: ${formatSkillValidationErrors(parsed.errors)}

What it means

Thrown by loadRuntimeSkillsFromDirectory while parsing each SKILL.md via parseRuntimeSkillMarkdown. It fires when the file's YAML frontmatter or body fails parsing — e.g. missing frontmatter delimiters, invalid YAML, an unknown field, a name that breaks the skill-name pattern, or a missing required 'name'/'description'. The error lists the source directory and every validation message via formatSkillValidationErrors. Wrapped as InvalidRuntimeSkillError.

Solutions

  1. Open the SKILL.md at the source directory named in the message and start the file with a `---`-delimited YAML frontmatter block.
  2. Ensure frontmatter has required `name` (lowercase, matching /^[a-z0-9][a-z0-9._-]{0,63}$/) and `description` (non-empty string), plus a non-empty instruction body below the closing `---`.
  3. Remove or rename any unknown frontmatter keys; move extension data into the `metadata` object if you need it.
  4. Re-run; the error message enumerates each remaining problem field-by-field.

Example fix

# skills/billing/SKILL.md  (before)
---
Name: Billing
Descripton: Billing help
---

# skills/billing/SKILL.md  (after)
---
name: billing
description: Billing help
---
Instructions for the billing skill.
Defensive patterns

Strategy: validation

Validate before calling

import { readFileSync } from 'fs';
import { parseRuntimeSkillMarkdown } from '@n8n/agents/skills'; // or relative path

function prevalidateSkillFile(skillPath: string): void {
  const content = readFileSync(skillPath, 'utf-8');
  const result = parseRuntimeSkillMarkdown(content, { sourcePath: skillPath });
  if (!result.ok) {
    throw new Error(`Refusing to load ${skillPath}: ${JSON.stringify(result.errors)}`);
  }
}

Type guard

function hasValidFrontmatter(content: string): boolean {
  const lines = content.split(/\r?\n/);
  return lines[0]?.trim() === '---' && lines.slice(1).some((l, i) => i > 0 && l.trim() === '---');
}

if (!hasValidFrontmatter(readFileSync(skillPath, 'utf-8'))) {
  throw new Error(`${skillPath} missing --- delimited YAML frontmatter`);

Try / catch

import { InvalidRuntimeSkillError } from '@n8n/agents/skills';

try {
  loadRuntimeSkillSourceFromDirectory(rootDir);
} catch (err) {
  if (err instanceof InvalidRuntimeSkillError) {
    // parse error — log and skip / fail the build, do not retry
    console.error(`Skill load failed: ${err.message}`);
    process.exit(1);
  }
  throw err;
}

Prevention

When it happens

Trigger: A SKILL.md whose first line is not `---`; frontmatter missing the closing `---`; a YAML syntax error (bad indentation, unquoted colon); an unknown frontmatter key (typo like `descripton`); a `name` with uppercase letters, spaces, or >64 chars; a missing `name` or `description` field; body that is empty/whitespace (instructions required).

Common situations: Authoring a new skill and misspelling a frontmatter key; copy-pasting from a non-skill template that lacks frontmatter; editor auto-stripping the leading `---`; renaming a skill to a display name with spaces instead of the lowercase slug; CI loading a skill directory that contains a stale draft SKILL.md.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/ac4ab987dfffa12a. Report an issue: GitHub.

Appendix: source

Thrown at packages/@n8n/agents/src/skills/registry.ts:142

	if (!existsSync(rootDir) || !statSync(rootDir).isDirectory()) return [];

	return collectSkillFiles(rootDir).map((skillPath) => {
		const skillDir = dirname(skillPath);
		const sourceDirectory = toPosixPath(relative(rootDir, skillDir));
		validateRuntimeSkillFolder(skillDir, skillPath, sourceDirectory);

		const content = readFileSync(skillPath, 'utf-8');
		const parsed = parseRuntimeSkillMarkdown(content, {
			sourceName: basename(skillDir),
			path: toPosixPath(skillPath),
			sourcePath: toPosixPath(skillPath),
			directory: toPosixPath(skillDir),
			sourceDirectory,
			category: categoryFor(sourceDirectory),
		});

		if (!parsed.ok) {
			throw new InvalidRuntimeSkillError(
				`Invalid skill at ${sourceDirectory}: ${formatSkillValidationErrors(parsed.errors)}`,
			);
		}

		return {
			...parsed.skill,
			linkedFiles: loadLinkedFiles(skillDir),
		};
	});
}

export function formatSkillValidationErrors(
	errors: Array<{ message: string; field?: string; path?: string; hint?: string }>,
): string {
	return errors
		.map((error) => {
			const field = error.field ? ` field "${error.field}"` : '';
			const path = error.path ? ` (${error.path})` : '';

View on GitHub (pinned to 5ac6606e81)