Create skills directly in Python code for dynamic capabilities that require runtime dependencies, custom logic, or configuration-based generation. **View the complete example:** [programatic_skills.py](../examples/programatic_skills.py)
Installs into .claude/skills of the current project.
Are you the author of 2919 Programmatic Skills 3ca2713a?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/tools-only-2919-programmatic-skills-3ca2713a)
# Programmatic Skills
Create skills directly in Python code for dynamic capabilities that require runtime dependencies, custom logic, or configuration-based generation.
**View the complete example:** [programatic_skills.py](../examples/programatic_skills.py)
## Video Tutorial
Learn how to create programmatic skills by watching the Programmatic Skills Tutorial, which demonstrates creating skills with decorators and dynamic resources:
<video controls style="max-width:100%; border-radius:8px">
<source src="../assets/programmatic_skills.mp4" type="video/mp4">
</video>
## Overview
Programmatic skills let you:
- Create skills using Python dataclasses instead of files
- Add static resources with inline content
- Register dynamic resources via `@skill.resource` decorator
- Register executable scripts via `@skill.script` decorator
- Access dependencies through `RunContext`
## Creating a Basic Programmatic Skill
```python
from pydantic_ai.toolsets.skills import Skill, SkillResource, SkillsToolset
# Create a skill with static resources
my_skill = Skill(
name='my-skill',
description='A programmatic skill example',
content="""Use this skill for example tasks.
## Instructions
1. Call the `get_info` resource to understand available data
2. Use `process_data` script with your query
""",
resources=[
SkillResource(
name='reference',
content='## Reference\n\nStatic reference documentation here...'
)
]
)
# Initialize toolset with the programmatic skill
skills_toolset = SkillsToolset(skills=[my_skill])
```
## Adding Dynamic Resources
Use the `@skill.resource` decorator to create resources that generate content dynamically:
```python
from pydantic_ai import RunContext
from pydantic_ai.toolsets.skills import Skill
my_skill = Skill(
name='database-skill',
description='Database query and analysis',
content='Use this skill for database operations.'
)
@my_skill.resource
def get_schema() -> str:
"""Get current database schema."""
return "## Schema\n\nTables: users, orders, products"
@my_skill.resource
async def get_connection_info(ctx: RunContext[MyDeps]) -> str:
"""Get database connection information from dependencies."""
db_info = await ctx.deps.get_db_info()
return f"Connected to: {db_info}"
```
### Resource Features
- **Optional RunContext**: Resources can optionally take `RunContext[DepsType]` as first argument
- **Auto-detection**: The `takes_ctx` parameter is automatically detected from function signature
- **Async Support**: Resources can be sync or async functions
- **Type Safety**: Function signatures are analyzed for proper parameter types
## Adding Executable Scripts
Use the `@skill.script` decorator to create executable scripts:
```python
from pydantic_ai import RunContext
from pydantic_ai.toolsets.skills import Skill
my_skill = Skill(
name='data-processor',
description='Process and analyze data',
content='Use scripts to load and query data.'
)
@my_skill.script
async def load_dataset(ctx: RunContext[MyDeps]) -> str:
"""Load the dataset into memory."""
await ctx.deps.load_data()
return 'Dataset loaded successfully'
@my_skill.script
async def run_query(ctx: RunContext[MyDeps], query: str, limit: int = 10) -> str:
"""Execute a query on the loaded dataset.
Args:
query: SQL query string to execute
limit: Maximum number of results to return
"""
results = await ctx.deps.execute_query(query, limit)
return format_results(results)
```
### Script Features
- **Named Arguments**: Scripts accept named parameters matching function signature
- **Default Values**: Parameters can have default values
- **Type Annotations**: Use type hints for better validation
- **Docstrings**: Function docstrings become script descriptions for the LLM
- **RunContext Access**: First parameter can be `RunContext[DepsType]` for dependencies
## Complete Example: HR Analytics Agent
Here's a complete example showing all features:
```python
import datetime
import sqlite3
from dataclasses import dataclass, field
import datasets
from pydantic_ai import Agent, RunContext
from pydantic_ai.toolsets.skills import Skill, SkillResource, SkillsToolset
@dataclass
class AnalystDeps:
"""Dependencies for the HR analytics agent."""
hf_dataset_name: str = 'dougtrajano/hr-synthetic-database'
hf_dataset_subsets: list[str] = field(
default_factory=lambda: ['business_units', 'departments', 'jobs', 'employees']
)
db: sqlite3.Connection | None = field(default=None)
def get_db_tables(self) -> list[str]:
"""Get list of tables in database."""
if self.db is None:
return []
cursor = self.db.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
return [row[0] for row in cursor.fetchall()]
# Static resource with schema documentation
schema_resource = SkillResource(
name='table-schemas',
uri='table-schemas.md',
content="""## Database Schema
### Business Units
- `id` (string): Unique identifier
- `name` (string): Business unit name
- `director_job_id` (string): Reference to director's job
### Departments
- `id` (string): Unique identifier
- `name` (string): Department name
- `manager_job_id` (string): Reference to manager
- `business_unit_id` (string): Foreign key to business_units
### Jobs
- `id` (string): Unique identifier
- `name` (string): Job title
- `job_level` (string): Entry, Mid, Senior, Executive
- `job_family` (string): Engineering, Sales, etc.
### Employees
- `id` (string): Unique identifier
- `job_id` (string): Foreign key to jobs
- `first_name` / `last_name` (string): Employee name
- `birth_date` (string): YYYY-MM-DD format
"""
)
# Create skill with metadata and static resources
hr_skill = Skill(
name='hr-analytics-skill',
description='HR analytics with employee, department, and job data',
content="""Use this skill for HR data analysis.
**Workflow:**
1. Call `load_dataset` script to initialize database
2. Use `get_context` resource for dataset overview
3. Reference `table-schemas` resource for field definitions
4. Execute `run_query` script with SQL to analyze data
""",
resources=[schema_resource]
)
# Add dynamic resource
@hr_skill.resource
def get_context() -> str:
"""Provide high-level context about the dataset."""
return (
'The HR dataset has 4 tables: business_units, departments, '
'jobs, and employees. Tables are linked via foreign keys.'
)
# Add scripts with RunContext access
@hr_skill.script
async def load_dataset(ctx: RunContext[AnalystDeps]) -> str:
"""Load HuggingFace dataset into in-memory SQLite database."""
if ctx.deps.db is None:
ctx.deps.db = sqlite3.connect(':memory:', check_same_thread=False)
loaded_tables = []
for subset in ctx.deps.hf_dataset_subsets:
if subset not in ctx.deps.get_db_tables():
dataset = datasets.load_dataset(
ctx.deps.hf_dataset_name,
name=subset,
split='train'
)
df = dataset.to_pandas()
df.to_sql(subset, ctx.deps.db, if_exists='replace', index=False)
loaded_tables.append(subset)
if loaded_tables:
return f'Loaded tables: {", ".join(loaded_tables)}'
return 'Dataset already loaded'
@hr_skill.script
async def run_query(ctx: RunContext[AnalystDeps], query: str) -> str:
"""Execute SQL query on the HR dataset.
Args:
query: SQL query string (use table names: business_units,
departments, jobs, employees)
"""
if ctx.deps.db is None:
return 'Error: Dataset not loaded. Run load_dataset first.'
try:
cursor = ctx.deps.db.cursor()
cursor.execute(query)
rows = cursor.fetchall()
columns = [desc[0] for desc in cursor.description]
if not rows:
return 'Query executed. No rows returned.'
# Format as table
col_widths = [
max(len(str(col)), max(len(str(row[i])) for row in rows))
for i, col in enumerate(columns)
]
header = ' | '.join(col.ljust(col_widths[i]) for i, col in enumerate(columns))
separator = '-+-'.join('-' * width for width in col_widths)
result_lines = [header, separator]
for row in rows:
result_lines.append(
' | '.join(str(item).ljust(col_widths[i]) for i, item in enumerate(row))
)
return '\n'.join(result_lines)
except sqlite3.Error as e:
return f'SQL Error: {e}'
# Create toolset and agent
skills_toolset = SkillsToolset(skills=[hr_skill])
agent = Agent(
model='openai:gpt-4o',
deps_type=AnalystDeps,
instructions='You are an expert HR data analyst.',
toolsets=[skills_toolset]
)
@agent.instructions
async def add_skills(ctx: RunContext) -> str | None:
"""Inject skill instructions via progressive disclosure."""
return await skills_toolset.get_instructions(ctx)
@agent.instructions
def add_today_date() -> str:
"""Provide current date context."""
return f'Today is {datetime.datetime.now().strftime("%B %d, %Y")}.'
# Run the agent
result = await agent.run(
"What is the average salary by department?",
deps=AnalystDeps()
)
print(result.output)
```
## Mixing File-Based and Programmatic Skills
You can combine both approaches in the same toolset:
```python
from pydantic_ai.toolsets.skills import Skill, SkillsToolset
# Programmatic skill
my_skill = Skill(
name='programmatic-skill',
description='Created in code',
content='Instructions here...'
)
@my_skill.script
async def my_script(ctx: RunContext[MyDeps]) -> str:
return 'Script output'
# Mix with file-based skills
skills_toolset = SkillsToolset(
directories=["./skills"], # File-based skills
skills=[my_skill] # Programmatic skills
)
```
## Implementation Details
### Resource and Script Execution
When the agent calls a resource or script:
1. **Function Schema Generation**: On registration, Pydantic AI analyzes the function signature to generate a JSON schema
2. **RunContext Detection**: The system auto-detects if the function takes `RunContext` as first parameter
3. **Parameter Validation**: Arguments are validated against the function schema
4. **Execution**: The function is called with validated arguments and optional context
5. **Return Handling**: Return values can be any type (str, dict, list, etc.)
### Type Safety
Programmatic skills leverage Pydantic AI's type-safe function schema generation:
```python
from pydantic_ai import RunContext
from pydantic_ai.toolsets.skills import Skill
my_skill = Skill(name='typed-skill', description='Type-safe skill', content='...')
@my_skill.script
async def typed_script(
ctx: RunContext[MyDeps],
query: str, # Required string parameter
limit: int = 10, # Optional with default
verbose: bool = False # Optional with default
) -> dict[str, Any]: # Return type annotation
"""Execute a typed query.
Args:
query: Search query string
limit: Maximum results to return
verbose: Include detailed output
"""
results = await ctx.deps.search(query, limit)
return {'results': results, 'count': len(results)}
```
The function signature becomes the schema, ensuring:
- Required parameters are validated
- Default values are respected
- Type annotations guide validation
- Docstrings provide descriptions
### Custom Decorators
You can customize resource and script registration:
```python
@my_skill.resource(
name='custom-name', # Override function name
description='Custom desc', # Override docstring
takes_ctx=True, # Explicitly set RunContext usage
docstring_format='google' # Specify docstring style
)
def my_resource(ctx: RunContext[MyDeps]) -> str:
return 'Custom resource'
@my_skill.script(
name='custom-script',
description='Custom script description'
)
async def my_script(ctx: RunContext[MyDeps], arg: str) -> str:
return f'Processed: {arg}'
```
## Advanced: Using @toolset.skill() Decorator
For concise skill definition directly on a `SkillsToolset`, use the `@toolset.skill()` decorator:
```python
from pydantic_ai import Agent, RunContext
from pydantic_ai.toolsets.skills import SkillsToolset
skills = SkillsToolset()
@skills.skill(
name='analytics',
license='MIT',
metadata={'version': '1.0.0'}
)
def analytics_skill() -> str:
"""Analyze application data."""
return """# Analytics Skill
Provides analytics and reporting capabilities."""
# Add resources and scripts
@analytics_skill.resource
async def metrics(ctx: RunContext[AppDeps]) -> str:
"""Current system metrics."""
return await ctx.deps.get_metrics()
@analytics_skill.script
async def report(ctx: RunContext[AppDeps], period: str = 'week') -> str:
"""Generate report for period."""
return await ctx.deps.database.generate_report(period)
# Create agent with decorator-defined skill
agent = Agent(
model='openai:gpt-4o',
toolsets=[skills],
deps=AppDeps(...)
)
```
See [Advanced Features](advanced.md) for full decorator documentation.
## Mixing Skill Types
Combine file-based and programmatic skills in a single toolset:
```python
from pydantic_ai.toolsets.skills import SkillsToolset
skills = SkillsToolset(directories=['./skills'])
@skills.skill()
def runtime_config() -> str:
return "Access runtime configuration"
@runtime_config.resource
async def env_info(ctx: RunContext[AppDeps]) -> str:
return f"Environment: {ctx.deps.environment}"
agent = Agent(model='openai:gpt-4o', toolsets=[skills])
```
## Parameter Types for Resources and Scripts
### Simple Typed Parameters
```python
@skill.resource
def get_config(setting: str) -> str:
"""Get configuration value."""
return f"Value: {config[setting]}"
@skill.script
def calculator(expression: str) -> str:
"""Calculate expression using Python's numexpr library.
Expression should be a single line mathematical expression
that solves the problem.
Examples:
"37593 * 67" for "37593 times 67"
"37593**(1/5)" for "37593^(1/5)"
"""
import math
import numexpr
local_dict = {'pi': math.pi, 'e': math.e}
return str(
numexpr.evaluate(
expression.strip(),
global_dict={}, # restrict access to globals
local_dict=local_dict, # add common mathematical functions
)
)
```
### With RunContext and Multiple Parameters
```python
@skill.resource
async def fetch_data(
ctx: RunContext[AppDeps],
dataset: str,
filters: dict | None = None
) -> str:
"""Fetch data with optional filters."""
return await ctx.deps.database.fetch(dataset, filters)
```
## Best Practices
### Resource Guidelines
- Use resources for **context and documentation** that agents need to understand capabilities
- Keep resources **focused and concise** - return only what's needed
- Make resources **idempotent** - safe to call multiple times
- Use **clear names** that describe what the resource provides
- Document parameters in docstrings for LLM understanding
### Script Guidelines
- Use scripts for **actions and computations** that transform data or state
- **Validate inputs** and provide clear error messages
- Keep scripts **stateless** when possible - prefer passing data via dependencies
- Use **meaningful return values** - structured data or formatted text
- Document **parameters clearly** in docstrings for better LLM understanding
### Dependency Management
- Access dependencies via `ctx.deps` in resources and scripts
- Keep dependencies **minimal and focused** for the skill's purpose
- Consider **lazy loading** expensive resources (databases, large datasets)
- Use TypedDict for dependency type safety
### Testing Programmatic Skills
Test resources and scripts independently:
```python
import pytest
from pydantic_ai import RunContext
from pydantic_ai.toolsets.skills import Skill
@pytest.fixture
def skill():
skill = Skill(name='test-skill', description='Test', content='Test skill')
@skill.resource
def get_data() -> str:
return 'test data'
@skill.script
async def process(ctx: RunContext[MyDeps], value: str) -> str:
return f'processed: {value}'
return skill
def test_resource(skill):
resource = skill.resources[0]
result = resource.function()
assert result == 'test data'
@pytest.mark.asyncio
async def test_script(skill):
script = skill.scripts[0]
ctx = create_mock_context(MyDeps())
result = await script.function(ctx, 'test')
assert result == 'processed: test'
```
## When to Use Which Approach
### Use Programmatic Skills When You Need:
- **Dynamic Content**: Generate resources based on runtime state
- **Dependency Access**: Leverage shared dependencies (databases, APIs)
- **Type Safety**: IDE autocomplete and type checking
- **Complex Logic**: Sophisticated algorithms in Python
- **Direct Testing**: Unit test resources and scripts
### Use File-Based Skills When You Need:
- **Simplicity**: Quick creation without code
- **Portability**: Share skills as standalone directories
- **Version Control**: Track instruction changes easily
- **Non-Python Scripts**: Execute scripts in other languages
- **Separation**: Keep instructions separate from application code
### Use Decorator Pattern (@toolset.skill()) When You Need:
- **Inline Definition**: Define skills at agent initialization
- **Runtime Generation**: Create skills based on configuration
- **Conciseness**: Minimal boilerplate for simple skills
- **Integrated Approach**: Skills bundled with agent logic
## Advantages Over File-Based Skills
**Use programmatic skills when you need:**
- **Dynamic Content**: Generate resources based on runtime state
- **Dependency Access**: Leverage shared dependencies (databases, APIs)
- **Type Safety**: Benefit from IDE autocomplete and type checking
- **Complex Logic**: Implement sophisticated algorithms in Python
- **Testing**: Unit test resources and scripts directly
**Use file-based skills when you need:**
- **Simplicity**: Quick creation without Python code
- **Portability**: Share skills as standalone directories
- **Version Control**: Track changes to instruction documents easily
- **Non-Python Scripts**: Execute scripts in other languages
- **Separation**: Keep instructions separate from application code
## See Also
- [Creating Skills](creating-skills.md) - File-based skill creation
- [Advanced Features](advanced.md) - Decorator patterns and dependency injection
- [Implementation Patterns](patterns.md) - Common design patterns
- [API Reference](api/types.md) - Detailed API documentation for `Skill`, `SkillResource`, and `SkillScript`
- [Examples](https://github.com/dougtrajano/pydantic-ai-skills/tree/main/examples) - More examples in the repository