This document provides technical documentation for developers working with the Skills system programmatically.
- Overview
- Core Classes
- SkillLoader
- SkillRegistry
- ToolRegistry Integration
- Programmatic Skill Creation
- Error Handling
The Skills system is organized into three main modules:
| Module | Purpose |
|---|---|
clawlet.skills.base |
Core classes and data structures |
clawlet.skills.loader |
SKILL.md parsing and loading |
clawlet.skills.registry |
Skill management and discovery |
from clawlet.skills import (
SkillRegistry,
SkillLoader,
BaseSkill,
SkillMetadata,
ToolDefinition,
ToolParameter,
)Dataclass containing skill metadata parsed from SKILL.md frontmatter.
from clawlet.skills.base import SkillMetadata
@dataclass
class SkillMetadata:
name: str
version: str = "1.0.0"
description: str = ""
author: str = "unknown"
requires: list[str] = field(default_factory=list)
tools: list[ToolDefinition] = field(default_factory=list)| Property | Type | Description |
|---|---|---|
name |
str |
Unique skill identifier |
version |
str |
Semantic version string |
description |
str |
Brief description |
author |
str |
Author name |
requires |
list[str] |
Required config keys |
tools |
list[ToolDefinition] |
Tool definitions |
metadata = SkillMetadata(
name="email",
version="1.0.0",
description="Send and manage emails",
author="clawlet",
requires=["smtp_server", "smtp_port"],
tools=[...]
)Definition of a single tool parameter with JSON Schema support.
from clawlet.skills.base import ToolParameter
@dataclass
class ToolParameter:
name: str
type: str # "string", "integer", "number", "boolean", "array", "object"
description: Optional[str] = None
required: bool = True
default: Optional[Any] = None
enum: Optional[list[str]] = NoneConverts the parameter to JSON Schema format.
param = ToolParameter(
name="visibility",
type="string",
description="Access level",
required=False,
default="public",
enum=["public", "private"]
)
schema = param.to_json_schema()
# {
# "type": "string",
# "description": "Access level",
# "default": "public",
# "enum": ["public", "private"]
# }# Required string parameter
to_param = ToolParameter(
name="to",
type="string",
description="Recipient email address",
required=True
)
# Optional integer with default
limit_param = ToolParameter(
name="limit",
type="integer",
description="Maximum results",
required=False,
default=10
)Complete tool definition with OpenAI schema conversion.
from clawlet.skills.base import ToolDefinition
@dataclass
class ToolDefinition:
name: str
description: str
parameters: list[ToolParameter] = field(default_factory=list)Converts to OpenAI function calling format.
tool = ToolDefinition(
name="send_email",
description="Send an email",
parameters=[
ToolParameter(name="to", type="string", description="Recipient", required=True),
ToolParameter(name="subject", type="string", description="Subject", required=True),
]
)
schema = tool.to_openai_schema()
# {
# "type": "function",
# "function": {
# "name": "send_email",
# "description": "Send an email",
# "parameters": {
# "type": "object",
# "properties": {
# "to": {"type": "string", "description": "Recipient"},
# "subject": {"type": "string", "description": "Subject"}
# },
# "required": ["to", "subject"]
# }
# }
# }Returns the namespaced tool name.
tool = ToolDefinition(name="send_email", description="...")
tool.get_namespaced_name("email") # "email_send_email"Abstract base class for all skills. Extend this class to implement custom skill behavior.
from clawlet.skills.base import BaseSkill, SkillMetadata
from clawlet.tools.registry import ToolResult
class BaseSkill(ABC):
def __init__(self, metadata: SkillMetadata, skill_path: Optional[Path] = None):
self._metadata = metadata
self._skill_path = skill_path
self._config: dict[str, Any] = {}
self._enabled = True| Property | Type | Description |
|---|---|---|
name |
str |
Skill name from metadata |
version |
str |
Skill version |
description |
str |
Skill description |
author |
str |
Skill author |
requires |
list[str] |
Required config keys |
tools |
list[ToolDefinition] |
Tool definitions |
skill_path |
Optional[Path] |
Path to skill directory |
enabled |
bool |
Whether skill is enabled |
config |
dict[str, Any] |
Skill configuration |
Configure the skill with provided settings.
skill.configure({
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
})Validate that all required configuration is present.
is_valid, missing = skill.validate_requirements()
if not is_valid:
print(f"Missing: {missing}")Execute a skill tool (async).
result = await skill.execute_tool("send_email", to="user@example.com", subject="Hello")
if result.success:
print(result.output)
else:
print(result.error)Abstract method to implement tool execution. Override in subclasses.
class MySkill(BaseSkill):
async def _execute_tool_impl(self, tool_name: str, **kwargs) -> ToolResult:
if tool_name == "my_tool":
# Implement tool logic
return ToolResult(success=True, output="Done")
return ToolResult(success=False, error="Unknown tool")Convert skill tools to OpenAI format with namespaced names.
tools = skill.to_openai_tools()
# [{"type": "function", "function": {...}}, ...]Get the skill instructions (markdown content). Override to provide custom instructions.
instructions = skill.get_instructions()from pathlib import Path
from clawlet.skills.base import BaseSkill, SkillMetadata, ToolDefinition, ToolParameter
from clawlet.tools.registry import ToolResult
class EmailSkill(BaseSkill):
"""Email skill with actual SMTP implementation."""
def __init__(self, metadata: SkillMetadata, skill_path: Optional[Path] = None):
super().__init__(metadata, skill_path)
self._smtp_client = None
async def _execute_tool_impl(self, tool_name: str, **kwargs) -> ToolResult:
if tool_name == "send_email":
return await self._send_email(**kwargs)
return ToolResult(success=False, error=f"Unknown tool: {tool_name}")
async def _send_email(self, to: str, subject: str, body: str, **kwargs) -> ToolResult:
try:
# Implement SMTP sending
import smtplib
# ... SMTP logic ...
return ToolResult(success=True, output=f"Email sent to {to}")
except Exception as e:
return ToolResult(success=False, error=str(e))A skill that doesn't implement actual functionality. Used for skills defined in SKILL.md without Python implementations.
from clawlet.skills.base import PlaceholderSkill
class PlaceholderSkill(BaseSkill):
def __init__(
self,
metadata: SkillMetadata,
skill_path: Optional[Path] = None,
instructions: str = ""
):
super().__init__(metadata, skill_path)
self._instructions = instructions
async def _execute_tool_impl(self, tool_name: str, **kwargs) -> ToolResult:
return ToolResult(
success=False,
error=f"Skill '{self.name}' tool '{tool_name}' is defined but not implemented."
)When tools are called on a PlaceholderSkill, it returns an error indicating the skill needs implementation.
Parser and loader for SKILL.md files.
from clawlet.skills.loader import SkillLoader, SkillLoadError, discover_skillsException raised when skill loading fails.
class SkillLoadError(Exception):
passParse SKILL.md content into frontmatter and markdown.
content = """---
name: email
version: "1.0.0"
---
# Email Skill
Instructions...
"""
frontmatter, markdown = SkillLoader.parse_skill_md(content)
# frontmatter = {"name": "email", "version": "1.0.0"}
# markdown = "# Email Skill\nInstructions..."Parse a tool parameter from frontmatter data.
param_data = {
"name": "to",
"type": "string",
"description": "Recipient",
"required": True
}
param = SkillLoader.parse_tool_parameter(param_data)Parse a tool definition from frontmatter.
tool_data = {
"name": "send_email",
"description": "Send an email",
"parameters": [...]
}
tool = SkillLoader.parse_tool_definition(tool_data)Parse skill metadata from frontmatter.
metadata = SkillLoader.parse_metadata(frontmatter)Load a skill from a SKILL.md file.
from pathlib import Path
skill = SkillLoader.load_from_file(Path("~/.clawlet/skills/email/SKILL.md"))
print(skill.name) # "email"Load a skill from a directory containing SKILL.md.
skill = SkillLoader.load_from_directory(Path("~/.clawlet/skills/email"))Discover and load all skills in a directory.
from pathlib import Path
from clawlet.skills.loader import discover_skills
skills = discover_skills(Path("~/.clawlet/skills"))
for skill in skills:
print(f"Found: {skill.name}")Central registry for managing skills.
from clawlet.skills.registry import SkillRegistry
from clawlet.tools.registry import ToolRegistryregistry = SkillRegistry(
tool_registry=tool_registry, # Optional ToolRegistry
config={"email": {"smtp_server": "..."}}, # Optional config
)| Property | Type | Description |
|---|---|---|
skills |
dict[str, BaseSkill] |
All registered skills |
Add a directory to search for skills. Returns count of loaded skills.
count = registry.add_skill_directory(Path("~/.clawlet/skills"))
print(f"Loaded {count} skills")Load skills from multiple directories (in priority order).
total = registry.load_from_directories([
Path("~/.clawlet/skills"),
Path("./skills"),
])Load bundled skills shipped with Clawlet.
count = registry.load_bundled_skills()Manually register a skill.
registry.register(my_skill)Unregister a skill by name.
registry.unregister("email")Get a skill by name.
skill = registry.get("email")
if skill:
print(skill.description)Get all registered skills.
for skill in registry.all_skills():
print(f"{skill.name}: {skill.description}")Get all enabled skills.
for skill in registry.enabled_skills():
print(skill.name)Disable a skill by name.
registry.disable_skill("email")Enable a previously disabled skill.
registry.enable_skill("email")Configure a specific skill.
success = registry.configure_skill("email", {
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
})Configure all skills with nested config.
registry.configure_all({
"email": {
"smtp_server": "smtp.gmail.com",
},
"calendar": {
"provider": "google",
},
})Get all tool definitions from enabled skills.
tools = registry.get_all_tools()
for tool in tools:
print(tool.name)Get all tools in OpenAI format.
tools = registry.to_openai_tools()
# Pass to LLM API...Register all skill tools with the ToolRegistry. Returns count registered.
count = registry.register_tools_with_registry()
print(f"Registered {count} tools")Execute a skill tool by namespaced name (async).
result = await registry.execute_tool(
"email_send_email",
to="user@example.com",
subject="Hello",
body="World"
)Get instructions for all enabled skills.
instructions = registry.get_skill_instructions()
for name, content in instructions.items():
print(f"=== {name} ===\n{content}")Validate requirements for all skills. Returns dict of skill names to missing requirements.
missing = registry.validate_all_requirements()
if missing:
for skill_name, reqs in missing.items():
print(f"{skill_name} missing: {reqs}")Skills integrate with the ToolRegistry for unified tool management.
from clawlet.tools.registry import ToolRegistry
from clawlet.skills.registry import SkillRegistry
# Create tool registry
tool_registry = ToolRegistry()
# Create skill registry with tool registry
skill_registry = SkillRegistry(tool_registry=tool_registry)
# Load skills
skill_registry.load_bundled_skills()
skill_registry.add_skill_directory(Path("~/.clawlet/skills"))
# Register skill tools with tool registry
skill_registry.register_tools_with_registry()When tools are registered, a dynamic wrapper class is created:
class SkillTool:
"""Dynamic tool wrapper for skill tools."""
@property
def name(self) -> str:
return self._ns_name # Namespaced name
@property
def description(self) -> str:
return self._tool_def.description
@property
def parameters_schema(self) -> dict:
return self._tool_def.to_openai_schema()["function"]["parameters"]
async def execute(self, **kwargs) -> ToolResult:
return await self._skill.execute_tool(self._tool_def.name, **kwargs)from clawlet.skills.base import BaseSkill, SkillMetadata, ToolDefinition, ToolParameter
from clawlet.skills.registry import SkillRegistry
from clawlet.tools.registry import ToolResult
class WeatherSkill(BaseSkill):
"""Weather skill implementation."""
async def _execute_tool_impl(self, tool_name: str, **kwargs) -> ToolResult:
if tool_name == "get_weather":
location = kwargs.get("location")
# Call weather API...
return ToolResult(
success=True,
output=f"Weather for {location}: Sunny, 22°C"
)
return ToolResult(success=False, error=f"Unknown tool: {tool_name}")
# Create metadata
metadata = SkillMetadata(
name="weather",
version="1.0.0",
description="Get weather information",
author="developer",
requires=["weather_api_key"],
tools=[
ToolDefinition(
name="get_weather",
description="Get current weather",
parameters=[
ToolParameter(
name="location",
type="string",
description="City name",
required=True
)
]
)
]
)
# Create skill instance
weather_skill = WeatherSkill(metadata)
# Configure and register
weather_skill.configure({"weather_api_key": "your-key"})
registry = SkillRegistry()
registry.register(weather_skill)from pathlib import Path
from clawlet.skills.loader import SkillLoader
# Load from file
skill = SkillLoader.load_from_file(Path("skills/weather/SKILL.md"))
# Or from directory
skill = SkillLoader.load_from_directory(Path("skills/weather"))from clawlet.skills.base import PlaceholderSkill
# Create from parsed data
skill = PlaceholderSkill(
metadata=metadata,
skill_path=Path("skills/weather"),
instructions="# Weather Skill\n\nInstructions..."
)Raised when SKILL.md parsing fails.
from clawlet.skills.loader import SkillLoader, SkillLoadError
try:
skill = SkillLoader.load_from_file(Path("skills/broken/SKILL.md"))
except SkillLoadError as e:
print(f"Failed to load skill: {e}")Common causes:
- Missing YAML frontmatter
- Invalid YAML syntax
- Missing required fields
Tools return errors via ToolResult:
result = await skill.execute_tool("send_email", to="test@example.com")
if not result.success:
print(f"Error: {result.error}")Common error scenarios:
- Missing required parameters
- Tool not found in skill
- Skill not implemented (PlaceholderSkill)
- Configuration missing
Check requirements before using skills:
is_valid, missing = skill.validate_requirements()
if not is_valid:
print(f"Cannot use skill: missing {missing}")
# Handle missing configurationimport asyncio
from pathlib import Path
from clawlet.skills import SkillRegistry, SkillLoader
from clawlet.tools.registry import ToolRegistry
async def main():
# Create registries
tool_registry = ToolRegistry()
skill_registry = SkillRegistry(tool_registry=tool_registry)
# Load skills
skill_registry.load_bundled_skills()
skill_registry.add_skill_directory(Path("~/.clawlet/skills"))
# Configure skills
skill_registry.configure_all({
"email": {
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
"smtp_user": "user@gmail.com",
"smtp_password": "app-password",
}
})
# Validate
missing = skill_registry.validate_all_requirements()
if missing:
print(f"Warning: Missing configuration: {missing}")
# Register tools
count = skill_registry.register_tools_with_registry()
print(f"Registered {count} tools")
# Get OpenAI tools for LLM
tools = skill_registry.to_openai_tools()
print(f"Available tools: {[t['function']['name'] for t in tools]}")
# Execute a tool
result = await skill_registry.execute_tool(
"email_send_email",
to="recipient@example.com",
subject="Hello",
body="This is a test email."
)
if result.success:
print(f"Success: {result.output}")
else:
print(f"Failed: {result.error}")
if __name__ == "__main__":
asyncio.run(main())- Skills Documentation - User guide for creating skills
- Tool Registry - Tool management implementation
- Bundled Skills - Example skill implementations