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

  1. Rename the skill in the recorded input so it does not collide with the existing non-skill node
  2. Inspect the existing node (store.resolveNode) and delete or re-kind it if it is truly the same concept
  3. Adjust learner instructions/prompt to steer the model away from names already used for other knowledge kinds
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/f656c63cb713bdd8. Report an issue: GitHub.