# Pydantic AI - Agent Skills
> A standardized, composable framework for building and managing Agent Skills within the Pydantic AI ecosystem
A lightweight Python library implementing the Agent Skills specification (https://agentskills.io/specification) for Pydantic AI.
Skills are modular collections of instructions, scripts, and resources that extend AI agent
capabilities through progressive disclosure (load-on-demand to reduce token usage).
# Documentation
# Pydantic AI Skills
A standardized, composable framework for building and managing Agent Skills within the Pydantic AI ecosystem.
## What are Agent Skills?
Agent Skills are **modular collections of instructions, scripts, and resources** that extend AI agents with specialized capabilities. Instead of hardcoding features, skills are discovered and loaded on-demand, keeping your agent's context lean and focused.
## Key Features
- **Progressive Discovery**: Load skills only when needed, reducing token usage
- **Modular Design**: Self-contained skill directories with instructions and resources
- **Skill Registries**: Discover and install skills from Git repositories and other remote sources
- **Script Execution**: Include Python scripts that agents can execute
- **Resource Management**: Support for documentation and data files
- **Skill Selection**: Expose a per-agent subset of a shared library with `include` / `exclude`
- **Type-Safe**: Built on Pydantic AI's type-safe foundation
- **Simple Integration**: Drop-in toolset for Pydantic AI agents
- **Capabilities Integration**: Preferred via `SkillsCapability` and `capabilities=[...]`
Comparing options? See [Why pydantic-ai-skills](https://dougtrajano.github.io/pydantic-ai-skills/comparison/index.md).
## Quick Example
```python
from pydantic_ai import Agent
from pydantic_ai_skills import SkillsCapability
# Create agent with skills
agent = Agent(
model='openai:gpt-5.2',
instructions="You are a helpful research assistant.",
capabilities=[SkillsCapability(directories=['./skills'])],
)
# Use agent - skills tools are automatically available
result = await agent.run(
"What are the last 3 papers on arXiv about machine learning?"
)
print(result.output)
```
## Capabilities API Example
`SkillsCapability` is the preferred integration path.
It bundles `SkillsToolset` behavior and instruction injection through Pydantic AI's Capability API. If you use `SkillsToolset` directly, instructions are injected automatically.
```python
from pydantic_ai import Agent
from pydantic_ai_skills import SkillsCapability
agent = Agent(
model='openai:gpt-5.2',
capabilities=[
SkillsCapability(directories=['./skills'])
],
)
```
## Direct SkillsToolset Example
You can also use `SkillsToolset` directly when you need explicit control.
```python
from pydantic_ai import Agent
from pydantic_ai_skills import SkillsToolset
skills_toolset = SkillsToolset(directories=['./skills'])
agent = Agent(
model='openai:gpt-5.2',
instructions='You are a helpful research assistant.',
toolsets=[skills_toolset],
)
```
## How It Works
1. **Discovery**: The toolset scans specified directories for skills (folders with `SKILL.md` files)
1. **Registration**: Skills are registered as tools on your agent
1. **Progressive Loading**: Agents can:
- List all available skills with `list_skills()`
- Load detailed instructions with `load_skill(name)`
- Read additional resources with `read_skill_resource(skill_name, resource_name)`
- Execute scripts with `run_skill_script(skill_name, script_name, args)`
## Benefits
- **Cleaner Prompts**: Keep agent instructions focused instead of concatenating every possible feature
- **Scalability**: Add new capabilities by creating new skill folders
- **Maintainability**: Update skills independently without touching agent code
- **Reusability**: Share skills across multiple agents and projects
- **Efficiency**: Reduce token usage by loading skills only when needed
- **Testability**: Test and debug skills in isolation
## Security considerations
We strongly recommend that you use Skills only from trusted sources: those you created yourself or obtained from trusted sources. Skills provide AI Agents with new capabilities through instructions and code, and while this makes them powerful, it also means a malicious Skill can direct agents to invoke tools or execute code in ways that don't match the Skill's stated purpose.
Warning
If you must use a Skill from an untrusted or unknown source, exercise extreme caution and thoroughly audit it before use. Depending on what access agents have when executing the Skill, malicious Skills could lead to data exfiltration, unauthorized system access, or other security risks.
## llms.txt
The Pydantic AI Skills documentation is available in the [llms.txt](https://llmstxt.org/) format. This format is defined in Markdown and is suited for LLMs, AI coding assistants, and agents.
Two formats are available:
- [`llms.txt`](https://dougtrajano.github.io/pydantic-ai-skills/llms.txt): a file containing a brief description of the project, along with links to the different sections of the documentation. The structure of this file is described in detail [here](https://llmstxt.org/#format).
- [`llms-full.txt`](https://dougtrajano.github.io/pydantic-ai-skills/llms-full.txt): Similar to `llms.txt`, but with the linked documentation content included inline. This file may be too large for some LLMs.
As of today, these files are not automatically leveraged by most IDEs or coding agents, but they can use them if you provide a link or the full text.
## Next Steps
- [Quick Start](https://dougtrajano.github.io/pydantic-ai-skills/quick-start/index.md) - Build your first skill-enabled agent
- [Creating Skills](https://dougtrajano.github.io/pydantic-ai-skills/creating-skills/index.md) - Learn how to create custom skills
- [Programmatic Skills](https://dougtrajano.github.io/pydantic-ai-skills/programmatic-skills/index.md) - Create skills in Python code
- [Skill Registries](https://dougtrajano.github.io/pydantic-ai-skills/registries/index.md) - Load skills from Git repositories and remote sources
- [API Reference](https://dougtrajano.github.io/pydantic-ai-skills/api/toolset/index.md) - Detailed API documentation
## References
This package is inspired by:
- [Agent Skills Specification](https://agentskills.io/specification)
- [Using skills with Deep Agents](https://blog.langchain.com/using-skills-with-deep-agents/) Note
⚠️ **Only use skills from trusted sources.** Since skills provide agents with new capabilities through instructions and executable code, malicious skills could direct agents to invoke tools unexpectedly or access sensitive data. Always audit skills before use. See [Security & Deployment](https://dougtrajano.github.io/pydantic-ai-skills/security/index.md) for detailed guidance
# Advanced Features
Advanced patterns and features for sophisticated skill systems.
**View the complete example:** [advanced_usage.py](https://github.com/dougtrajano/pydantic-ai-skills/blob/main/examples/advanced_usage.py)
## Video Tutorial
Watch the Advanced Usage Tutorial for in-depth demonstrations of advanced skill integration patterns and decorator techniques:
\[
\](../assets/advanced_usage.mp4)
## Selecting Which Skills to Expose
One skill library often serves several agents. `include` and `exclude` scope the catalog per agent without duplicating the library on disk.
```python
from pydantic_ai_skills import SkillsCapability
research = SkillsCapability(directories=['./skills'], include=['arxiv-search', 'web-research'])
support = SkillsCapability(directories=['./skills'], exclude=['arxiv-search'])
```
| Configuration | Skills in the catalog |
| -------------------- | ---------------------- |
| Neither option | All discovered skills |
| `include=['a', 'b']` | Only `a` and `b` |
| `include=[]` | No skills |
| `exclude=['a', 'b']` | All except `a` and `b` |
| `exclude=[]` | All discovered skills |
Behavior:
- `include` and `exclude` cannot be combined; doing so raises `ValueError`.
- A name matching no discovered skill raises `ValueError` at construction, so typos surface early.
- Selection applies to every source — programmatic skills, directories, and registries.
- `reload()` and `auto_reload` re-apply the selection.
- A bare string (`include='arxiv-search'`) raises `TypeError` rather than matching per character.
Both options work on `SkillsToolset` as well:
```python
from pydantic_ai_skills import SkillsToolset
toolset = SkillsToolset(directories=['./skills'], exclude=['web-research'])
```
And in declarative agent specs:
```yaml
capabilities:
- SkillsCapability:
directories: ['./skills']
include:
- arxiv-search
```
> Selection controls what the model sees in the catalog. It is not a filesystem permission or an access-control boundary — see [Security](https://dougtrajano.github.io/pydantic-ai-skills/security/index.md). To restrict what loaded skills can *do*, use `exclude_tools` (for example `exclude_tools=['run_skill_script']`).
## Hot-Reload (Runtime Skill Discovery)
`SkillsCapability` is the preferred integration path. This section focuses on `SkillsToolset` because hot-reload controls (`reload()`, `auto_reload`) are configured on the underlying toolset.
Long-lived server processes (FastAPI, Starlette, etc.) may need to pick up skill edits — made by the agent itself, a git-sync job, or a human — without restarting. `SkillsToolset` supports this via `reload()` and the `auto_reload` parameter.
> **Note:** `reload()` always preserves programmatic skills registered via `skills=[]` or `@toolset.skill`. Only filesystem/registry skills are re-discovered.
### Auto-reload (recommended for most cases)
Pass `auto_reload=True` to re-scan directories automatically before every agent run:
```python
from pydantic_ai import Agent
from pydantic_ai_skills import SkillsToolset
skills_toolset = SkillsToolset(
directories=["./workspace/skills"],
auto_reload=True,
)
agent = Agent(
model="openai:gpt-4o",
toolsets=[skills_toolset],
)
# Every agent.run() call sees the current state of ./workspace/skills/
```
### Manual reload (for fine-grained control)
Call `toolset.reload()` yourself — e.g. after a git-sync in a lifespan handler:
```python
from contextlib import asynccontextmanager
from fastapi import FastAPI
from pydantic_ai_skills import SkillsToolset
skills_toolset = SkillsToolset(directories=["./workspace/skills"])
@asynccontextmanager
async def lifespan(app: FastAPI):
await git_sync() # sync skills from remote repo
skills_toolset.reload() # pick up the freshly synced files
yield
app = FastAPI(lifespan=lifespan)
```
### Reloading registry skills
By default, `reload()` preserves already-loaded registry skills from the initial cache without making any network or git calls. To re-fetch fresh skills from registries, pass `include_registries=True`:
```python
skills_toolset.reload(include_registries=True)
```
### Priority after reload
The priority order is identical to the initial load:
1. **Programmatic skills** (`skills=[]` param, `@toolset.skill` decorator) — always highest
1. **Directory skills** — fresh filesystem scan
1. **Registry skills** — always re-applied from cache; pass `include_registries=True` to refresh that cache from registries
______________________________________________________________________
## Skill Decorator Pattern
### @toolset.skill() Decorator
The `@toolset.skill()` decorator enables defining skills directly on a `SkillsToolset` instance. This approach is ideal when:
- Skills are tightly coupled with your agent initialization
- You want to define skills inline without separate files
- Skills depend on runtime dependencies available in your agent
### Basic Example
```python
from pydantic_ai import Agent, RunContext
from pydantic_ai_skills import SkillsToolset
skills = SkillsToolset()
@skills.skill()
def data_analyzer() -> str:
"""Analyze data from various sources."""
return """
# Data Analysis Skill
Use this skill to analyze datasets, generate statistical insights, and create visualizations.
## Instructions
1. Load the skill with `load_skill`
2. Access available resources with `read_skill_resource`
3. Execute analysis scripts with `run_skill_script`
## Key Capabilities
- Statistical summaries and distributions
- Correlation and trend analysis
- Data transformation and aggregation
- Report generation
"""
agent = Agent(
model='openai:gpt-4o',
toolsets=[skills]
)
result = agent.run_sync('Analyze the quarterly sales data')
```
### Decorator Parameters
```python
@toolset.skill(
name='custom-name', # Override function name (default: normalize_skill_name(func_name))
description='...', # Override docstring-based description
license='Apache-2.0', # License information
compatibility='Python 3.10+', # Environment requirements
metadata={'version': '1.0'}, # Custom metadata fields
resources=[...], # Initial resources
scripts=[...] # Initial scripts
)
def my_skill() -> str:
return "..."
```
#### Parameters Explained
| Parameter | Type | Required | Notes |
| --------------- | --------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `name` | `str` | No | Defaults to function name with underscores converted to hyphens. Must match pattern `^[a-z0-9]+(-[a-z0-9]+)*$` and be ≤64 chars. |
| `description` | `str` | No | Extracted from function docstring if not provided. Max 1024 characters. |
| `license` | `str` | No | License identifier (e.g., "Apache-2.0", "MIT"). Included in skill metadata. |
| `compatibility` | `str` | No | Environment/dependency requirements (e.g., "Requires git, docker, internet access"). Max 500 chars. |
| `metadata` | `dict` | No | Custom key-value pairs preserved in skill metadata. Useful for versioning or custom properties. |
| `resources` | `list[SkillResource]` | No | Initial resources attached to the skill. Can be extended with `@skill.resource`. |
| `scripts` | `list[SkillScript]` | No | Initial scripts attached to the skill. Can be extended with `@skill.script`. |
### Adding Resources and Scripts
The decorator returns a `SkillWrapper` that allows further decorating resources and scripts:
```python
@skills.skill()
def data_analyzer() -> str:
return "Analyze data from various sources..."
# Add dynamic resources
@data_analyzer.resource
def get_schema() -> str:
"""Get the data schema."""
return "## Schema\n\nColumns: id, name, value, timestamp"
@data_analyzer.resource
async def get_available_tables(ctx: RunContext[MyDeps]) -> str:
"""Get available database tables (with runtime context)."""
tables = await ctx.deps.database.list_tables()
return f"Available tables:\n" + "\n".join(tables)
# Add executable scripts
@data_analyzer.script
async def analyze_query(ctx: RunContext[MyDeps], query: str) -> str:
"""Execute an analysis query."""
result = await ctx.deps.database.execute(query)
return str(result)
```
### How Skill Names Are Derived
The decorator automatically converts function names to valid skill names:
```python
@skills.skill()
def my_data_tool() -> str:
# Skill name: "my-data-tool"
return "..."
@skills.skill()
def MySkill() -> str:
# Skill name: "myskill" (lowercased, hyphens added between camelCase)
return "..."
@skills.skill(name='custom-name')
def function_with_explicit_name() -> str:
# Skill name: "custom-name" (uses provided name)
return "..."
```
## Custom Instruction Templates
### Customizing Agent Instructions
By default, `SkillsToolset` injects a standard instruction template that explains the progressive disclosure pattern. You can customize this for your specific use case:
```python
from pydantic_ai import Agent
from pydantic_ai_skills import SkillsToolset
custom_template = """\
You have access to specialized skills for domain-specific knowledge.
## How to Use Skills
Each skill contains:
- **Instructions**: Guidelines on when and how to use the skill
- **Resources**: Reference documentation and data
- **Scripts**: Executable functions for specific tasks
Always follow this workflow:
1. Call `list_skills` to see available skills
2. Call `load_skill` to understand the skill's capabilities
3. Use `read_skill_resource` for specific reference material
4. Execute scripts with `run_skill_script` when needed
## Available Skills
{skills_list}
## Tips
- Load skills only when relevant to the user's request
- Consult resources before calling scripts
- Check skill descriptions to understand their scope
"""
toolset = SkillsToolset(
directories=['./skills'],
instruction_template=custom_template
)
agent = Agent(
model='openai:gpt-4o',
toolsets=[toolset]
)
```
### Template Variables
Your custom template **must include** the `{skills_list}` placeholder, which is automatically replaced with formatted skill information:
```text
{skills_list} # Replaced with XML-formatted list of available skills
```
The skills list includes:
- Skill name, description, and URI
- List of available resources
- List of available scripts
- Full skill instructions
### Default Template
If you don't provide a custom template, the default is used:
```python
DEFAULT_INSTRUCTION_TEMPLATE = """\
Here is a list of skills that contain domain specific knowledge on a variety of topics.
Each skill comes with a description of the topic and instructions on how to use it.
When a user asks you to perform a task that falls within the domain of a skill, use the `load_skill` tool to acquire the full instructions.
{skills_list}
Use progressive disclosure: load only what you need, when you need it:
- First, use `load_skill` tool to read the full skill instructions
- To read additional resources within a skill, use `read_skill_resource` tool
- To execute skill scripts, use `run_skill_script` tool
"""
```
## Dependency Injection via RunContext
### Using RunContext in Resources and Scripts
Both `@skill.resource` and `@skill.script` decorated functions can optionally accept a `RunContext[DepsType]` parameter to access dependencies. The toolset automatically detects this by inspecting the function signature.
### Example: Database Access
```python
from typing import TypedDict
from pydantic_ai import RunContext
from pydantic_ai_skills import SkillsToolset
class MyDeps(TypedDict):
"""Dependencies available in RunContext."""
database: Database
cache: Cache
logger: Logger
skills = SkillsToolset()
@skills.skill()
def database_skill() -> str:
return "Query and analyze database content"
# Resource with context - accesses the database dependency
@database_skill.resource
async def get_current_schema(ctx: RunContext[MyDeps]) -> str:
"""Fetch current schema from the database."""
schema = await ctx.deps.database.get_schema()
return f"## Current Schema\n{schema}"
# Script with context - executes queries with database access
@database_skill.script
async def execute_query(
ctx: RunContext[MyDeps],
query: str,
limit: int = 100
) -> str:
"""Execute a SQL query against the database."""
result = await ctx.deps.database.query(query, limit=limit)
# Log the execution
await ctx.deps.logger.info(f"Executed query: {query[:50]}...")
# Cache the result
cache_key = f"query:{query}"
await ctx.deps.cache.set(cache_key, result, ttl=3600)
return str(result)
```
### Type-Safe Dependency Access
The `RunContext` is generic over your dependency type, enabling type-safe access:
```python
# When passing dependencies to your agent
from pydantic_ai import Agent
agent = Agent(
model='openai:gpt-4o',
deps=MyDeps(
database=my_database,
cache=my_cache,
logger=my_logger
),
toolsets=[skills]
)
result = agent.run_sync('Query the user database', deps=agent.deps)
```
### Signature Detection
The decorator automatically determines if a function takes context:
```python
# ✓ Automatically detected as needing context
@skill.resource
async def needs_context(ctx: RunContext[MyDeps]) -> str:
return str(ctx.deps)
# ✓ Correctly detected as not needing context
@skill.resource
def static_resource() -> str:
return "Static content"
# ✓ Works with sync functions too
@skill.script
def sync_script(ctx: RunContext[MyDeps]) -> str:
return str(ctx.deps)
```
## Custom Script Executors
### Debugging File-Based Scripts Locally
File-based scripts discovered from skill directories run in a subprocess by default (`LocalSkillScriptExecutor`). For local development and debugging, you can switch to an in-process executor that uses `runpy.run_path` to execute script files directly in the same process. This allows you to set breakpoints and inspect variables with your IDE's debugger.
```bash
python -m examples.debug_local_logging
```
The example above:
1. Creates a demo skill under `examples/tmp/debug-logging-skill`
1. Uses `CallableSkillScriptExecutor` to run script files in-process with `runpy.run_path`
1. Captures stdout/stderr and writes execution traces to `examples/tmp/debug-local-executor.log`
Minimal pattern:
```python
from pydantic_ai_skills import CallableSkillScriptExecutor, SkillsDirectory
async def in_process_executor(*, script, args=None):
# Development-only path for breakpoint debugging.
...
executor = CallableSkillScriptExecutor(func=in_process_executor)
skills_dir = SkillsDirectory(path='./skills', script_executor=executor)
```
Important caveats:
- **Not for production**: Running untrusted scripts in-process can be dangerous. Use this pattern only for local development with trusted code.
- **Limited isolation**: In-process execution means scripts have access to the same memory and environment as your agent. Be cautious of side effects and security implications.
- **Performance**: In-process execution may be faster for small scripts, but can lead to memory leaks or instability if the script misbehaves. Always test thoroughly when using custom executors.
### Creating Custom Executors
For advanced use cases, you can provide custom script executors that handle script execution differently than the built-in local executor. This enables:
- Remote execution (cloud functions, etc.)
- Sandboxing or containerized execution
- Custom security policies
- Integration with external systems
### Executor Interface
Custom executors must implement the `SkillScriptExecutor` protocol:
```python
from typing import Protocol, Any
class SkillScriptExecutor(Protocol):
"""Protocol for custom script execution."""
async def execute(
self,
script: SkillScript,
args: dict[str, Any] | None = None
) -> str:
"""Execute a skill script.
Args:
script: The script to execute
args: Optional arguments to pass to the script
Returns:
String output from the script
"""
...
```
### Example: Remote Execution
```python
from pydantic_ai_skills import SkillScript, CallableSkillScriptExecutor
import httpx
class RemoteExecutor:
"""Execute scripts on a remote server."""
def __init__(self, server_url: str):
self.server_url = server_url
async def execute(self, script: SkillScript, args: dict | None = None) -> str:
"""Send script execution request to remote server."""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.server_url}/execute",
json={
"script_name": script.name,
"args": args or {}
}
)
return response.text
# Use in custom script definitions
from pydantic_ai_skills import SkillScript
remote_executor = RemoteExecutor("https://api.example.com")
script = SkillScript(
name='remote-analysis',
description='Run analysis on remote server',
function=None,
executor=remote_executor # Custom executor
)
```
### Example: Sandboxed Execution
```python
import subprocess
from pathlib import Path
class SandboxedExecutor:
"""Execute Python scripts in isolated sandboxes."""
def __init__(self, timeout: int = 30):
self.timeout = timeout
async def execute(self, script: SkillScript, args: dict | None = None) -> str:
"""Execute script in sandbox."""
# Convert args to environment variables
env = {f"ARG_{k.upper()}": str(v) for k, v in (args or {}).items()}
try:
result = subprocess.run(
["sandbox", "--timeout", str(self.timeout), script.name],
capture_output=True,
text=True,
env={**os.environ, **env},
timeout=self.timeout + 5
)
return result.stdout or result.stderr
except subprocess.TimeoutExpired:
return f"ERROR: Script execution timed out after {self.timeout}s"
```
## Metadata Management
### Custom Metadata Fields
You can attach arbitrary metadata to skills for custom use cases:
```python
@skills.skill(
metadata={
'version': '1.0.0',
'author': 'data-team',
'tags': ['analytics', 'database', 'reporting'],
'supported_formats': ['csv', 'json', 'parquet'],
'min_pydantic_ai_version': '0.1.0'
}
)
def advanced_analytics() -> str:
return "Advanced analytics skill..."
```
### Accessing Metadata
Metadata is preserved in the `Skill` object and accessible via:
```python
skill = toolset.get_skill('advanced-analytics')
# Access via skill attributes
if hasattr(skill, 'metadata') and skill.metadata:
version = skill.metadata.get('version', '0.0.0')
tags = skill.metadata.get('tags', [])
```
### Recommended Metadata Fields
Common metadata patterns:
| Field | Type | Purpose |
| --------------------- | ---------------- | ----------------------------------------- |
| `version` | `str` | Semantic version for the skill |
| `author` | `str` | Team or person who maintains the skill |
| `tags` | `list[str]` | Categorization tags for discovery |
| `requires` | `dict[str, str]` | External dependencies and versions |
| `deprecated` | `bool` | Mark skills as deprecated |
| `deprecation_message` | `str` | Explanation and alternative if deprecated |
| `breaking_changes` | `str` | Document breaking changes in versions |
| `supported_models` | `list[str]` | Models this skill works best with |
## Mixed Skill Scenarios
### Combining Programmatic and File-Based Skills
The real power comes when combining both approaches:
```python
from pydantic_ai import Agent
from pydantic_ai_skills import SkillsToolset, Skill
# Create programmatic skills
skills = SkillsToolset(
directories=['./skills', './custom-skills'], # File-based skills
max_depth=3 # Limit discovery depth
)
# Add decorator-defined skills to the same toolset
@skills.skill()
def runtime_analyzer() -> str:
return "Analyze runtime metrics and performance data"
@runtime_analyzer.resource
async def get_metrics(ctx: RunContext[MyDeps]) -> str:
metrics = await ctx.deps.monitoring.get_current()
return f"Current metrics:\n{metrics}"
# Now agent has access to both:
# - File-based skills from ./skills and ./custom-skills
# - Programmatically defined runtime_analyzer skill
agent = Agent(
model='openai:gpt-4o',
toolsets=[skills]
)
```
### Skill Precedence and Conflicts
When multiple skills have the same name:
1. **Programmatic skills** (defined via `@decorator`) take precedence
1. **File-based skills** are loaded in directory order
1. **Duplicate detection**: If a duplicate is detected, a warning is issued and the new skill is registered
```python
# This will warn if a skill named 'data-analyzer' exists in ./skills
@skills.skill(name='data-analyzer')
def conflicting_skill() -> str:
return "..."
```
### Dynamic Skill Registration
You can programmatically register skills after toolset creation (though typically skills are defined at initialization):
```python
from pydantic_ai_skills import Skill
# Create toolset
toolset = SkillsToolset()
# Register a new programmatic skill
new_skill = Skill(
name='dynamically-added',
description='Added after initialization',
content='This skill was registered dynamically'
)
toolset._register_skill(new_skill) # Internal method, use with caution
```
> **Note**: Direct manipulation of internal `_register_skill()` is not recommended for production code. Define skills at initialization time for better clarity and maintainability.
## See Also
- [Programmatic Skills](https://dougtrajano.github.io/pydantic-ai-skills/programmatic-skills/index.md) - Detailed guide to programmatic skill creation
- [Skill Registries](https://dougtrajano.github.io/pydantic-ai-skills/registries/index.md) - Load skills from Git repositories and remote sources
- [API Reference - SkillsToolset](https://dougtrajano.github.io/pydantic-ai-skills/api/toolset/index.md) - Complete API documentation
- [Implementation Patterns](https://dougtrajano.github.io/pydantic-ai-skills/patterns/index.md) - Common design patterns and best practices
# Why pydantic-ai-skills
There is more than one way to give a Pydantic AI agent Agent Skills. This page explains what `pydantic-ai-skills` does, how it differs from the alternatives, and when each one is the better fit.
## The alternatives
### Core `Capability` with `defer_loading`
Pydantic AI's core [on-demand capabilities](https://ai.pydantic.dev/capabilities/) let you define a capability in Python and hide its instructions and tools behind a framework-managed `load_capability` tool:
```python
from pydantic_ai.capabilities import Capability
refunds = Capability(
id='refunds',
description='Refund policy tools and instructions.',
instructions='Check the refund policy before answering refund questions.',
defer_loading=True,
)
```
This is the right tool when your instructions and tools live in Python. It is not an Agent Skills implementation: there is no `SKILL.md`, no bundled resources, and no portable skill package.
### `pydantic-ai-harness` `Skills`
[`pydantic-ai-harness`](https://github.com/pydantic/pydantic-ai-harness) ships a `Skills` capability that scans skill libraries and turns each `SKILL.md` into one deferred core capability. It is a deliberately minimal reader: it parses the frontmatter and Markdown body and, in its own words, does not enumerate, read, or execute bundled files such as `references/`, `assets/`, or `scripts/`.
```python
from pydantic_ai_harness.skills import Skills
agent = Agent('anthropic:claude-sonnet-4-6', capabilities=[Skills('.agents/skills')])
```
### `pydantic-ai-skills`
This library implements the whole Agent Skills package — metadata, instructions, bundled resources, and executable scripts — plus discovery from remote registries and skills defined in Python.
## Feature comparison
| | `pydantic-ai-skills` | harness `Skills` | Core `Capability` |
| -------------------------------------- | ---------------------------------- | ---------------------------- | ----------------- |
| Reads `SKILL.md` packages | ✅ | ✅ | ❌ |
| Skill metadata catalog (level 1) | ✅ | ✅ | ✅ |
| Instructions on demand (level 2) | ✅ | ✅ | ✅ |
| Bundled resources (level 3) | ✅ `read_skill_resource` | ❌ not loaded | ❌ |
| Bundled script execution | ✅ `run_skill_script` | ❌ not executed | ❌ |
| `include` / `exclude` selection | ✅ | ✅ | n/a |
| Programmatic skills in Python | ✅ decorators and dataclasses | ❌ | ✅ |
| Remote registries (Git, S3) | ✅ | ❌ | ❌ |
| Registry composition | ✅ combine, filter, prefix, rename | ❌ | ❌ |
| Reload without restarting | ✅ `reload()` / `auto_reload` | ❌ snapshot at construction | ❌ |
| Recursive discovery | ✅ up to `max_depth` | ❌ immediate children only | n/a |
| Path traversal and symlink containment | ✅ | ❌ explicitly not a boundary | n/a |
| Per-tool restriction | ✅ `exclude_tools` | n/a | n/a |
| Deferred loading via `load_capability` | ✅ `defer_loading=True` | ✅ always deferred | ✅ opt-in |
| Declarative agent specs | ✅ | ✅ | ✅ |
## The practical difference: level 3
[Progressive disclosure](https://dougtrajano.github.io/pydantic-ai-skills/concepts/index.md) has three levels. Metadata is always in the prompt, instructions load on demand, and **bundled resources and scripts load only when the skill actually needs them**.
An implementation that stops after level 2 gives the model a skill's instructions but no way to act on them. Consider a typical published skill:
```text
pdf-processing/
├── SKILL.md # "Consult FORMS.md, then run scripts/fill_form.py"
├── FORMS.md
└── scripts/
└── fill_form.py
```
With instruction-only loading the model reads that guidance, then has no `FORMS.md` and no way to run `fill_form.py`. Relative paths and placeholders such as `${CLAUDE_SKILL_DIR}` stay unresolved in the loaded text. `pydantic-ai-skills` exposes `read_skill_resource` and `run_skill_script`, so skills written this way — including those in Anthropic's [skills repository](https://github.com/anthropics/skills) — run as published.
If script execution is not appropriate in your deployment, turn it off explicitly rather than losing resource reads with it:
```python
SkillsCapability(directories=['./skills'], exclude_tools=['run_skill_script'])
```
## Choosing which skills to expose
A shared library can serve several agents. `include` and `exclude` scope the catalog per agent:
```python
from pydantic_ai_skills import SkillsCapability
research = SkillsCapability(directories=['./skills'], include=['arxiv-search', 'web-research'])
support = SkillsCapability(directories=['./skills'], exclude=['arxiv-search'])
```
| Configuration | Skills in the catalog |
| -------------------- | ---------------------- |
| Neither option | All discovered skills |
| `include=['a', 'b']` | Only `a` and `b` |
| `include=[]` | No skills |
| `exclude=['a', 'b']` | All except `a` and `b` |
| `exclude=[]` | All discovered skills |
Notes:
- `include` and `exclude` cannot be combined; doing so raises `ValueError`.
- A name matching no discovered skill raises `ValueError` at construction, so typos surface early.
- Selection applies to every source: programmatic skills, directories, and registries.
- `reload()` and `auto_reload` re-apply the selection.
- Passing a bare string (`include='arxiv-search'`) raises `TypeError` rather than matching each character.
Both options are available on `SkillsToolset` and `SkillsCapability`, and in agent specs:
```yaml
capabilities:
- SkillsCapability:
directories: ['./skills']
include:
- arxiv-search
- web-research
```
Selection controls catalog exposure. It is not a filesystem permission or an access-control boundary — see [Security](https://dougtrajano.github.io/pydantic-ai-skills/security/index.md).
## Which should you use?
Use **core `Capability`** when instructions and tools are written in Python and you do not need a portable skill format.
Use **harness `Skills`** when you only need instruction text injected on demand, your skills are plain `SKILL.md` files with no bundled files, and you already depend on the harness.
Use **`pydantic-ai-skills`** when skills carry bundled resources or scripts, come from a Git or S3 registry, are generated in Python, need to change while the process runs, or when you want path traversal and symlink containment on the loading path.
# Core Concepts
Understanding the key concepts behind pydantic-ai-skills will help you build better agent systems.
## Skill Structure
Skills are modular packages that extend agent capabilities. You can create them in two ways:
### File-Based Skills
A directory containing:
- `SKILL.md` - Metadata (YAML frontmatter) and instructions (required)
- `scripts/` - Optional executable Python scripts
- `resources/` - Optional additional documentation or data
```markdown
my-skill/
├── SKILL.md # Required: Instructions and metadata
├── scripts/ # Optional: Executable scripts
│ └── my_script.py
└── resources/ # Optional: Additional files
├── reference.md
└── data.json
```
### Programmatic Skills
Alternatively, define skills directly in Python using the `Skill` class with decorators for dynamic resources and scripts. This enables dependency injection and runtime configuration. See [Programmatic Skills](https://dougtrajano.github.io/pydantic-ai-skills/programmatic-skills/index.md) for details.
### Registry Skills
Skills can also be loaded from remote sources via **skill registries**. For example, `GitSkillsRegistry` clones a Git repository and exposes its skills. Registries support composition (filtering, prefixing, renaming, combining). See [Skill Registries](https://dougtrajano.github.io/pydantic-ai-skills/registries/index.md) for details.
## Integration Modes
You can integrate skills in two ways:
1. `SkillsToolset` (uses the `toolsets=[...]` API)
1. `SkillsCapability` (uses the `capabilities=[...]` API)
`SkillsCapability` is the preferred integration path. It wraps an internal `SkillsToolset` so behavior is consistent across both approaches, and it bundles instruction injection through the Capability API.
### SkillsToolset mode
Use this mode when your app is built around `toolsets=[...]`. Skills instructions are injected into the agent's context automatically.
### SkillsCapability mode
Use this mode when your app is built around `capabilities=[...]`. Skills tools and skills instructions are bundled in one capability.
```python
from pydantic_ai import Agent
from pydantic_ai_skills import SkillsCapability
agent = Agent(
model='openai:gpt-5.2',
capabilities=[SkillsCapability(directories=['./skills'])],
)
```
#### Deferred loading
Pydantic AI v2 lets a capability stay hidden until the model explicitly loads it via the agent's built-in `load_capability` tool. Set `defer_loading=True` and give the capability a stable `id`:
```python
agent = Agent(
model='openai:gpt-5.2',
capabilities=[
SkillsCapability(id='skills', directories=['./skills'], defer_loading=True),
],
)
```
When deferred, the skills tools and instructions are collapsed to a one-line catalog entry (derived from the skill names, or set `description=...` to override) and only activate after the model loads the capability. This complements the toolset's own progressive disclosure (`list_skills` → `load_skill`): `defer_loading` gates the *entire skills system* behind one catalog entry, which is useful when an agent bundles many capabilities and most turns need none of the skills.
## SKILL.md Format
Each `SKILL.md` file has **YAML frontmatter** (metadata) followed by **Markdown** (instructions).
**Minimal example:**
```yaml
---
name: my-skill
description: A brief description of what this skill does
---
```
**Required fields:**
- `name` - Unique identifier (lowercase, hyphens, ≤64 chars)
- `description` - Brief summary (≤1024 chars, appears in listings)
**Optional fields:**
```yaml
---
name: arxiv-search
description: Search arXiv for research papers
version: 1.0.0
author: Your Name
category: research
---
```
## Progressive Disclosure
The toolset implements **progressive disclosure** - exposing information only when needed:
1. **Initial**: Skill names and descriptions are added to agent instructions via `get_instructions(ctx)` (called automatically by the framework on newer pydantic-ai versions)
1. **Loading**: Agent calls `load_skill(name)` to get full instructions when needed
1. **Resources**: Agent calls `read_skill_resource()` for additional documentation
1. **Execution**: Agent calls `run_skill_script()` to execute scripts
This approach:
Skills use **progressive disclosure** to minimize context:
1. **Discovery**: Skill names/descriptions are added to instructions via `get_instructions()`
1. **Loading**: Agent calls `load_skill(name)` for full instructions when needed
1. **Resources**: Agent calls `read_skill_resource()` for additional files
1. **Execution**: Agent calls `run_skill_script()` to execute code
This reduces token usage, enables dynamic capability discovery, and scales to many skills. **Returns**: Formatted markdown with skill names and descriptions
**When to use**: Optional - skills are already listed in the agent's instructions via `get_instructions(ctx)` injection. Use only if the agent needs to re-check available skills dynamically.
### 2. load_skill(name)
Lo# The Four Tools
| Tool | Purpose |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| `list_skills()` | List all available skills (usually redundant with `get_instructions()`) |
| `load_skill(name)` | Load complete instructions for a skill |
| `read_skill_resource(skill_name, resource_name)` | Read additional files (e.g., forms, reference docs) |
| `run_skill_script(skill_name, script_name, args)` | Execute Python scripts defined in the skill |
| id="skills" # Unique identifier | |
| ) | |
| `` text ### Key Methods - `get_instructions(ctx)` - Get instructions text (automatically injected into agent) - `get_skill(name)` - Get a specific skill object - `refresh()` - Re-scan directories for skills (if using SkillsDirectory instances) ### Properties - `skills` - Dictionary of loaded skills (`dict[str, Skill]`) ## Skill Discovery Skills are discovered by scanning directories for `SKILL.md` files using the `SkillsDirectory` class: ``python from pydantic_ai_skills import SkillsDirectory skill_dir = SkillsDirectory(path="./skills", validate=True) all_skills = skill_dir.get_skills() for name, skill in all_skills.items(): print(f"{name}: {skill.description}") \`\`\` | |
## Type Safety
The package provides type-safe dataclasses for working with skills:
### Skill
```python
from pydantic_ai_skills import Skill
skill = Skill(
name="my-skill",
description="My skill description",
content="# Instructions...",
resources=[...],
scripts=[...],
metadata={"version": "1.0.0", "author": "Me"},
)
```
### SkillResource
```python
from pydantic_ai_skills import SkillResource
resource = SkillResource(
name="reference.md",
description="Reference material",
content="# Schema definitions...", # or pass a file `uri` / callable `function`
)
```
### SkillScript
```python
from pydantic_ai_skills import SkillScript
script = SkillScript(
name="my_script",
description="Runs an analysis",
uri="scripts/my_script.py", # file-based; or pass a callable `function`
skill_name="my-skill",
)
```
## Security
The toolset implements security measures:
- **Path Validation**: Scripts and resources must be within the skill directory
- **No Path Traversal**: Attempts to access files outside the skill directory are blocked
- **Script Timeout**: Scripts are killed after the configured timeout
- **Safe Execution**: Scripts run in a subprocess with limited privileges
## Next Steps
- [Creating Skills](https://dougtrajano.github.io/pydantic-ai-skills/creating-skills/index.md) - Learn how to build skills
- [Implementation Patterns](https://dougtrajano.github.io/pydantic-ai-skills/patterns/index.md) - Common patterns and best practices
- [API Reference](https://dougtrajano.github.io/pydantic-ai-skills/api/toolset/index.md) - Detailed API documentation
# Contributing
Thank you for your interest in contributing to pydantic-ai-skills!
## Ways to Contribute
- **Report bugs** - Open an issue describing the problem
- **Suggest features** - Share ideas for new functionality
- **Improve documentation** - Fix typos, clarify explanations, add examples
- **Share skills** - Contribute useful skill examples
- **Submit code** - Fix bugs or implement features
## Development Setup
### 1. Fork and Clone
```bash
git clone https://github.com/YOUR_USERNAME/pydantic-ai-skills.git
cd pydantic-ai-skills
```
### 2. Create Virtual Environment
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
### 3. Install Development Dependencies
```bash
pip install -e ".[dev]"
```
### 4. Install Pre-commit Hooks
```bash
pre-commit install
```
## Making Changes
### 1. Create a Branch
```bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix
```
### 2. Make Your Changes
- Follow existing code style
- Add tests for new functionality
- Update documentation as needed
- Keep commits focused and atomic
### 3. Run Tests
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=pydantic_ai_skills
# Run specific test
pytest tests/test_toolset.py::test_discover_skills
```
### 4. Check Code Quality
```bash
# Run pre-commit checks
pre-commit run --all-files
# Or run individually
ruff check .
ruff format .
mypy pydantic_ai_skills
```
## Coding Standards
### Python Style
- Follow [PEP 8](https://pep8.org/)
- Use type hints for all functions
- Maximum line length: 120 characters
- Use Ruff for linting and formatting
### Documentation
- Add docstrings to all public functions/classes
- Use Google-style docstring format
- Include examples in docstrings when helpful
### Example Docstring
````python
def discover_skills(
directories: list[str | Path],
validate: bool = True,
) -> list[Skill]:
"""Discover skills from filesystem directories.
Searches for SKILL.md files in the given directories and loads
skill metadata and structure.
Args:
directories: List of directory paths to search for skills.
validate: Whether to validate skill structure.
Returns:
List of discovered Skill objects.
Raises:
ValueError: If validation enabled and skill is invalid.
Example:
```python
skills = discover_skills(
directories=["./skills"],
validate=True
)
for skill in skills:
print(f"{skill.name}: {skill.metadata.description}")
```
"""
````
## Testing
### Writing Tests
- Place tests in `tests/` directory
- Use pytest for testing
- Aim for high code coverage
- Test edge cases and error conditions
### Test Structure
```python
import pytest
from pydantic_ai_skills import SkillsToolset
def test_toolset_init():
"""Test SkillsToolset initialization."""
toolset = SkillsToolset(directories=["./test_skills"])
assert len(toolset.skills) > 0
def test_get_skill_not_found():
"""Test get_skill raises error for non-existent skill."""
toolset = SkillsToolset(directories=["./test_skills"])
with pytest.raises(KeyError):
toolset.get_skill("non-existent")
```
## Pull Request Process
### 1. Update Documentation
- Update README.md if needed
- Add/update docstrings
- Update relevant docs/ pages
### 2. Update CHANGELOG
Add an entry under "Unreleased":
```markdown
## [Unreleased]
### Added
- New feature description (#PR_NUMBER)
### Fixed
- Bug fix description (#PR_NUMBER)
```
### 3. Create Pull Request
- Write clear PR title and description
- Reference related issues
- Ensure all checks pass
- Request review
### PR Template
```markdown
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Checklist
- [ ] Tests added/updated
- [ ] Documentation updated
- [ ] CHANGELOG updated
- [ ] All tests pass
- [ ] Pre-commit checks pass
```
## Reporting Issues
### Bug Reports
Include:
- Python version
- pydantic-ai-skills version
- Minimal reproducible example
- Expected vs actual behavior
- Full error traceback
### Feature Requests
Include:
- Use case description
- Proposed solution
- Alternative approaches considered
- Examples of usage
## Community Guidelines
- Be respectful and inclusive
- Follow the [Code of Conduct](https://github.com/dougtrajano/pydantic-ai-skills/blob/main/CODE_OF_CONDUCT.md)
- Help others learn and grow
- Credit contributors
## Questions?
- Open a [Discussion](https://github.com/dougtrajano/pydantic-ai-skills/discussions)
- Join community channels (if available)
- Check existing issues and PRs
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
# Creating Skills
This guide covers creating effective **file-based skills** for Pydantic AI agents.
Programmatic Skills
Skills can also be created in Python code using the `Skill` class. See [Programmatic Skills](https://dougtrajano.github.io/pydantic-ai-skills/programmatic-skills/index.md) for dynamic resources and dependency injection.
## Basic Skill Structure
Every file-based skill must have at minimum:
```markdown
my-skill/
└── SKILL.md
```
The `SKILL.md` file contains:
1. **YAML frontmatter** with metadata
1. **Markdown content** with instructions
## Writing SKILL.md
### Minimal Example
```markdown
---
name: my-skill
description: A brief description of what this skill does
---
# My Skill
Instructions for the agent on how to use this skill...
```
### Required Fields
- `name`: Unique identifier (lowercase, hyphens for spaces)
- `description`: Brief summary (appears in skill listings)
### Naming Conventions
Follow the [Agent Skills](https://agentskills.io/home) naming conventions:
- **Name**: lowercase, hyphens only, ≤64 chars (no "anthropic" or "claude")
- **Description**: ≤1024 chars, clear and concise
**Valid**: `arxiv-search`, `web-research`, `data-analyzer` **Invalid**: `ArxivSearch`, `arxiv_search`, `very-long-skill-name-exceeds-limit`
The toolset logs warnings for violations but skills will still load.
### Best Practices for Instructions
**✅ Do:**
- Use clear, action-oriented language
- Provide specific examples
- Break down complex workflows
- Specify when to use the skill
**❌ Don't:**
- Write vague instructions
- Assume implicit context
- Create circular skill dependencies
- Include API keys or sensitive data
### Example: Well-Written Instructions
````markdown
---
name: arxiv-search
description: Search arXiv for research papers
---
# arXiv Search Skill
## When to Use
Use this skill when you need to:
- Find recent preprints in physics, math, or computer science
- Search for papers not yet published in journals
- Access cutting-edge research
## Instructions
To search arXiv, use the `run_skill_script` tool with:
1. **skill_name**: "arxiv-search"
2. **script_name**: "scripts/arxiv_search.py"
3. **args**:
- `query`: Required search query (e.g., "neural networks")
- `max-papers`: Optional, defaults to 10
## Examples
Search for 5 papers on machine learning:
```python
run_skill_script(
skill_name="arxiv-search",
script_name="scripts/arxiv_search.py",
args={"query": "machine learning", "max-papers": 5}
)
````
## Output Format
The script returns a formatted list with:
- Paper title
- Authors
- arXiv ID
- Abstract
## Adding Scripts
Scripts enable skills to perform custom operations that aren't available as standard agent tools.
### Script Location
Place scripts in either:
- `scripts/` subdirectory (recommended)
- Directly in the skill folder
```markdown
my-skill/
├── SKILL.md
└── scripts/
├── process_data.py
└── fetch_info.py
```
### Writing Scripts
Scripts should:
- Accept named command-line arguments (for example, using `argparse` in Python or an equivalent option parser in your runtime)
- Print output to stdout
- Exit with code 0 on success, non-zero on error
- Handle errors gracefully
#### Example Script
```python
#!/usr/bin/env python3
"""Example skill script."""
import sys
import json
def main():
if len(sys.argv) < 2:
print("Usage: process_data.py ")
sys.exit(1)
input_data = sys.argv[1]
try:
# Process the input
result = process(input_data)
# Output results
print(json.dumps(result, indent=2))
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def process(data):
# Your processing logic here
return {"processed": data.upper()}
if __name__ == "__main__":
main()
```
### Script Best Practices
**✅ Do:**
- Validate input arguments
- Return structured output (JSON preferred)
- Handle errors gracefully
- Document expected inputs/outputs in SKILL.md
- Use timeouts for external calls
- Add a shebang line for interpreter portability (for example `#!/usr/bin/env python3`)
**❌ Don't:**
- Make network calls without timeouts
- Write to files outside the skill directory
- Require interactive input
- Use environment-specific paths
### Script Interpreter Selection
When filesystem scripts are executed, interpreter selection follows this order:
1. Shebang line, when present and resolvable
1. Extension-based fallback for compatibility (`.py`, `.sh`, `.bash`, `.zsh`, `.fish`, `.ps1`, `.bat`, `.cmd`)
1. Direct executable invocation fallback
This allows extensionless and custom-extension executable scripts to run while preserving compatibility with existing script names and extensions.
### Script Argument Handling
When agents call scripts via `run_skill_script()`, arguments are converted to command-line flags:
```python
# Agent calls:
run_skill_script(
skill_name='data-analyzer',
script_name='scripts/analyze.py',
args={'query': 'SELECT * FROM users', 'limit': '100', 'format': 'json'}
)
# Your script receives command-line arguments:
#