n8n-io/n8n · error · InvalidRuntimeSkillError

Duplicate skill name "${normalizedSkill.name}"

Error message

Duplicate skill name "${normalizedSkill.name}"

What it means

Thrown by normalizeRuntimeSkills when two skills collide on `name` (compared case-insensitively, so 'Billing' and 'billing' clash). Names are user-facing identifiers and must be unique across the loaded source. The check runs after id-uniqueness, so a shared name with different ids still trips it. Wrapped as InvalidRuntimeSkillError.

Source

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

	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);
		}

		return validation.skill;
	});
}

function toRegistryEntry(skill: RuntimeSkill): RuntimeSkillRegistryEntry {
	return {

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Rename one skill's frontmatter `name` to a distinct lowercase slug (remember the check is case-insensitive).
  2. If both names are intentional, namespace one (e.g. `search-internal` vs `search-web`).
  3. Re-run to confirm the collision is resolved — note this error can mask a deeper id/source-directory duplicate, so fix all reported duplicates.

Example fix

# before — two skills both named:
---
name: search
---

# after
---
name: search-web
---
Defensive patterns

Strategy: validation

Validate before calling

function assertUniqueSkillNames(skills: RuntimeSkill[]): void {
  const seen = new Set<string>();
  for (const s of skills) {
    const key = s.name.toLowerCase();
    if (seen.has(key)) {
      throw new Error(`Duplicate skill name "${s.name}" (case-insensitive)`);
    }
    seen.add(key);
  }
}

assertUniqueSkillNames(skills); // before createRuntimeSkillSource

Type guard

function hasUniqueNames(skills: RuntimeSkill[]): boolean {
  const lower = skills.map((s) => s.name.toLowerCase());
  return new Set(lower).size === lower.length;
}

Try / catch

try {
  createRuntimeSkillSource(skills);
} catch (err) {
  if (err instanceof InvalidRuntimeSkillError && /Duplicate skill name/.test(err.message)) {
    // rename the colliding skill's frontmatter name, then rebuild
    throw err;
  }
  throw err;
}

Prevention

When it happens

Trigger: Two skills with frontmatter `name: search` in different folders; a name differing only by case (`docs` vs `Docs`); importing a third-party skill pack that reuses a name already in your tree.

Common situations: Merging two skill collections that independently chose the same name; case-only differences from cross-platform filesystem moves; a vendor skill colliding with an in-house skill of the same name.

Related errors


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