Skip to content
Back to skills

2919 Programmatic Skills 3ca2713a

ASecurity

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)

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 11, 2026
documentationpythongosqlexpresstestinggitapidatabasedocumentation

Works with

  • cursor
  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 2919-programmatic-skills_3ca2713a --agent claude-code

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.

Security grade badge for 2919 Programmatic Skills 3ca2713a
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-2919-programmatic-skills-3ca2713a/badge)](https://www.skillsdirectory.com/skills/tools-only-2919-programmatic-skills-3ca2713a)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# 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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.