Skills
Markdown-based skill loading for Crux agents. Compatible with skills.sh.
import { registry, skill, skillsSh } from '@crux/core/skill'Skills are Markdown instruction sets that an LLM loads on-demand. When placed in a prompt's use array, the resolution pipeline generates a skill index in the system prompt and injects LoadSkill/LoadReference tools. Loaded skills are injected at the system prompt level via executor re-resolution.
skill.inline(config)
Create a skill from inline text.
| Field | Type | Description |
|---|---|---|
id | string | Required. Skill identifier |
description | string | Required. One-line description shown in index |
instructions | string | Required. The skill's instruction content |
references | Record<string, string>? | Optional reference files (name -> content) |
Returns: Skill — frozen object with _tag: 'Skill'.
const tone = skill.inline({
id: 'tone',
description: 'Writing tone guidelines',
instructions: 'Always write in a warm professional tone.',
})skill.fromFile(path)
Load a SKILL.md file. Reads synchronously at import time. Parses YAML frontmatter.
| Parameter | Type | Description |
|---|---|---|
path | string | Path to the SKILL.md file |
Automatically discovers .md files in a sibling references/ directory.
Throws: SkillLoadError if file not found or frontmatter invalid.
const seo = skill.fromFile('./skills/seo-analysis/SKILL.md')SKILL.md Format
Follows the skills.sh community standard:
---
name: seo-analysis
description: Analyze and optimize content for search engines
version: 1.0.0
license: Apache-2.0
tags: seo, content
---
# SEO Analysis
Instructions here...Parsed fields: name, description, version, license, tags.
Ignored fields: allowed-tools, model, argument-hint, user-invocable (IDE-specific).
skill.fromRegistry(registry, path)
Load a skill from a registry. Content is fetched lazily on first prompt.resolve(), then cached in-memory.
| Form | Type | Description |
|---|---|---|
skill.fromRegistry(registry, path) | Registry, string | Binds the registry by reference at the call site and loads the registry-local path. |
// From skills.sh (built-in)
const seo = skill.fromRegistry(skillsSh, 'mattpocock/skills/seo-analysis')
// From a custom registry value
const acme = registry({ name: 'acme', baseUrl: 'https://skills.acme.corp' })
const brand = skill.fromRegistry(acme, 'brand-guidelines')Throws: SkillLoadError on missing path, network failure, 404, or registry protocol errors.
registry(config)
Define a custom skill registry using the .well-known/agent-skills/ protocol.
| Field | Type | Description |
|---|---|---|
name | string | Registry name (used as prefix) |
baseUrl | string | Registry base URL |
auth | () => string? | Optional auth token provider |
const acme = registry({
name: 'acme',
baseUrl: 'https://skills.acme.corp',
auth: () => process.env.SKILLS_TOKEN,
})registry() returns a registry value. Keep exported registry values in normal TypeScript source so local tooling can inspect them; do not repeat registries in crux.config.ts. Runtime code should create registry-backed skills from the registry value directly because Crux does not scan project files to locate custom registries.
export const acme = registry({
name: 'acme',
baseUrl: 'https://skills.acme.corp',
})
export const brand = skill.fromRegistry(acme, 'brand-guidelines')Skill Properties
| Property | Type | Description |
|---|---|---|
_tag | 'Skill' | Discriminant for ContextEntry union |
id | string | Skill identifier |
description | string | Description shown in index |
instructions | string | Full instruction content |
references | SkillReference[] | Bundled reference files |
meta | SkillMeta | Parsed frontmatter metadata |
dump() | () => string | Raw instruction text (exits skill system) |
How It Works
- Skills in
use: [...]are separated from regular contexts during resolution - A skill index context is auto-generated (priority 90) listing all skills
LoadSkillandLoadReferencetools are injected into the resolved tool set- When the LLM calls
LoadSkill(name):- adapter path (Anthropic/OpenAI/Google): Executor intercepts the tool call, re-resolves the prompt, injects skill content into the system prompt, continues the tool loop. Does not count against
maxSteps. - @crux/ai path (Vercel AI SDK):
wrapGeneratemiddleware detects newly activated skills and injects content into the system prompt on the next model step. - Convex Agent path:
convexAgent()stores a skill activation snapshot and re-resolves on later turns, picking up activated skills automatically.
- adapter path (Anthropic/OpenAI/Google): Executor intercepts the tool call, re-resolves the prompt, injects skill content into the system prompt, continues the tool loop. Does not count against
LoadReference(skillName, refName)returns reference content as a normal tool result.
createSkillActivationSession(options)
Creates the internal session boundary that powers skill loading. Most app code does not need to call it directly, but external framework adapters can use it to share the same semantics as Crux adapters.
import { createSkillActivationSession } from '@crux/core/skill'
const session = createSkillActivationSession({
skills,
initial: {
activeSkillIds: ['seo'],
injectedSkillIds: ['seo'],
},
})
session.activate('brand')
session.resolveInput({ message: 'Draft a landing page' })
session.loadedContexts()
session.tools()
session.snapshot()createSkillActivationSession.forTarget({ skills, target, persistence })
loads a SkillActivationSnapshot from a SkillActivationPersistence port
before creating the session.
SkillLoadError
Thrown when skill loading fails. Extends Error with _tag: 'SkillLoadError'.
import { SkillLoadError } from '@crux/core/skill'| Property | Type | Description |
|---|---|---|
skillId | string | The skill that failed to load |
reason | string | Human-readable error reason |
createAgentSkillKit(prompt, options)
Helper for agent frameworks that manage their own tool loop (Convex Agent, Mastra, etc.). Handles pre-resolve, tool extraction, persistence wrapping, and input enhancement.
| Parameter | Type | Description |
|---|---|---|
prompt | Prompt | A Crux prompt with skills in its use array |
options | AgentSkillKitOptions | { target, persistence, resolveInput? } snapshot port |
Returns: AgentSkillKit with:
| Property | Type | Description |
|---|---|---|
tools | Record<string, SkillToolDef> | LoadSkill (with persistence) + LoadReference |
resolveInput(baseInput) | (input) => Promise<input> | Enhances input with _crux_activeSkills |
getActiveIds() | () => Promise<string[]> | Current active skill IDs |
Only needed for external agent frameworks (Convex Agent, Mastra, etc.) that control their own tool loop. Not needed for @crux/anthropic, @crux/openai, @crux/google, or @crux/ai — those handle re-resolution automatically.
See the Agent framework integration guide for a complete end-to-end example with Convex Agent.
import { createAgentSkillKit } from '@crux/core/skill'
// In your agent factory function (called per conversation/thread):
const kit = await createAgentSkillKit(myPrompt, {
target: { threadId },
persistence: {
load: async () => {
// Called before pre-resolve and each later resolveInput() call.
return (await store.get('activeSkills')) ?? null
},
save: async (_target, snapshot) => {
// Called after LoadSkill succeeds.
await store.set('activeSkills', snapshot)
},
},
})
// Merge skill tools with your agent's tools
const allTools = { ...myTools, ...kit.tools }
// In your context handler / per-turn resolve:
const resolved = await myPrompt.resolve({
input: await kit.resolveInput(dynamicData),
})Instrumentation Hooks
| Hook | Fired when |
|---|---|
onSkillLoad | LoadSkill tool is intercepted |
onSkillCacheHit | Registry skill served from cache |
onSkillCacheMiss | Registry skill requires fresh fetch |
onSkillResolve | Skill content injected into system prompt |
Related
- Guide: Skills overview
- Guide: Writing skills
- Guide: Registries
- Guide: Agent framework integration
- Reference: Agent