n8n-io/n8n · error · InvalidRuntimeSkillError
${formatSkillValidationErrors(validation.errors)}
Error message
${formatSkillValidationErrors(validation.errors)} What it means
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.
Source
Thrown at packages/@n8n/agents/src/skills/registry.ts:180
return `${error.message}${field}${path}.${hint}`;
})
.join(' ');
}
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}"`,
);View on GitHub (pinned to 5ac6606e81)
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.
Example fix
// before
createRuntimeSkillSource([{
id: 'billing', name: 'billing', description: 'Billing help', instructions: '',
}]);
// after
createRuntimeSkillSource([{
id: 'billing', name: 'billing', description: 'Billing help',
instructions: 'Step-by-step billing guidance...',
}]); Defensive patterns
Strategy: validation
Validate before calling
import { validateRuntimeSkill, type RuntimeSkill } from '@n8n/agents/skills';
function assertSkillObject(skill: RuntimeSkill): void {
const result = validateRuntimeSkill(skill);
if (!result.ok) {
throw new Error(`Invalid skill object: ${JSON.stringify(result.errors)}`);
}
}
// Call before createRuntimeSkillSource:
skills.forEach(assertSkillObject); Type guard
function isCompleteSkill(s: Partial<RuntimeSkill>): s is RuntimeSkill {
return ['id', 'name', 'description', 'instructions'].every(
(f) => typeof s[f] === 'string' && (s[f] as string).trim().length > 0,
);
}
if (!isCompleteSkill(maybeSkill)) throw new Error('Skill missing required non-empty string fields'); Try / catch
try {
createRuntimeSkillSource(skills);
} catch (err) {
if (err instanceof InvalidRuntimeSkillError) {
// normalization-time validation error — fix the skill object, do not retry
throw new Error(`Skill normalization failed: ${err.message}`);
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid skill at ${sourceDirectory}: ${formatSkillValidation
- Invalid skill at ${sourceDirectory}: ${errors.join('; ')}
- Cannot decrease maxIterations when resuming a run. Expected
- Checkpoint for runId ${this.runId} has pending tool calls —
- Model ID is required
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/075660fef91a50c5.
Report an issue: GitHub.