content/cookbook/00-guides/06-agent-skills.mdx
In this guide, you will learn how to extend your agent with Agent Skills, a lightweight, open format for adding specialized knowledge and workflows that load at runtime from markdown files.
At its core, a skill is a folder containing a SKILL.md file with metadata and instructions that tell an agent how to perform a specific task.
my-skill/
├── SKILL.md # Required: instructions + metadata
├── scripts/ # Optional: executable code
├── references/ # Optional: documentation
└── assets/ # Optional: templates, resources
Skills use progressive disclosure to manage context efficiently:
SKILL.md instructions into contextThis approach keeps agents fast while giving them access to more context on demand.
Every skill starts with a SKILL.md file containing YAML frontmatter and Markdown instructions:
---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents.
---
# PDF Processing
## When to use this skill
Use this skill when the user needs to work with PDF files...
## How to extract text
1. Use pdfplumber for text extraction...
## How to fill forms
...
The frontmatter requires:
name: A short identifierdescription: Instructions for when to use this skillThe Markdown body contains the actual skill content with no restrictions on structure or content.
To support skills, your agent needs:
SKILL.md content into contextCreate a generic sandbox interface that provides a consistent way to interact with the filesystem. This abstraction lets you implement it differently depending on your environment (Node.js fs, a containerized sandbox, cloud storage, etc.):
interface Sandbox {
readFile(path: string, encoding: 'utf-8'): Promise<string>;
readdir(
path: string,
opts: { withFileTypes: true },
): Promise<{ name: string; isDirectory(): boolean }[]>;
exec(command: string): Promise<{ stdout: string; stderr: string }>;
}
Scan skill directories and extract metadata from each SKILL.md:
interface SkillMetadata {
name: string;
description: string;
path: string;
}
async function discoverSkills(
sandbox: Sandbox,
directories: string[],
): Promise<SkillMetadata[]> {
const skills: SkillMetadata[] = [];
const seenNames = new Set<string>();
for (const dir of directories) {
let entries;
try {
entries = await sandbox.readdir(dir, { withFileTypes: true });
} catch {
continue; // Skip directories that don't exist
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillDir = `${dir}/${entry.name}`;
const skillFile = `${skillDir}/SKILL.md`;
try {
const content = await sandbox.readFile(skillFile, 'utf-8');
const frontmatter = parseFrontmatter(content);
// First skill with a given name wins (allows project overrides)
if (seenNames.has(frontmatter.name)) continue;
seenNames.add(frontmatter.name);
skills.push({
name: frontmatter.name,
description: frontmatter.description,
path: skillDir,
});
} catch {
continue; // Skip skills without valid SKILL.md
}
}
}
return skills;
}
function parseFrontmatter(content: string) {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match?.[1]) throw new Error('No frontmatter found');
// Parse YAML using your preferred library
return yaml.parse(match[1]);
}
Include discovered skills in the system prompt so the agent knows what's available:
function buildSkillsPrompt(skills: SkillMetadata[]): string {
const skillsList = skills
.map(s => `- ${s.name}: ${s.description}`)
.join('\n');
return `
## Skills
Use the \`loadSkill\` tool to load a skill when the user's request
would benefit from specialized instructions.
Available skills:
${skillsList}
`;
}
The agent sees only names and descriptions. Full instructions stay out of the context window until loaded.
The load skill tool reads the full SKILL.md and returns the body (without frontmatter):
function stripFrontmatter(content: string): string {
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
return match ? content.slice(match[0].length).trim() : content.trim();
}
const loadSkillTool = tool({
description: 'Load a skill to get specialized instructions',
inputSchema: z.object({
name: z.string().describe('The skill name to load'),
}),
execute: async ({ name }, { experimental_context }) => {
const { sandbox, skills } = experimental_context as {
sandbox: Sandbox;
skills: SkillMetadata[];
};
const skill = skills.find(s => s.name.toLowerCase() === name.toLowerCase());
if (!skill) {
return { error: `Skill '${name}' not found` };
}
const skillFile = `${skill.path}/SKILL.md`;
const content = await sandbox.readFile(skillFile, 'utf-8');
const body = stripFrontmatter(content);
return {
skillDirectory: skill.path,
content: body,
};
},
});
The tool returns the skill directory path alongside the content so the agent can construct full paths to bundled resources.
Wire up the sandbox and skills using callOptionsSchema and prepareCall:
const callOptionsSchema = z.object({
sandbox: z.custom<Sandbox>(),
skills: z.array(
z.object({
name: z.string(),
description: z.string(),
path: z.string(),
}),
),
});
const readFileTool = tool({
description: 'Read a file from the filesystem',
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }, { experimental_context }) => {
const { sandbox } = experimental_context as { sandbox: Sandbox };
return sandbox.readFile(path, 'utf-8');
},
});
const bashTool = tool({
description: 'Execute a bash command',
inputSchema: z.object({ command: z.string() }),
execute: async ({ command }, { experimental_context }) => {
const { sandbox } = experimental_context as { sandbox: Sandbox };
return sandbox.exec(command);
},
});
const agent = new ToolLoopAgent({
model: yourModel,
tools: {
loadSkill: loadSkillTool,
readFile: readFileTool,
bash: bashTool,
},
callOptionsSchema,
prepareCall: ({ options, ...settings }) => ({
...settings,
instructions: `${settings.instructions}\n\n${buildSkillsPrompt(options.skills)}`,
experimental_context: {
sandbox: options.sandbox,
skills: options.skills,
},
}),
});
// Create sandbox (your filesystem/execution abstraction)
const sandbox = createSandbox({ workingDirectory: process.cwd() });
// Discover skills at startup
const skills = await discoverSkills(sandbox, [
'.agents/skills',
'~/.config/agent/skills',
]);
// Run the agent
const result = await agent.run({
prompt: userMessage,
options: { sandbox, skills },
});
When a user asks something that matches a skill description, the agent calls loadSkill. The full instructions load into context, and the agent follows them using bash and readFile to access bundled resources.
Skills can reference files relative to their directory. The agent uses existing tools to access them:
Skill directory: /path/to/.agents/skills/my-skill
# My Skill Instructions
Read the configuration template:
templates/config.json
Run the setup script:
bash scripts/setup.sh
The agent sees the skill directory path in the tool result and prepends it when accessing templates/config.json or scripts/setup.sh. No special resource loading mechanism is needed—the agent uses the same tools it uses for everything else.