{"record":{"id":"075660fef91a50c5","repo":"n8n-io/n8n","slug":"formatskillvalidationerrors-validation-errors","errorCode":null,"errorMessage":"${formatSkillValidationErrors(validation.errors)}","messagePattern":"\\$\\{formatSkillValidationErrors\\(validation\\.errors\\)\\}","errorType":"exception","errorClass":"InvalidRuntimeSkillError","httpStatus":null,"severity":"error","filePath":"packages/@n8n/agents/src/skills/registry.ts","lineNumber":180,"sourceCode":"\t\t\treturn `${error.message}${field}${path}.${hint}`;\n\t\t})\n\t\t.join(' ');\n}\n\nfunction normalizeRuntimeSkills(skills: RuntimeSkill[]): RuntimeSkill[] {\n\tconst sortedSkills = [...skills].sort(compareRuntimeSkills);\n\tconst seenIds = new Set<string>();\n\tconst seenNames = new Set<string>();\n\tconst seenSourceDirectories = new Set<string>();\n\n\treturn sortedSkills.map((skill) => {\n\t\tconst normalizedSkill: RuntimeSkill = {\n\t\t\t...skill,\n\t\t\tlinkedFiles: normalizeLinkedFiles(skill.linkedFiles),\n\t\t};\n\t\tconst validation = validateRuntimeSkill(normalizedSkill);\n\t\tif (!validation.ok) {\n\t\t\tthrow new InvalidRuntimeSkillError(formatSkillValidationErrors(validation.errors));\n\t\t}\n\n\t\tif (seenIds.has(normalizedSkill.id)) {\n\t\t\tthrow new InvalidRuntimeSkillError(`Duplicate skill id \"${normalizedSkill.id}\"`);\n\t\t}\n\t\tseenIds.add(normalizedSkill.id);\n\n\t\tconst normalizedName = normalizedSkill.name.toLowerCase();\n\t\tif (seenNames.has(normalizedName)) {\n\t\t\tthrow new InvalidRuntimeSkillError(`Duplicate skill name \"${normalizedSkill.name}\"`);\n\t\t}\n\t\tseenNames.add(normalizedName);\n\n\t\tif (normalizedSkill.sourceDirectory) {\n\t\t\tif (seenSourceDirectories.has(normalizedSkill.sourceDirectory)) {\n\t\t\t\tthrow new InvalidRuntimeSkillError(\n\t\t\t\t\t`Duplicate skill source directory \"${normalizedSkill.sourceDirectory}\"`,\n\t\t\t\t);","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/skills/registry.ts#L162-L198","documentation":"Thrown by normalizeRuntimeSkills after parsing succeeds, when the assembled RuntimeSkill fails the deeper validateRuntimeSkill check. That check requires non-empty string values for id, name, description, and instructions. Unlike error 102 (markdown/frontmatter parsing), this guards the in-memory object shape — e.g. an empty instructions body after trim, or a programmatically-constructed skill missing a required field. The raw validation messages are surfaced without a directory prefix.","triggerScenarios":"Loading skills programmatically via createRuntimeSkillSource(skills) with a hand-built RuntimeSkill whose `instructions` is an empty string; a SKILL.md whose frontmatter is valid but body is blank (so instructions trims to ''); passing a skill object missing the `id` field.","commonSituations":"Building a RuntimeSkill in code instead of from markdown and forgetting instructions; a skill file that is all frontmatter and no body; a test fixture with a placeholder empty body; an upstream transform that stripped the body.","solutions":["Read the surfaced validation messages to see which of id/name/description/instructions is empty or missing.","If the skill came from a SKILL.md, add non-empty content below the closing frontmatter delimiter (this becomes `instructions`).","If constructed in code, populate all four required string fields with non-empty values before calling createRuntimeSkillSource/createRuntimeSkillRegistry."],"exampleFix":"// before\ncreateRuntimeSkillSource([{\n  id: 'billing', name: 'billing', description: 'Billing help', instructions: '',\n}]);\n\n// after\ncreateRuntimeSkillSource([{\n  id: 'billing', name: 'billing', description: 'Billing help',\n  instructions: 'Step-by-step billing guidance...',\n}]);","handlingStrategy":"validation","validationCode":"import { validateRuntimeSkill, type RuntimeSkill } from '@n8n/agents/skills';\n\nfunction assertSkillObject(skill: RuntimeSkill): void {\n  const result = validateRuntimeSkill(skill);\n  if (!result.ok) {\n    throw new Error(`Invalid skill object: ${JSON.stringify(result.errors)}`);\n  }\n}\n\n// Call before createRuntimeSkillSource:\nskills.forEach(assertSkillObject);","typeGuard":"function isCompleteSkill(s: Partial<RuntimeSkill>): s is RuntimeSkill {\n  return ['id', 'name', 'description', 'instructions'].every(\n    (f) => typeof s[f] === 'string' && (s[f] as string).trim().length > 0,\n  );\n}\n\nif (!isCompleteSkill(maybeSkill)) throw new Error('Skill missing required non-empty string fields');","tryCatchPattern":"try {\n  createRuntimeSkillSource(skills);\n} catch (err) {\n  if (err instanceof InvalidRuntimeSkillError) {\n    // normalization-time validation error — fix the skill object, do not retry\n    throw new Error(`Skill normalization failed: ${err.message}`);\n  }\n  throw err;\n}","preventionTips":["When building RuntimeSkill objects in code, always populate id, name, description, AND a non-empty instructions body.","If sourcing instructions from a template, assert the rendered string is non-empty before constructing the skill.","Run validateRuntimeSkill() on each skill before passing the array to createRuntimeSkillSource."],"tags":["skills","validation"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}