mastra-ai/mastra · error · Error
Knowledge node is not a skill: ${normalizedName}
Error message
Knowledge node is not a skill: ${normalizedName} What it means
The learner agent's record-skill tool refuses to proceed when the requested skill name already exists in the knowledge store but as a non-skill node (a different knowledge 'kind'). The library throws this to prevent silently overwriting or conflating distinct knowledge types under one name. Names are normalized before lookup, so the collision is checked against the canonical form.
Source
Thrown at packages/memory/src/processors/observational-memory/subconscious/learn.ts:104
} satisfies JSONSchema7,
execute: async raw => {
const value = raw as { name: string; procedure: string; sourceRecordIds: string[] };
const sourceIds = [...new Set(value.sourceRecordIds)];
const pending = new Map(input.pendingRecords.map(record => [record.id, record]));
if (sourceIds.length < 2 || sourceIds.some(id => !pending.has(id))) {
throw new Error('Skill evidence requires at least two distinct records from the pending learner worklist.');
}
const normalizedName = value.name.trim();
if (
input.state.recordedName &&
input.state.recordedName.toLocaleLowerCase() !== normalizedName.toLocaleLowerCase()
) {
throw new Error('The learner may record at most one skill per reflection.');
}
input.state.recordedName = normalizedName;
const nodeScope = expandKnowledgeScope(input.scope, input.defaultScope);
let node = await input.store.resolveNode({ name: normalizedName, scope: input.scope });
if (node && node.kind !== 'skill') throw new Error(`Knowledge node is not a skill: ${normalizedName}`);
node ??= await input.store.createNode({ name: normalizedName, kind: 'skill', scope: nodeScope });
const evidence = [];
for (const sourceId of sourceIds) {
const id = evidenceRecordId(sourceId, normalizedName);
const existing = await input.store.getKnowledge({ id });
if (existing) {
evidence.push(existing);
continue;
}
const source = pending.get(sourceId)!;
try {
evidence.push(
await input.store.appendKnowledge({
id,
node: node.id,
text: `Procedure: ${value.procedure.trim()} Evidence source: ${source.id}.`,
scope: source.scope,
sourceThreadId: `subconscious:${input.parentThreadId}:learn`,View on GitHub (pinned to 75dd419e61)
Solutions
- Rename the skill in the recorded input so it does not collide with the existing non-skill node
- Inspect the existing node (store.resolveNode) and delete or re-kind it if it is truly the same concept
- Adjust learner instructions/prompt to steer the model away from names already used for other knowledge kinds
- Enable at most one skill per reflection (already enforced) and retry the reflection after cleanup
Example fix
// before: tool input reuses a name held by a fact node
await recordSkillTool.execute({ name: 'deploy-rollback', ... });
// Error: Knowledge node is not a skill: deploy-rollback
// after: resolve and remove/re-kind the stale node first
const node = await store.resolveNode({ name: 'deploy-rollback', scope });
if (node && node.kind !== 'skill') await store.deleteNode({ id: node.id }); Defensive patterns
Strategy: validation
Validate before calling
const node = await store.resolveNode({ name: normalizedName, scope });
if (node && node.kind !== 'skill') {
throw new Error(`Name '${normalizedName}' is taken by a ${node.kind} node; choose another skill name.`);
} Type guard
function isSkillNode(node) {
return !!node && node.kind === 'skill';
} Try / catch
try {
await recordSkillTool.execute(input);
} catch (err) {
if (err.message.startsWith('Knowledge node is not a skill')) {
// rename the skill or clean up the colliding node, then retry
} else throw err;
} Prevention
- Resolve the node before recording and branch on kind
- Keep skill names namespaced (e.g. 'skill/deploy-rollback') to avoid collisions with other knowledge kinds
- Steer learner instructions to check existing nodes before naming
- Clean up re-kindled or stale nodes after prompt/schema changes
When it happens
Trigger: Calling the record-skill tool (via the subconscious learner reflection) with a normalizedName that resolves to an existing knowledge node whose kind !== 'skill'; e.g. the same name was previously recorded as a fact/insight kind.
Common situations: Reusing a name across kinds after changing the learner instructions or schema; a previous run stored the concept under a different node kind; hand-seeded knowledge nodes in the store that collide with skill names the model chooses.
Related errors
- Pin limit reached: the set holds at most ${options.maxPins}.
- Pin budget exceeded: the pin set is limited to ${options.max
- Pin not found: ${recordId}
- Record is not a pin: ${recordId}
- AGENT_FS_ROUTING_SUBAGENT_NAME_COLLISION
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/f656c63cb713bdd8.
Report an issue: GitHub.