Skills
Skills are reusable, composable units of agent capability that bundle related tools with domain-specific instructions. They sit between raw tools and infrastructure plugins in the abstraction hierarchy.
#Overview
A skill packages a set of tools together with the instructions that tell the LLM how to use them. Unlike plugins — which are infrastructure connectors (PostgreSQL, S3, Slack) — skills carry behavioral intelligence: domain knowledge, conventions, prompt snippets, and tool orchestration patterns.
Use skills to:
- Bundle related tools so agents can pick them up in one call
- Inject domain-specific instructions into the system prompt automatically
- Share reusable capabilities across multiple agents and projects
- Load prompt engineering from markdown files instead of hard-coded strings
- Declare and resolve plugin dependencies at attach time
Skills are LifecyclePlugin subclasses, so on_before_run hooks inject instructions into the system prompt automatically before every agent run.
#Quick Start
#The Skill helper
For simple skills — a handful of tools plus a short instruction string — use the Skill convenience class:
from daita import Agent, Skill, tool
@tool
def format_report(data: list, title: str) -> str:
"""Render a markdown report."""
rows = "\n".join(f"- {r}" for r in data)
return f"# {title}\n\n{rows}"
@tool
def generate_chart(series: list, kind: str = "bar") -> str:
"""Generate a chart description."""
return f"{kind} chart with {len(series)} series"
report_skill = Skill(
name="report_gen",
description="Produces polished analytical reports",
instructions="Always render results as markdown with a title and bulleted rows.",
tools=[format_report, generate_chart],
)
agent = Agent(name="Analyst", llm_provider="openai", model="gpt-4o")
agent.add_skill(report_skill)
result = await agent.run("Summarize Q3 revenue with a chart.")#The BaseSkill subclass pattern
For skills that need dynamic instructions, plugin dependencies, or tools that are built at init time, subclass BaseSkill:
from daita import BaseSkill
from daita.core.tools import AgentTool
class SecurityReviewSkill(BaseSkill):
name = "security_review"
description = "Detect security vulnerabilities in Python code"
version = "1.0.0"
instructions_file = "prompts/security_review.md"
def get_tools(self) -> list[AgentTool]:
return [scan_security_patterns, check_input_validation]Class attributes (name, description, version, instructions, instructions_file) keep static metadata out of __init__. Override get_tools() to build tools dynamically.
#Core Concepts
#Instructions
Skill instructions are returned from get_instructions(user_prompt) and resolved in this order:
instructions_file(read once, cached) — a path to a file containing the instructions text. Relative paths resolve against the skill's module directory when usingBaseSkill;Skillrequires an absolute path.instructions— an inline string.None— no instructions injected.
Override get_instructions to generate dynamic, prompt-aware instructions:
class ContextAwareSkill(BaseSkill):
name = "context_aware"
def get_instructions(self, user_prompt: str = "") -> str:
if "report" in user_prompt.lower():
return "Format output as a formal report with headings."
return "Respond concisely."Because BaseSkill inherits from LifecyclePlugin, its on_before_run hook injects whatever get_instructions returns into the system prompt automatically — you don't need to wire anything else up.
#Tools
Skills expose tools through get_tools(), which returns a list of AgentTool objects. The agent's tool registry picks them up the moment the skill is added.
class CodeQualitySkill(BaseSkill):
name = "code_quality"
instructions = "Prefer clear naming and small functions."
def get_tools(self):
return [check_complexity, check_naming, check_line_length]The Skill helper exposes the tools you pass to its constructor — no override needed.
#Plugin Dependencies
Skills can declare that they require one or more plugin types to be registered on the agent before they can be attached. Override requires() and return a mapping of logical names to plugin types:
from daita import BaseSkill
from daita.plugins.base_db import BaseDatabasePlugin
class MigrationsSkill(BaseSkill):
name = "migrations"
instructions = "Follow forward-only migration policy."
def requires(self):
return {"db": BaseDatabasePlugin}
def get_tools(self):
db = self._resolved_plugins["db"]
return [build_migration_tool(db), build_rollback_tool(db)]When agent.add_skill(skill) runs, Daita:
- Calls
skill.requires()to get the dependency map - Searches the agent's registered plugins for a match by
isinstance - Populates
skill._resolved_plugins[key]with the matching plugin instance - Raises
SkillErrorif any requirement is unmet
Add required plugins before skills that depend on them:
agent = Agent(name="DBA", llm_provider="openai", model="gpt-4o")
agent.add_plugin(postgresql(host="localhost", database="app")) # first
agent.add_skill(MigrationsSkill()) # then#Usage
#Attaching skills to an agent
agent = Agent(name="Analyst", llm_provider="openai", model="gpt-4o")
agent.add_skill(report_skill)
agent.add_skill(CodeQualitySkill())
# All attached skills
for skill in agent.skills:
print(skill.name, skill.version)agent.skills returns every BaseSkill currently attached.
#Multiple skills on one agent
Skills compose — attach as many as you need. Their instructions concatenate into the system prompt in attach order, and their tools merge into the shared registry.
agent.add_skill(SecurityReviewSkill())
agent.add_skill(CodeQualitySkill())
agent.add_skill(documentation_skill) # a Skill() instance
result = await agent.run("Review this PR: ...")#Loading instructions from a file
Keep prompt engineering out of Python. instructions_file points at a markdown (or any text) file; BaseSkill resolves relative paths against the module the subclass is defined in:
my_project/
skills/
security.py
prompts/
security_review.md# skills/security.py
class SecurityReviewSkill(BaseSkill):
name = "security_review"
instructions_file = "prompts/security_review.md" # resolved alongside security.pyFiles are read once and cached on the instance.
#API Reference
#BaseSkill
Abstract base class for skills. Subclass this for anything non-trivial.
Class attributes
| Attribute | Type | Description |
|---|---|---|
name | str | Unique skill name. Used in logs and error messages. |
description | str | Human-readable description of what the skill does. |
version | str | Skill version (defaults to "0.1.0"). |
instructions | str | Inline instruction string. Ignored if instructions_file is set. |
instructions_file | Optional[str] | Path to a text file containing instructions. Relative paths resolve to the subclass module directory. |
Methods
get_instructions(user_prompt: str = "") -> Optional[str]— return the instruction text. Override for dynamic prompts.get_tools() -> List[AgentTool]— return the tools this skill exposes. Default: empty list.requires() -> Dict[str, type]— declare plugin dependencies. Default:{}.on_before_run(prompt: str) -> Optional[str]— lifecycle hook that injects instructions. You usually don't override this.
Instance attributes
config— any keyword arguments passed to__init__._resolved_plugins— dict of resolved dependencies, populated byadd_skill.
#Skill
Concrete skill built inline from instructions and a list of tools.
Skill(
name: str,
*,
instructions: str = "",
instructions_file: Optional[str] = None,
tools: Optional[List[AgentTool]] = None,
description: str = "",
version: str = "0.1.0",
**config,
)Raises ValueError if:
- Both
instructionsandinstructions_fileare provided. instructions_fileis a relative path (useBaseSkillfor relative path resolution).
#agent.add_skill(skill)
Attach a skill to an agent. Resolves skill.requires() against already-registered plugins.
Raises SkillError if any required plugin is missing.
#agent.skills
Property returning every BaseSkill currently attached to the agent.
#SkillError
Subclass of PluginError raised for skill-specific failures — most commonly unmet plugin dependencies. Importable from the top-level daita package.
from daita import SkillError
try:
agent.add_skill(MigrationsSkill())
except SkillError as exc:
print(f"Skill attach failed: {exc}")#Complete Example
A code-review agent composed of two skills — security checks (from a BaseSkill subclass with a file-based prompt) and code quality checks:
import asyncio
from daita import Agent, BaseSkill
from daita.core.tools import AgentTool, tool
# -- Security skill ----------------------------------------------------------
@tool
async def scan_security_patterns(code: str) -> dict:
"""Scan Python source code for known vulnerability patterns."""
...
@tool
async def check_input_validation(code: str) -> dict:
"""Identify functions that accept string parameters without validation."""
...
class SecurityReviewSkill(BaseSkill):
name = "security_review"
description = "Detect security vulnerabilities in Python code"
version = "1.0.0"
instructions_file = "prompts/security_review.md"
def get_tools(self) -> list[AgentTool]:
return [scan_security_patterns, check_input_validation]
# -- Code quality skill ------------------------------------------------------
@tool
async def check_complexity(code: str) -> dict:
"""Compute cyclomatic complexity per function."""
...
@tool
async def check_naming(code: str) -> dict:
"""Flag non-PEP8 names."""
...
class CodeQualitySkill(BaseSkill):
name = "code_quality"
description = "Static analysis for clarity and maintainability"
instructions = (
"Prefer small functions and clear naming. "
"Report issues grouped by severity."
)
def get_tools(self) -> list[AgentTool]:
return [check_complexity, check_naming]
# -- Agent -------------------------------------------------------------------
async def main():
agent = Agent(
name="code_reviewer",
llm_provider="anthropic",
model="claude-sonnet-4-6",
)
agent.add_skill(SecurityReviewSkill())
agent.add_skill(CodeQualitySkill())
with open("module.py") as f:
source = f.read()
report = await agent.run(f"Review this file and produce a report:\n\n{source}")
print(report)
asyncio.run(main())See examples/deployments/code-review-agent/ in the daita-agents repo for the full runnable version, including the security_review.md prompt, tests, and deploy manifest.
#Skills vs Plugins vs Tools
| Primitive | Purpose | Example |
|---|---|---|
| Tool | A single callable the LLM can invoke | @tool def get_weather(city: str) -> str |
| Skill | Bundle of tools + domain instructions | SecurityReviewSkill, ReportGenSkill |
| Plugin | Infrastructure connector (stateful) | postgresql, s3, slack |
Rule of thumb:
- Need to call one function? Use
@tool. - Need an agent to connect to a database, queue, or API? Use a plugin.
- Need to teach an agent how to do something — conventions, prompt engineering, multi-tool workflows — use a skill.
#See Also
- Tools — how
@toolandAgentToolwork - Plugins Overview — the infrastructure side of the abstraction
- Agent — attaching skills and plugins to agents