Skip to main content
Better Skills uses a markdown mention syntax to create typed, traversable links between skills and resources. Mentions are parsed, validated, and automatically synchronized into the graph.

Mention Syntax

There are three types of mention syntax:

Persisted Mentions (UUID-based)

Canonical storage format for graph-connected references:
[[skill:550e8400-e29b-41d4-a716-446655440000]]
These are the only valid mentions in persisted skill markdown and resource content. Path-based mentions are resolved to UUIDs before storage.

Draft Mentions (Path-based)

Used during local authoring before resources are created:
[[resource:new:references/api-guide.md]]
[[resource:new:scripts/setup.sh]]
[[resource:new:assets/diagram.png]]
Draft mentions use the :new: prefix to signal they’re not yet persisted. The CLI resolves these to UUID mentions when creating or updating skills.

Escaped Mentions

Treated as literal text, not links:
To create a mention, use \[[skill:uuid]] syntax.
Example: \[[resource:new:path/to/file]]
The backslash prefix (\) prevents parsing and link creation.

Mention Parsing

Mentions are extracted using regex with negative lookbehind to skip escaped tokens:
// From packages/markdown/persisted-mentions
const SKILL_MENTION_REGEX = /(?<!\\)\[\[skill:([a-f0-9-]{36})\]\]/g;
const RESOURCE_MENTION_REGEX = /(?<!\\)\[\[resource:([a-f0-9-]{36})\]\]/g;

// From packages/markdown/new-resource-mentions  
const NEW_RESOURCE_MENTION_REGEX = /(?<!\\)\[\[resource:new:([^\]]+)\]\]/g;
Mentions inside fenced code blocks and inline code are currently parsed. Only escaped mentions (\[[...]]) are ignored. This is a known limitation.

Auto-Linking Behavior

When skill markdown or resource content is created or updated, the system automatically:
1

Parse mentions

Extract all [[skill:uuid]] and [[resource:uuid]] mentions from the markdown.
2

Validate targets

  • Check that each UUID exists in the database
  • Verify same-vault constraint (source and target in same vault)
3

Delete old auto links

Remove existing skill_link rows where source matches and metadata.origin = "markdown-auto".
4

Insert new auto links

Create skill_link rows for each mention with kind: "mention" and metadata.origin: "markdown-auto".
// From packages/api/src/lib/link-sync.ts
export async function validateMention(
  sourceVaultId: string,
  targetId: string,
  targetType: "skill" | "resource"
) {
  const target = await db.query[targetType].findFirst({
    where: eq(schema[targetType].id, targetId),
  });

  if (!target) {
    throw new Error(`${targetType} ${targetId} not found`);
  }

  const targetVaultId = targetType === "skill"
    ? target.ownerVaultId
    : (await getSkillForResource(targetId)).ownerVaultId;

  if (sourceVaultId !== targetVaultId) {
    throw new Error(
      `Cross-vault mention: source in ${sourceVaultId}, target in ${targetVaultId}`
    );
  }
}

Same-Vault Validation Rules

Mentions must reference targets in the same vault as the source:

Valid: Same Vault

<!-- Both in 'acme-vault' -->
See [[skill:auth-uuid]] for details.

Invalid: Cross-Vault

<!-- Source in 'acme-vault', target in 'personal-vault' -->
See [[skill:personal-notes-uuid]]
Error: Cross-vault mention detected
Same-vault validation applies only to markdown-auto mentions. Manual links created via UI can reference skills in other vaults (subject to access permissions).

Why Same-Vault Only?

  1. Portability: Skills can be exported and imported without breaking internal references
  2. Access control: Vault membership determines visibility
  3. Versioning: Cross-vault links would require complex synchronization

Draft Mention Resolution

The CLI uses draft mentions during local authoring:
1

Author creates SKILL.md with draft mentions

# My API Skill

See the authentication guide: [[resource:new:references/auth.md]]
And the setup script: [[resource:new:scripts/setup.sh]]
2

