n8n-io/n8n · error · InvalidRuntimeSkillError

Duplicate skill id "${normalizedSkill.id}"

Error message

Duplicate skill id "${normalizedSkill.id}"

What it means

Thrown by normalizeRuntimeSkills when two loaded skills resolve to the same `id`. The skill id defaults to the frontmatter `name`, so this typically means two SKILL.md files declare the same name (or you passed two programmatic skills with identical ids). Duplicate ids would corrupt the registry's id-keyed lookup, so the loader fails fast during normalization. Wrapped as InvalidRuntimeSkillError.

Source

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

function normalizeRuntimeSkills(skills: RuntimeSkill[]): RuntimeSkill[] {
	const sortedSkills = [...skills].sort(compareRuntimeSkills);
	const seenIds = new Set<string>();
	const seenNames = new Set<string>();
	const seenSourceDirectories = new Set<string>();

	return sortedSkills.map((skill) => {
		const normalizedSkill: RuntimeSkill = {
			...skill,
			linkedFiles: normalizeLinkedFiles(skill.linkedFiles),
		};
		const validation = validateRuntimeSkill(normalizedSkill);
		if (!validation.ok) {
			throw new InvalidRuntimeSkillError(formatSkillValidationErrors(validation.errors));
		}

		if (seenIds.has(normalizedSkill.id)) {
			throw new InvalidRuntimeSkillError(`Duplicate skill id "${normalizedSkill.id}"`);
		}
		seenIds.add(normalizedSkill.id);

		const normalizedName = normalizedSkill.name.toLowerCase();
		if (seenNames.has(normalizedName)) {
			throw new InvalidRuntimeSkillError(`Duplicate skill name "${normalizedSkill.name}"`);
		}
		seenNames.add(normalizedName);

		if (normalizedSkill.sourceDirectory) {
			if (seenSourceDirectories.has(normalizedSkill.sourceDirectory)) {
				throw new InvalidRuntimeSkillError(
					`Duplicate skill source directory "${normalizedSkill.sourceDirectory}"`,
				);
			}
			seenSourceDirectories.add(normalizedSkill.sourceDirectory);
		}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Search the skill tree for the duplicated id/name and rename one of the frontmatter `name` fields to a unique lowercase slug.
  2. Remove the stale duplicate folder if one is a leftover copy.
  3. If ids are set programmatically, ensure each RuntimeSkill.id is unique before calling createRuntimeSkillSource.

Example fix

# before — two folders both declare:
---
name: billing
---

# after — rename the second one
---
name: billing-enterprise
---
Defensive patterns

Strategy: validation

Validate before calling

function assertUniqueSkillIds(skills: RuntimeSkill[]): void {
  const seen = new Set<string>();
  for (const s of skills) {
    if (seen.has(s.id)) {
      throw new Error(`Duplicate skill id "${s.id}" — rename one before registering`);
    }
    seen.add(s.id);
  }
}

assertUniqueSkillIds(skills); // before createRuntimeSkillSource

Type guard

function hasUniqueIds(skills: RuntimeSkill[]): boolean {
  return new Set(skills.map((s) => s.id)).size === skills.length;
}

Try / catch

try {
  createRuntimeSkillSource(skills);
} catch (err) {
  if (err instanceof InvalidRuntimeSkillError && /Duplicate skill id/.test(err.message)) {
    // dedupe or rename, then retry once — not transient, so fix data first
    throw err;
  }
  throw err;
}

Prevention

When it happens

Trigger: Two skill folders each containing a SKILL.md with `name: billing`; calling createRuntimeSkillSource with two RuntimeSkill objects sharing `id`; two skills in different category subdirectories that happen to use the same short name; a symlinked/copied skill folder duplicated under another path.

Common situations: Forking a skill and forgetting to rename it; organizing skills into category folders and reusing the leaf name; a CI workspace that mounts the same skill collection twice; a rename that left the old copy in place.

Related errors


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