Skip to main content

Agent Skills

Learn how to extend your AI agent with Agent Skills, a lightweight format for adding specialized knowledge and workflows from markdown files.

What are Agent Skills?

Agent Skills are folders containing instructions and resources that agents can load on-demand:
my-skill/
├── SKILL.md          # Required: instructions + metadata
├── scripts/          # Optional: executable code
├── references/       # Optional: documentation
└── assets/           # Optional: templates, resources

How Skills Work

Skills use progressive disclosure to manage context efficiently:
  1. Discovery: Agent loads only name and description at startup
  2. Activation: When relevant, agent reads full SKILL.md instructions
  3. Execution: Agent follows instructions, accessing bundled resources as needed

The SKILL.md File

Every skill contains a SKILL.md file with frontmatter and 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:
- Extract text or tables from PDFs
- Fill PDF forms programmatically
- Merge or split PDF files

## How to extract text

1. Install pdfplumber: `bash scripts/install-deps.sh`
2. Run extraction: `bash scripts/extract-text.sh <pdf-path>`
3. Results will be in `output/extracted.txt`

## How to fill forms

1. Use the template: `templates/form-template.json`
2. Run: `bash scripts/fill-form.sh <pdf-path> <data-json>`
Frontmatter requires:
  • name: Short identifier
  • description: When to use this skill

Prerequisites

Your agent needs:
  1. Filesystem access: Read files and directories
  2. Load skill tool: Read SKILL.md content
  3. Command execution: Optional, for skills with scripts

Implementation

Step 1: Define Sandbox Interface

Create a generic interface for filesystem operations:
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 }>;
}

Step 2: Discover Skills

Scan directories and extract metadata:
import yaml from 'js-yaml';

interface SkillMetadata {
  name: string;
  description: string;
  path: string;
}

function parseFrontmatter(content: string) {
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
  if (!match?.[1]) throw new Error('No frontmatter found');
  return yaml.load(match[1]) as { name: string; description: string };
}

export 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;
    }

    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);

        if (seenNames.has(frontmatter.name)) continue;
        seenNames.add(frontmatter.name);

        skills.push({
          name: frontmatter.name,
          description: frontmatter.description,
          path: skillDir,
        });
      } catch {
        continue;
      }
    }
  }
  return skills;
}

Step 3: Build System Prompt

Include skill descriptions in system prompt:
export 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}
`;
}

Step 4: Create Load Skill Tool

Implement the tool to load skill content:
import { tool } from 'ai';
import { z } from 'zod';

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();
}

export 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,
    };
  },
});

Step 5: Create Additional Tools

Provide tools for accessing skill resources:
export 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');
  },
});

export 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);
  },
});

Step 6: Wire Up the Agent

Combine everything using prepareCall:
import { ToolLoopAgent } from 'ai';
import { z } from 'zod';
import { loadSkillTool, readFileTool, bashTool } from './skills/tools';
import { buildSkillsPrompt } from './skills/prompt';

const callOptionsSchema = z.object({
  sandbox: z.custom<Sandbox>(),
  skills: z.array(
    z.object({
      name: z.string(),
      description: z.string(),
      path: z.string(),
    }),
  ),
});

export const agent = new ToolLoopAgent({
  model: 'openai/gpt-4o',
  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,
    },
  }),
});

Step 7: Run the Agent

import { createSandbox } from './lib/sandbox';
import { discoverSkills } from './lib/skills/discovery';
import { agent } from './lib/agent';

async function main() {
  // Create sandbox
  const sandbox = createSandbox({ workingDirectory: process.cwd() });

  // Discover skills at startup
  const skills = await discoverSkills(sandbox, [
    '.agents/skills',
    '~/.config/agent/skills',
  ]);

  console.log(`Loaded ${skills.length} skills:`);
  skills.forEach(s => console.log(`  - ${s.name}: ${s.description}`));

  // Run the agent
  const result = await agent.run({
    prompt: 'Extract text from report.pdf',
    options: { sandbox, skills },
  });

  console.log(result);
}

main();

Example Skill

Here’s a complete example skill:
---
name: web-scraping
description: Scrape websites, extract data, handle pagination, and export to JSON/CSV.
---

# Web Scraping

## When to use

Use when the user needs to:
- Extract data from websites
- Monitor website changes
- Collect structured data at scale

## Setup

Install dependencies:
```bash
bash {skillDirectory}/scripts/install.sh

Extract data from a single page

bash {skillDirectory}/scripts/scrape.sh <url> <css-selector>
Example:
bash {skillDirectory}/scripts/scrape.sh https://example.com "h1, p"

Handle pagination

Use the pagination template:
readFile {skillDirectory}/templates/pagination-config.json
Modify the config and run:
bash {skillDirectory}/scripts/scrape-paginated.sh config.json

Corresponding files:

```bash filename="skills/web-scraping/scripts/scrape.sh"
#!/bin/bash
url=$1
selector=$2
curl -s "$url" | pup "$selector" text{}
{
  "baseUrl": "https://example.com/page",
  "pageParam": "page",
  "startPage": 1,
  "maxPages": 10,
  "selector": ".item"
}

Using Skills in Next.js

Integrate with a Next.js API route:
import { streamText } from 'ai';
import { agent } from '@/lib/agent';
import { createSandbox } from '@/lib/sandbox';
import { discoverSkills } from '@/lib/skills/discovery';

export async function POST(req: Request) {
  const { prompt } = await req.json();

  const sandbox = createSandbox({ workingDirectory: process.cwd() });
  const skills = await discoverSkills(sandbox, ['.agents/skills']);

  const result = await agent.run({
    prompt,
    options: { sandbox, skills },
  });

  return result.toUIMessageStreamResponse();
}

Best Practices

Skill Design

  1. Clear descriptions: Help the agent know when to use the skill
  2. Explicit instructions: Provide step-by-step guidance
  3. Examples: Include example commands and outputs
  4. Error handling: Document common errors and solutions

Security

  1. Validate inputs: Sanitize all user inputs in skill scripts
  2. Restrict commands: Limit what bash commands can execute
  3. Sandbox environment: Run skills in isolated containers
  4. Audit trails: Log all skill executions

Organization

skills/
├── data-processing/
│   ├── SKILL.md
│   ├── scripts/
│   └── templates/
├── api-integration/
│   ├── SKILL.md
│   ├── scripts/
│   └── references/
└── code-generation/
    ├── SKILL.md
    └── templates/

Example Use Cases

  1. Code Generation: Templates and linters for different languages
  2. Data Processing: ETL workflows and data validation
  3. API Integration: Pre-configured API clients and auth flows
  4. Testing: Test generation and execution frameworks
  5. Documentation: Doc generators and formatters

Skill Discovery

Browse community skills at skills.sh

Next Steps

Resources