CLI validates all draft paths exist

$ bun cli skills create my-api-skill
 Validating mentions...
 Found references/auth.md
 Found scripts/setup.sh
3

API creates skill and resources

Skill and resources are inserted into the database, receiving UUIDs.
4

API resolves draft mentions to UUIDs

# My API Skill

See the authentication guide: [[resource:7c9e6679-7425-40de-944b-e07fc1f90ae7]]
And the setup script: [[resource:9f8e7d6c-5b4a-3c2d-1e0f-8a7b6c5d4e3f]]
5

API creates auto links

skill_link rows are created for each resolved mention.
// From packages/markdown/new-resource-mentions
export function parseNewResourceMentions(markdown: string): string[] {
  const mentions: string[] = [];
  const regex = /(?<!\\)\[\[resource:new:([^\]]+)\]\]/g;
  
  let match;
  while ((match = regex.exec(markdown)) !== null) {
    mentions.push(match[1]); // Extract path
  }
  
  return mentions;
}

Mention Rendering

When reading skills via API, mentions are rendered into human-readable labels:

Storage Form

See [[skill:550e8400-e29b-41d4-a716-446655440000]]
and [[resource:7c9e6679-7425-40de-944b-e07fc1f90ae7]]

Rendered Form

See [turborepo](/skills/turborepo)
and [API Reference](/skills/my-api/resources/7c9e6679)
// API returns both forms
interface SkillResponse {
  id: string;
  name: string;
  originalMarkdown: string;  // UUID mentions (canonical)
  renderedMarkdown: string;  // Human-readable labels
}
The web app uses the renderedMarkdown for display, then converts back to storage form when editing.

Write Flow Summary

The complete mention lifecycle during skill creation/update:

Mention Packages

Mention utilities are split across packages:
PackageResponsibility
packages/markdown/persisted-mentionsParse and remap UUID mentions
packages/markdown/new-resource-mentionsParse and resolve :new: draft mentions
packages/markdown/editor-mentionsConvert between storage and editor forms
packages/markdown/render-persisted-mentionsRender UUID mentions to labels/links
packages/api/src/lib/mentions.tsRe-export and validation logic
packages/api/src/lib/link-sync.tsAuto link creation and sync
apps/cliDraft mention workflow for local folders
apps/webEditor mention conversion and routing
Mention parsing and rendering logic is centralized in packages/markdown. Do not duplicate this logic in app code.

Single Write Path

All auto-link writes are centralized in packages/api/src/lib/link-sync.ts:
export async function syncAutoLinksForSources(
  sources: Array<{ skillId?: string; resourceId?: string }>,
  userId: string
) {
  for (const source of sources) {
    const markdown = await getMarkdownForSource(source);
    const mentions = parsePersistedMentions(markdown);
    
    await validateSameVault(source, mentions);
    await deleteOldAutoLinks(source);
    await insertNewAutoLinks(source, mentions, userId);
  }
}
This centralization ensures consistent behavior across:
  • Skill create/update/duplicate operations
  • Resource create/update operations
  • Default skill sync from templates

Edge Cases

Mention in Code Block (Currently Parsed)

To create a mention, use:

\`\`\`markdown
[[skill:uuid]]  <!-- This is INCORRECTLY parsed as a mention -->
\`\`\`
Workaround: Use escaped mentions in code examples: \[[skill:uuid]]

Circular References (Allowed)

# Skill A
See [[skill:skill-b-uuid]]

# Skill B  
See [[skill:skill-a-uuid]]
Circular mention chains are allowed. Graph traversal should implement depth limits to prevent infinite loops.
See [[skill:uuid]] for introduction.
...
Also review [[skill:uuid]] for advanced usage.
This creates two separate auto links with the same source and target. Duplicate edges are allowed by design.

Next Steps

Skill Graph

Understand how mentions become graph edges

Skills

Learn about SKILL.md format and resources

API Reference

Explore the skills API endpoints

Build docs developers (and LLMs) love