Skills
Use Agent Skills to give an agent specialized instructions without putting every instruction in its initial prompt.
Point Skills at one or more skill libraries. The model first sees each
skill’s name and description. When a skill is useful, the model loads it with
the load_capability tool to receive that skill’s instructions.
While Pydantic AI Harness is on 0.x releases, the API may change between minor releases; when it does, deprecation warnings and release-note migration guidance tell you (or your agent) exactly how to upgrade. See the version policy.
Install the skills extra for YAML frontmatter support:
pip install "pydantic-ai-harness[skills]"
uv add "pydantic-ai-harness[skills]"
Create a skill library:
.agents/skills/
code-review/
SKILL.md
Add the skill’s description and instructions:
---
name: code-review
description: Review a change for correctness and repository conventions.
---
Inspect the change and report findings by severity.
Then add the library to your agent, with a workspace to read it from:
from pydantic_ai import Agent
from pydantic_ai.capabilities import LocalWorkspace
from pydantic_ai_harness import Skills
agent = Agent(
'anthropic:claude-opus-5-5',
capabilities=[LocalWorkspace('.'), Skills('.agents/skills')],
)
Skills does not search .agents, .claude, or your home directory
automatically. Pass each library you want it to load; to layer several the way
coding agents do, see Layer project and personal skills.
At the start of every run, Skills:
- Reads each configured library from the run’s workspace. Relative paths resolve from its working directory.
- Scans the immediate child directories and validates the selected
SKILL.mdfiles. - Offers each selected skill as a
deferred capability
named after it: the model sees its name and description, and loads it with the
load_capabilitytool.
Loading a skill returns a # Skill: <name> heading, a line naming the skill’s
directory in the workspace, and the skill’s Markdown body. The catalog is the
same on every run over the same files, so it stays in the cached prefix; a new
or renamed skill appears in it on the next run. Without any skills, Skills
adds no instructions and no load_capability tool.
A run without a workspace fails at its start. To read skills from somewhere else, such as skills shipped with your application while the agent works in a sandbox, pass a workspace backend:
from pydantic_ai.workspaces import LocalWorkspaceBackend
from pydantic_ai_harness import Skills
skills = Skills('skills', workspace=LocalWorkspaceBackend('/app'))
workspace= takes a backend, not the LocalWorkspace capability. Under a
durable engine such as Temporal, a workspace= backend is read in-process, not
through the workflow.
By default, all discovered skills are included. Use include or exclude to
change the catalog for a particular agent:
from pydantic_ai_harness import Skills
review_skills = Skills(
'.agents/skills',
include=['code-review'],
)
release_skills = Skills(
'.agents/skills',
exclude=['code-review'],
)
| 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 |
include and exclude cannot be used together. The constructor overloads catch
this in typed code, and runtime validation covers agent specs and untyped
callers. Unknown names fail at run start.
Selection happens before frontmatter is parsed. An unselected skill does not add
instructions or frontmatter validation errors to that Skills instance.
These options control catalog exposure. They are not filesystem permissions or an access-control boundary.
Directory paths choose where discovery starts in the workspace; they do not create a containment boundary, and normal symlink resolution applies.
A selected SKILL.md body becomes model instructions. Load libraries only from
sources you trust, and review repository-provided skills before exposing them.
Each immediate child directory containing SKILL.md is a skill:
.agents/skills/
code-review/
SKILL.md
release-notes/
SKILL.md
The loader uses these parts of SKILL.md:
| Part | Requirement |
|---|---|
name | Optional. Defaults to the parent directory name. If provided, it must match the directory after Unicode normalization. |
description | Required and non-blank. The Agent Skills limit is 1,024 characters; longer descriptions load with a warning. This appears in the initial catalog. |
| Markdown body | Optional. This is loaded under a generated # Skill: <name> heading. |
disable-model-invocation | Optional, true or false. true leaves the skill out of the model’s catalog; see Invoke a skill yourself. |
user-invocable | Optional, true or false. false tells a host not to offer the skill as a command; the model still sees it. |
Skill names and include or exclude values are normalized with Unicode NFKC
before matching. A normalized name can contain at most 64 lowercase Unicode
letters or numbers, separated by single hyphens. It cannot start or end with a
hyphen.
disable-model-invocation and user-invocable come from
Claude Code.
Any value other than true or false (in any letter case) skips the skill
with a warning, rather than guessing whether it may be invoked.
Only immediate children are discovered. For example,
code-review/references/SKILL.md does not create another skill. Ordinary files
and child directories without SKILL.md are ignored.
You can pass several libraries:
from pydantic_ai_harness import Skills
skills = Skills([
'.agents/skills',
'company/skills',
])
By default, selected skill names must be unique across those libraries.
Repeated references to the same resolved library are scanned once, and a skill
found twice, through a symlinked library or skill directory or as a
byte-identical copy of its SKILL.md, counts once.
Coding agents read skills from conventional locations that a project may not
have, and let a project’s skill take precedence over a personal one with the
same name. Two options give Skills the same behavior:
from pydantic_ai_harness import Skills
skills = Skills(
['.agents/skills', '.claude/skills', '/home/me/.agents/skills', '/home/me/.claude/skills'],
missing_directories='skip',
duplicate_names='keep_first',
)
missing_directories='skip'leaves out a library directory that does not exist, instead of failing the run. A path that is a file, or that holds aSKILL.mditself (a skill package rather than a library of them), is left out with aUserWarning, so one a model creates mid-session doesn’t fail every later run.duplicate_names='keep_first'keeps the skill from the earlier directory when two valid skills with differentSKILL.mdfiles share a name, and skips the other with aUserWarning. An invalidSKILL.mdis skipped first, so it does not hide a valid skill with its name.
Directories are listed in precedence order. Paths are workspace paths, and ~
is not expanded, because the workspace may be a sandbox with a home directory of
its own; spell out the absolute path for a library on this machine.
If .agents/skills is a symlink to .claude/skills, or holds byte-identical
copies of its SKILL.md files, as in many repositories, each skill is listed
once. Only SKILL.md is compared: the first directory’s bundled files are the
ones the model is pointed at.
Agent Skill packages can contain directories such as references/, assets/,
and scripts/. Skills does not enumerate, read, or execute those files.
The loaded instructions name the skill’s directory in the run’s workspace, so a
model with file or shell tools, such as those from FileSystem or Shell, can
follow a relative reference like references/guide.md or run
scripts/check.py. Skills read from a workspace= backend are not where those
tools work, so their instructions leave the directory out. Placeholders such as
${CLAUDE_SKILL_DIR} remain unchanged in the body.
Skills reads the libraries itself: the model does not need FileSystem or
Shell to load a skill, and adding either does not change which files Skills
reads.
The portable name, description, and Markdown instructions are supported.
name may be omitted and derived from the directory.
The following behavioral fields are accepted for compatibility, but their behavior is not implemented:
agent, allowed-tools, argument-hint, arguments, context, dependencies,
disallowed-tools, effort, hooks, model, paths, shell, tools, when_to_use
If a selected skill uses any of these fields, the run emits one aggregated
UserWarning at its start. Fields such as license, compatibility, and metadata are
accepted without changing runtime behavior. Other unknown, non-behavioral fields
are also accepted.
The model decides when to load a skill. A host that also lets a person invoke a
skill, such as a /code-review src/app.py command in a terminal client, reads
the same catalog with load and renders the skill as a prompt:
from pydantic_ai.workspaces import LocalWorkspaceBackend
from pydantic_ai_harness import Skills
skills = Skills('.agents/skills', missing_directories='skip')
catalog = await skills.load(LocalWorkspaceBackend('.'))
review = next(skill for skill in catalog.skills if skill.name == 'code-review')
result = await agent.run(review.render('src/app.py'))
load reads the libraries as a run would, without emitting warnings. Each
entry in the catalog’s skipped is a SkippedSkill with the path left out,
a reason, and the message a run would warn with, for the host to show:
reason | Left out |
|---|---|
'invalid' | A SKILL.md with malformed frontmatter, invalid UTF-8, or an invalid or mismatched name |
'duplicate_name' | A SKILL.md named like a skill found earlier, with duplicate_names='keep_first' |
'not_a_directory' | A library path that is a file, with missing_directories='skip' |
'skill_package' | A library path that holds a SKILL.md itself, with missing_directories='skip' |
Each SkillDefinition has the skill’s name, description, body, path,
directory, in_run_workspace, model_invocable, and user_invocable. The
catalog includes skills with disable-model-invocation: true
(model_invocable is False), which a run leaves out of the model’s catalog,
so a host can offer them as commands only. Skills doesn’t offer commands
itself, so user_invocable is for the host to check before it offers one.
render(arguments) returns what loading the skill returns, with every
$ARGUMENTS in the body replaced by arguments. A body without $ARGUMENTS
gets ARGUMENTS: <arguments> appended. Other placeholders, such as Claude
Code’s indexed $0 or $ARGUMENTS[0], are left unchanged, and do not count as
$ARGUMENTS. render() without arguments returns the instructions the model
gets when it loads the skill, including leaving the directory out for a skill
read from workspace= (in_run_workspace is False).
Skills works with Pydantic AI’s YAML and JSON agent specs:
model: anthropic:claude-opus-5-5
capabilities:
- Skills:
directories: .agents/skills
include:
- code-review
- release-notes
Register Skills when loading the spec:
from pydantic_ai import Agent
from pydantic_ai_harness import Skills
agent = Agent.from_file('agent.yaml', custom_capability_types=[Skills])
The skills extra installs PyYAML. It parses SKILL.md frontmatter and YAML
agent specs.
Use Pydantic AI’s core Capability when your instructions or tools are defined
in Python instead of a SKILL.md package:
from pydantic_ai.capabilities import Capability
refunds = Capability(
id='refunds',
description='Use for refund policy questions.',
instructions='Check the refund policy before answering.',
defer_loading=True,
)
Skills loads portable Agent Skill packages. It does not replace the core API
for code-defined capabilities.
Skills(
directories: str | Path | Sequence[str | Path],
*,
include: Collection[str] | None = None,
exclude: Collection[str] | None = None,
missing_directories: Literal['error', 'skip'] = 'error',
duplicate_names: Literal['error', 'keep_first'] = 'error',
workspace: WorkspaceBackend | None = None,
)
directoriesaccepts one library path or a sequence of paths.includeexposes only the named skills.excludeomits the named skills from the catalog.missing_directories='skip'leaves out library paths that do not exist, and warns about and leaves out those that are files or skill packages.duplicate_names='keep_first'keeps the first of two skills with one name, with a warning.workspacereads the libraries from this backend instead of the run’s workspace.
Pass at least one library directory, not the path of an individual skill
package. Malformed frontmatter, invalid UTF-8, and invalid or mismatched names warn and skip that skill. Unknown selections fail at run start, and so do duplicate selected names, missing libraries, and library paths that are files or skill packages, unless duplicate_names or missing_directories say otherwise.
Two Skills on one agent combine into one catalog.
- Agent Skills specification
- Adding skills support to an agent
- Pydantic AI workspaces
- Pydantic AI capabilities overview
Bases: AbstractCapability[AgentDepsT]
Offer Agent Skill instructions from the run’s workspace as deferred capabilities.
Skill libraries are directories in the run’s workspace (ctx.workspace), read at the
start of every run; relative paths resolve against its working directory. Attach
LocalWorkspace to read directories on this machine, or pass workspace= to read them
from a workspace of their own. A run with neither fails at its start.
Each selected immediate child containing SKILL.md becomes a deferred capability named after
the skill: the model sees its name and description, and loads its Markdown body, with the
skill’s directory in the run’s workspace, with load_capability. Bundled files are not loaded
or executed. Descriptions longer than the Agent Skills limit are preserved and emit a warning.
A skill found twice, through a symlink or as a byte-identical SKILL.md, counts once. A skill
whose frontmatter has disable-model-invocation: true is left out of the model’s catalog.
load reads the same skills outside a run, for a host that lets a person invoke a skill
itself; render one with SkillDefinition.render.
Two Skills on one agent combine, so every library either names stays reachable.
One per agent: two Skills combine into one catalog.
Type: str | None Default: 'skills'
Skill-library paths in the workspace, read at the start of each run.
Type: tuple[str | Path, …] Default: self._normalize_directories(directories)
Exact skill names to expose, or None to expose all discovered skills.
Type: frozenset[str] | None Default: self._normalize_selection('include', include) if include is not None else None
Exact skill names to omit from the catalog.
Type: frozenset[str] Default: self._normalize_selection('exclude', exclude) if exclude is not None else frozenset()
'error' fails the run on a library path that is not a library; 'skip' leaves it out, with a warning
unless it does not exist.
Type: MissingDirectories Default: missing_directories
'error' fails the run when two different SKILL.md files share a name; 'keep_first' keeps the first.
Type: DuplicateNames Default: duplicate_names
Where the libraries live, when not in the run’s workspace; see __init__.
Type: WorkspaceBackend | None Default: workspace
def __init__(
directories: str | Path | Sequence[str | Path],
*,
include: Collection[str],
exclude: None = None,
missing_directories: MissingDirectories = 'error',
duplicate_names: DuplicateNames = 'error',
workspace: WorkspaceBackend | None = None,
) -> None
def __init__(
directories: str | Path | Sequence[str | Path],
*,
include: None = None,
exclude: Collection[str] | None = None,
missing_directories: MissingDirectories = 'error',
duplicate_names: DuplicateNames = 'error',
workspace: WorkspaceBackend | None = None,
) -> None
Configure the skill libraries to read at the start of each run.
One skill-library path or a sequence of paths in the workspace, in precedence
order. ~ is not expanded: the workspace may be a sandbox with a home of its own.
include : Collection[str] | None Default: None
Exact names to expose. Omit to expose all discovered skills.
exclude : Collection[str] | None Default: None
Exact names to omit. Cannot be combined with include.
'error' (the default) fails the run when a library path does not
exist, is not a directory, or is itself a skill package. 'skip' leaves such a path out,
for conventional locations such as .agents/skills that a project may not have: one
that does not exist quietly, any other with a warning.
'error' (the default) fails the run when two different SKILL.md files
share a name. 'keep_first' keeps the one from the earlier directory and skips the
other with a warning, the way coding agents layer project skills over personal ones.
workspace : WorkspaceBackend | None Default: None
A workspace backend to read the libraries from instead of the run’s, such as
LocalWorkspaceBackend('/app') for skills shipped with the code while the agent
works in a sandbox. It is read in-process only in this release: a durable engine
does not route it through its workflow machinery.
def __repr__() -> str
Show only the Skills configuration that callers control.
@classmethod
def combine(
cls,
capabilities: Sequence[AbstractCapability[AgentDepsT]],
) -> AbstractCapability[AgentDepsT]
Serve every combined configuration’s libraries through one catalog.
The field-by-field default would keep only the last configuration’s directories, dropping the
other libraries. A skill name selected by two configurations must name the same SKILL.md, or
a byte-identical copy, unless the later configuration has duplicate_names='keep_first'.
AbstractCapability[AgentDepsT]
@async
def for_run(ctx: RunContext[AgentDepsT]) -> AbstractCapability[AgentDepsT]
Read the selected skills, from each configuration’s workspace= or else the run’s workspace.
Returns one deferred capability per skill the model may load, or, without any, this capability,
which adds no instructions or tools. Emits a UserWarning for each skipped SKILL.md or
library path, an overlong description, or unsupported frontmatter. Raises UserError when a configuration without
workspace= meets a run without a workspace.
AbstractCapability[AgentDepsT]
@async
def load(workspace: WorkspaceBackend | None = None) -> SkillCatalog
Read the selected skills now, as a run does at its start, without emitting warnings.
For a host that offers skills to a person as well as to the model, such as a /code-review
command that sends skill.render(arguments) as a prompt. The catalog includes skills the model
may not load (model_invocable), and its skipped entries say which SKILL.md files and
library paths were left out, for the host to show.
SkillCatalog
workspace : WorkspaceBackend | None Default: None
Where libraries without their own workspace= live, as the run’s workspace
would be, such as LocalWorkspaceBackend('.').
UserError— A configuration withoutworkspace=needsworkspace, and none was passed.ValueError— A configuration problem that would also fail a run, such as an unknownincludename or, withduplicate_names='error', two skills with one name.
Validated skill metadata and body from a single SKILL.md.
The skill’s name: its name frontmatter field, or else its directory’s name.
Type: str
The description frontmatter field, listed in the model’s catalog.
Type: str
The Markdown after the frontmatter, without leading and trailing blank lines.
Type: str
Frontmatter fields such as allowed-tools whose behavior Skills does not implement.
Whether the model may load it; False when its frontmatter has disable-model-invocation: true.
A run leaves such a skill out of the model’s catalog. Skills.load still returns it, for a host
that lets a person invoke it.
Type: bool Default: True
Whether a host should let a person invoke it; False when its frontmatter has user-invocable: false.
Skills itself only offers skills to the model, so this is for a host, such as one with a /skill-name command.
Type: bool Default: True
Absolute workspace path of the SKILL.md it was read from.
Type: str
Whether it was read from the run’s workspace, where the model’s file and shell tools can reach its directory.
False for a library read from Skills(workspace=...); render then leaves the directory out.
Type: bool Default: True
Absolute workspace path of the skill’s directory, which holds any scripts/ or references/.
Type: str
def render(arguments: str | None = None) -> str
The skill’s instructions as the model receives them: a heading, the skill’s directory, and the body.
Pass arguments when a user invokes the skill with a command such as /code-review src/app.py:
every $ARGUMENTS in the body becomes arguments, and a body without $ARGUMENTS gets
ARGUMENTS: <arguments> appended. Other placeholders, such as Claude Code’s indexed $0 and
$ARGUMENTS[0], are left unchanged. Without arguments, the result is the instructions the
model gets when it loads the skill.
The directory line is left out unless in_run_workspace.
The skills Skills.load read, and each SKILL.md or library it left out.
Run-time advice, such as frontmatter fields whose behavior is not implemented, is not included:
a run emits it as a UserWarning, and each skill’s ignored_behavioral_fields lists its own.
The selected, valid skills, in catalog order, including those the model may not load.
Type: tuple[SkillDefinition, …]
Each SKILL.md or library path left out, in the order it was found.
A run emits each message as a UserWarning; a host showing skills to a person can show them instead.
Type: tuple[SkippedSkill, …]
A SKILL.md or skill library path that Skills left out, and why.
Absolute workspace path of the skipped SKILL.md, or of the skipped library.
Type: str
Why it was left out.
Type: SkipReason
A sentence naming the path and the problem, as a run’s UserWarning says it.
Type: str