Advanced Agent Skill Patterns for Pydantic AI
A deep dive into advanced design patterns for creating robust, scalable, and maintainable AI agent skills with Pydantic.
Posted on: 2026-02-27 by AI Assistant
Introduction: From Simple Tools to Robust Systems
In the world of AI agents, “skills” are the building blocks of capability. While it’s easy to create a simple tool, building a robust, scalable, and maintainable AI system requires a more structured approach. This is where design patterns come in. By applying proven patterns, you can create agent skills that are not just functional, but also reusable, resilient, and easy to manage.
This post explores advanced patterns for building sophisticated agent skills using PydanticAI, based on best practices for real-world applications.
1. Skill Organization: File-Based vs. Programmatic
How you organize your skills is the first critical decision.
File-Based Skills
This approach involves organizing your skills in a directory structure. Each skill, with its resources and scripts, lives in its own folder.
- Best for: Large, stable skills that can be shared across multiple projects, open-source skills, or when collaborating with a team that needs a centralized skill repository.
# ./skills/
# ├── data-analysis/
# │ ├── SKILL.md
# │ └── scripts/
# │ └── analyze.py
# └── web-research/
# ├── SKILL.md
# └── scripts/
from pydantic_ai import Agent
from pydantic_ai.toolsets.skills import SkillsToolset
toolset = SkillsToolset(directories=['./skills'])
agent = Agent(model='openai:gpt-4o', toolsets=[toolset])
Programmatic Skills
Here, you define skills directly in your application code using decorators.
- Best for: Dynamic skills that depend on runtime configurations, require access to live dependencies (like database connections or APIs), or are tightly coupled with your application’s logic.
from pydantic_ai import RunContext
from pydantic_ai.toolsets.skills import SkillsToolset
skills = SkillsToolset()
@skills.skill()
def database_analysis() -> str:
"""Analyze data in the active database."""
return "Use the get_schema resource to understand the database structure."
@database_analysis.resource
async def get_schema(ctx: RunContext[MyDeps]) -> str:
"""Gets the current schema from the database."""
schema = await ctx.deps.database.get_schema()
return f"## Current Schema
{schema}"
A mixed approach, combining both file-based and programmatic skills, often provides the perfect balance of stability and flexibility.
2. Providing Context: Resource Patterns
Resources are how you provide your skills with the information they need to function.
- Static Resources: For fixed content that doesn’t change, like API documentation or examples. This content is loaded once and reused.
- Dynamic Resources: For information that depends on the runtime state of your application. These resources use a
RunContextto access live dependencies (e.g., fetching the current schema from a database or reading a user’s session data). - Parameterized Resources: Make your resources more flexible by allowing them to accept parameters. For example, a
documentationresource could take atopicargument to return different sections of a large document.
3. Making Skills Act: Script Execution Patterns
Scripts are the executable part of a skill.
- Simple Synchronous Scripts: For quick, stateless operations like simple text manipulation.
- Stateful Scripts: For tasks that require managing state, such as maintaining a database connection. This is handled by passing dependencies through the
RunContext. - Asynchronous & Secure Scripts: For I/O-bound tasks (like making API calls) and for implementing crucial security measures. For example, you should always use whitelists to validate parameters and prevent injection attacks.
- Chaining Scripts: Design scripts to be called in a sequence to accomplish a complex task. For example, a
prepare_datascript might run beforerun_analysis, which in turn is a prerequisite forexport_results.
4. Building Resilience: Error Handling and Dependency Management
- Error Handling: Catch specific errors like
SkillNotFoundErrorand implement graceful degradation. For instance, if a requested skill isn’t found, an agent can be designed to list the available skills instead. - Dependency Management: Use Python’s
TypedDictto create clear, structured contracts for your dependencies. This improves type checking and makes your skills easier to test and maintain. You can also design for optional dependencies and use lazy initialization to defer the setup of expensive resources until they are actually needed.
5. Ensuring Quality: Testing Patterns
Finally, a robust system is a well-tested one.
- Unit Testing: Test each skill, resource, and script in isolation to ensure it functions correctly.
- Integration Testing: Test how the agent interacts with the entire toolset to catch issues that only appear when different parts of the system work together.
Conclusion
By moving beyond simple tools and adopting these advanced design patterns, you can build an AI agent ecosystem that is scalable, secure, and ready for mission-critical, real-world tasks. The structure and validation provided by PydanticAI make it the ideal framework for implementing these robust patterns.