Skills extension for the OpenAI Agents SDK
Project description
openai-agents-skills
Skills extension for the OpenAI Agents SDK.
A Skill is a named, reusable prompt fragment injected into the LLM's context at the right moment
in the agent loop via AgentHooks. This lets you package workflow instructions, checklists, or
procedures as composable named units that can be shared across agents without duplicating configuration.
File-based skills follow the agentskills.io open standard, making
SKILL.md files portable across any compatible agent tool.
Installation
pip install openai-agents-skills
Quick Start
Define a skill by subclassing Skill and overriding get_prompt_blocks:
from openai_agents_skills import Skill
class CitationSkill(Skill):
name = "citation"
description = "Always cite sources when making factual claims."
async def get_prompt_blocks(self, context, agent, args: str = "") -> list:
return [{"role": "user", "content": "Always cite your sources when making factual claims."}]
Attach it to an agent via SkillHooks:
from agents import Agent, Runner
from openai_agents_skills import SkillHooks
agent = Agent(
name="Assistant",
instructions="You are a helpful assistant.",
hooks=SkillHooks([CitationSkill()]),
)
result = await Runner.run(agent, "What is the speed of light?")
print(result.final_output)
Before each LLM call, SkillHooks prepends the skill's prompt blocks to the model's input list.
The model receives and acts on those instructions transparently — no changes to the agent's
instructions, tools, or any other configuration.
How It Works
The SDK passes a mutable input_items list to AgentHooks.on_llm_start before every model
invocation, then passes the same list object directly to model.get_response(). SkillHooks
prepends skill prompt blocks to that list, so the model sees the skill's instructions on every call.
Runner.run()
└─ on_llm_start(context, agent, system_prompt, input_items)
└─ SkillHooks prepends skill blocks to input_items here
└─ model.get_response(input=input_items) ← model sees skill content
This is a standard AgentHooks subclass — no monkey-patching, no internal SDK changes.
Defining Skills
Subclass Skill and override get_prompt_blocks. The method receives an optional args string
and returns a list of input-item dicts:
from openai_agents_skills import Skill
class ReplyInBulletsSkill(Skill):
name = "reply_in_bullets"
description = "Instructs the agent to respond using bullet points."
async def get_prompt_blocks(self, context, agent, args=""):
return [{"role": "user", "content": "Always respond using bullet points."}]
Class attributes
| Attribute | Type | Purpose |
|---|---|---|
name |
str |
Unique identifier |
description |
str |
Human-readable summary; used by routing logic to decide relevance |
always_on |
bool |
True → injects on every call; False (default) → routed by description |
allowed_tools |
list[str] |
Tools this skill may invoke; surfaced in the manifest. Default: [] |
user_invocable |
bool |
False hides the skill from the manifest while still allowing injection. Default: True |
triggers_after_tools |
list[str] |
Tool names that queue this skill for injection after the tool completes. Default: [] |
triggers_after_turn |
bool |
True → queued after every model response for quality checks or review. Default: False |
Context-aware skills
Both get_prompt_blocks and is_enabled receive the SDK's RunContextWrapper and
Agent from the hook, so skills can inject dynamic content or gate themselves on
runtime state:
class OrgContextSkill(Skill):
name = "org-context"
description = "Injects the current organisation ID."
async def get_prompt_blocks(self, context, agent, args=""):
org_id = context.context.org_id if context else None
if not org_id:
return []
return [{"role": "user", "content": f"Your org_id is `{org_id}`."}]
Skills must handle context=None and agent=None — both are None when the skill
is called outside a live agent run (e.g. via make_invoke_skill_tool or in tests).
Gating a skill with is_enabled
Override is_enabled() to activate or suppress a skill at runtime:
import os
from openai_agents_skills import Skill
class FeatureFlagSkill(Skill):
name = "feature_flag_skill"
description = "Only active when ENABLE_SKILL=1."
def is_enabled(self, context=None, agent=None) -> bool:
return os.getenv("ENABLE_SKILL") == "1"
async def get_prompt_blocks(self, context, agent, args: str = "") -> list:
return [{"role": "user", "content": "The feature flag skill is active."}]
Disabled skills are silently skipped by SkillHooks on every call.
Attaching Multiple Skills
Pass a list of skills to SkillHooks. All enabled skills inject in registration order:
from openai_agents_skills import SkillHooks
hooks = SkillHooks([CitationSkill(), ReplyInBulletsSkill(), FeatureFlagSkill()])
agent = Agent(
name="Assistant",
instructions="You are helpful.",
hooks=hooks,
)
File-based Skills
Skills can be defined as SKILL.md files on disk — no Python required. This follows the
agentskills.io open standard, so the same skill folders work across
any compatible agent tool (Cursor, VS Code, GitHub Copilot, Claude Code, and others).
Directory layout
~/.agent/skills/ # user layer (personal, cross-repo)
my-skill/
SKILL.md
<project>/.agent/skills/ # project layer (checked in)
payments-workflow/
SKILL.md
SKILL.md format
---
name: payments-workflow # required: lowercase letters, digits, hyphens; max 64 chars
# description (required): what the skill does and when to use it; max 1024 chars
description: >
Guides the agent through the
end-to-end payments workflow.
license: MIT # optional
compatibility: Requires Python 3.11+ # optional; max 500 chars
metadata: # optional: arbitrary key-value pairs
author: my-team
version: "1.0"
allowed-tools: Bash(git:*) Read # optional: space-separated list of pre-approved tools
always-on: false # extension: inject unconditionally (default false)
user-invocable: true # extension: show in manifest (default true)
---
Step-by-step instructions the agent follows when handling a payment request...
Standard fields (name, description, license, compatibility, metadata, allowed-tools)
are defined by the agentskills.io specification.
Fields prefixed "extension" are specific to this library.
Loading file-based skills
from pathlib import Path
from openai_agents_skills import load_all_skills, SkillHooks
from agents import Agent
registry = await load_all_skills(cwd=Path.cwd())
agent = Agent(
name="Assistant",
instructions="You are helpful.",
hooks=SkillHooks(registry=registry),
)
load_all_skills searches the user layer (~/.agent/skills/) then the project layer
(.agent/skills/ relative to cwd). User-layer skills win on name conflicts.
Registry & Routing
For dynamic per-turn skill selection, use SkillRegistry with LLMSkillRouter.
Skills without always_on=True are forwarded to the router, which uses description
to decide relevance. Skills with always_on=True inject unconditionally.
from agents import Agent, Runner
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
from openai import AsyncOpenAI
from openai_agents_skills import LLMSkillRouter, SkillHooks, SkillRegistry
model = OpenAIChatCompletionsModel("gpt-4o-mini", AsyncOpenAI())
router = LLMSkillRouter(model=model)
# Pass any SDK Model instance — LitellmModel, AnyLLMModel, or OpenAIChatCompletionsModel all work.
registry = SkillRegistry(router=router)
registry.register(CitationSkill()) # always_on=False (default) — routed by description
registry.register(SafetyReminderSkill()) # always_on=True — always injected
agent = Agent(
name="Assistant",
instructions="You are helpful.",
hooks=SkillHooks(registry=registry),
)
result = await Runner.run(agent, "What is the speed of light?")
For multi-agent runs (handoffs), pass RunSkillHooks to Runner.run instead —
it fires for every agent in the handoff chain:
from openai_agents_skills import RunSkillHooks
result = await Runner.run(
agent,
"What is the speed of light?",
hooks=RunSkillHooks(registry=registry),
)
Writing Effective Descriptions
For routed skills (always_on=False), the description is the only text the
router sees when deciding whether a skill is relevant — it doubles as human
documentation and as the routing signal. A vague description is the most common
cause of a skill that never gets selected.
Write the description for a human deciding when to reach for the skill; the router benefits as a side effect. A few habits cover almost every case:
- Lead with the symptom in the user's words. Users describe problems ("clients aren't getting addresses"), not internals ("DHCPv4 binding subsystem"). The router matches against the user's message, so mirror their vocabulary.
- Add an explicit "use when…" clause that enumerates the triggering conditions. This is the single highest-leverage habit.
- Disambiguate from sibling skills with
Not forlines. Most routing mistakes are confusion between near-neighbours, not a failure to find anything. When two skills overlap, name what each is not for and point to the right one — e.g.Not for AP-side loops — use ap-loop-troubleshooting instead.A single exclusion line prevents the most common class of mis-routing. - Keep the core short; add exclusions only when needed.
Start with the symptom and the "use when" clause; only add
Not forlines and scope notes when a skill has close neighbours. If the core description grows past a short paragraph, that is usually a signal to split the skill. Remember the hard budget below — the whole field must fit in 1024 characters.
Avoid keyword-stuffing the description with bare terms (dhcp lease pool relay option82 renew nak ...). It reads worse for humans, drifts from the
agentskills.io standard that other tools rely on, and
actually routes worse — a model reasons better over a clean "use when" sentence
than over a bag of words.
# Weak — only matches the literal word "DHCP"
description: DHCP troubleshooting for Junos.
# Strong — names the symptoms a user would actually type, and disambiguates
description: >
Diagnose Junos DHCP server and relay problems. Use when a user reports leases
not renewing, clients not receiving addresses, address pool exhaustion, relay
forwarding misconfiguration, or Option 82 mismatches.
Not for DHCPv6 prefix delegation — use dhcpv6-troubleshooting instead.
This guidance applies equally to Python Skill subclasses and file-based
SKILL.md skills.
Length budget. File-based skills have a hard limit of 1024 characters for
description, set by the agentskills.io specification. The loader rejects a longer description and skips the skill (logged at debug level), so an over-long description silently disappears rather than failing loudly. If you are porting skills from a tool that allows longer descriptions, trim to 1024 characters and move detail into the body.Field names. This library reads
allowed-tools(nottools) for the list of tools a skill may invoke, and ignores non-standard fields such astitle,headers,tags, andplatforms— they are parsed but have no effect on loading or routing.
Custom SkillRouter
LLMSkillRouter accepts any agents.models.interface.Model instance from the
openai-agents SDK, so Bedrock, Azure, Ollama, and any other supported provider
work out of the box. Pass the same model object you already have configured for
your agent — no extra client setup required:
from agents.extensions.models.litellm_model import LitellmModel
from openai_agents_skills import LLMSkillRouter, SkillRegistry
router = LLMSkillRouter(model=LitellmModel(model="bedrock/anthropic.claude-3-haiku-..."))
registry = SkillRegistry(router=router)
For providers that require extra configuration on the model call — such as AWS
Bedrock credentials via extra_args or inference profiles via extra_body —
pass a ModelSettings instance to model_settings:
from agents.model_settings import ModelSettings
from openai_agents_skills import LLMSkillRouter, SkillRegistry
router = LLMSkillRouter(
model=model,
model_settings=ModelSettings(
extra_args={"aws_access_key_id": "...", "aws_secret_access_key": "..."}
),
)
registry = SkillRegistry(router=router)
For truly custom integrations not covered by the SDK (e.g. a proprietary API with
a non-standard interface), subclass BaseSkillRouter and implement only
_call_model. All routing logic — prompt building, JSON extraction, and LRU
caching — is inherited automatically:
from openai_agents_skills import BaseSkillRouter
class MyCustomRouter(BaseSkillRouter):
async def _call_model(self, prompt: str) -> str:
# Call your custom model API here.
# The response may contain prose before/after the JSON —
# BaseSkillRouter handles extraction automatically.
...
registry = SkillRegistry(router=MyCustomRouter())
Compatibility
Tested weekly against the latest OpenAI Agents SDK to ensure compatibility.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openai_agents_skills-0.4.0.tar.gz.
File metadata
- Download URL: openai_agents_skills-0.4.0.tar.gz
- Upload date:
- Size: 197.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c2ce08f36d952f15978c158e21a48c45f970bde10d3bbea28131cdb23f4a9d4
|
|
| MD5 |
2b8c93f851515cbcd79125f3c6ba6c9a
|
|
| BLAKE2b-256 |
ad036affe4397ef6e1e418a3cb3eed18ce4596a5e90e3c2f802d90220a2615b9
|
File details
Details for the file openai_agents_skills-0.4.0-py3-none-any.whl.
File metadata
- Download URL: openai_agents_skills-0.4.0-py3-none-any.whl
- Upload date:
- Size: 38.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dadefdf8247b5421ff6dd734ee8d0b51b7176252ecb20884adbc5148cc6b516d
|
|
| MD5 |
a4992f85f69ea9ca520451c440e30168
|
|
| BLAKE2b-256 |
a7464e390434837adb79464bc31c4f7b64ecda51ca0158bf107b0fedfbd0697a
|