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 Mention
Resource Mention
[[ 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:
Parse mentions
Extract all [[skill:uuid]] and [[resource:uuid]] mentions from the markdown.
Validate targets
Check that each UUID exists in the database
Verify same-vault constraint (source and target in same vault)
Delete old auto links
Remove existing skill_link rows where source matches and metadata.origin = "markdown-auto".
Insert new auto links
Create skill_link rows for each mention with kind: "mention" and metadata.origin: "markdown-auto".
Mention Validation
Auto Link Creation
// 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?
Portability : Skills can be exported and imported without breaking internal references
Access control : Vault membership determines visibility
Versioning : Cross-vault links would require complex synchronization
Draft Mention Resolution
The CLI uses draft mentions during local authoring:
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 ]]
CLI validates all draft paths exist
$ bun cli skills create my-api-skill
✓ Validating mentions...
✓ Found references/auth.md
✓ Found scripts/setup.sh
API creates skill and resources
Skill and resources are inserted into the database, receiving UUIDs.
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 ]]
API creates auto links
skill_link rows are created for each resolved mention.
Draft Mention Parsing
Draft Mention Resolution
// 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:
Package Responsibility 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.
Duplicate Mentions (Create Multiple Links)
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