Creating Skills
WASP supports three ways to create custom skills:
- Via
skill_managerskill — dynamic creation via Telegram/dashboard chat - Direct file creation — write Python class directly to
/data/skills/ - Built-in registration — add to
src/skills/builtin/and rebuild
Method 1: Via skill_manager (Recommended)
The easiest way — no container restart needed.
Basic Syntax
skill_manager(
action="create",
name="my_skill_name",
description="What this skill does",
params="param1,param2,optional_param3",
code="...Python class code..."
)
Complete Example
Create a skill that calculates compound interest:
skill_manager(
action="create",
name="compound_interest",
description="Calculate compound interest given principal, rate, and time",
params="principal,rate_percent,years,compounds_per_year",
code="
from src.skills.base import SkillBase, SkillDefinition, SkillParameter, SkillResult
class CompoundInterestSkill(SkillBase):
def definition(self):
return SkillDefinition(
name='compound_interest',
description='Calculate compound interest',
parameters=[
SkillParameter(name='principal', type='number', required=True, description='Initial amount'),
SkillParameter(name='rate_percent', type='number', required=True, description='Annual rate %'),
SkillParameter(name='years', type='number', required=True, description='Number of years'),
SkillParameter(name='compounds_per_year', type='integer', required=False, description='Compounding frequency', default=12),
]
)
async def execute(self, principal: float, rate_percent: float, years: float, compounds_per_year: int = 12) -> SkillResult:
r = rate_percent / 100
n = compounds_per_year
t = years
amount = principal * (1 + r/n) ** (n*t)
interest = amount - principal
return SkillResult(
output=f'Principal: \${principal:,.2f}\nFinal amount: \${amount:,.2f}\nInterest earned: \${interest:,.2f}\nRate: {rate_percent}% compounded {n}x/year for {t} years'
)
"
)
Verify the Skill
skill_manager(action="list")
The new skill should appear in the list as python-custom.
Method 2: Direct File Creation
Create the file directly on the host:
mkdir -p /home/agent/data/skills/my_skill
cat > /home/agent/data/skills/my_skill/skill.py << 'EOF'
from src.skills.base import SkillBase, SkillDefinition, SkillParameter, SkillResult
class MySkillSkill(SkillBase):
def definition(self):
return SkillDefinition(
name='my_skill',
description='Description here',
parameters=[
SkillParameter(name='input', type='string', required=True),
]
)
async def execute(self, input: str) -> SkillResult:
result = f"Processed: {input}"
return SkillResult(output=result)
EOF
Then restart agent-core to load it:
cd /home/agent && docker compose restart agent-core
Method 3: Built-in Registration
For skills that should always be available, add them to the source:
1. Create the Skill File
cat > /home/agent/containers/agent-core/src/skills/builtin/my_skill.py << 'EOF'
from ..base import SkillBase, SkillDefinition, SkillParameter, SkillResult
from ..capability import CapabilityLevel
class MySkill(SkillBase):
def definition(self) -> SkillDefinition:
return SkillDefinition(
name="my_skill",
description="Does something useful",
parameters=[
SkillParameter(
name="input_text",
type="string",
required=True,
description="The text to process"
),
SkillParameter(
name="max_length",
type="integer",
required=False,
description="Maximum output length",
default=500
),
],
examples=[
{"input_text": "hello world", "max_length": 100}
]
)
async def execute(self, input_text: str, max_length: int = 500) -> SkillResult:
try:
result = input_text[:max_length]
return SkillResult(output=result)
except Exception as e:
return SkillResult(error=str(e))
EOF
2. Register in __init__.py
# src/skills/builtin/__init__.py
from .my_skill import MySkill
def register_builtin_skills(registry, memory, settings=None):
# ... existing registrations ...
registry.register(MySkill())
cap_reg.register("my_skill", CapabilityLevel.SAFE)
3. Rebuild and Restart
cd /home/agent
docker compose build agent-core
docker compose up -d agent-core
SkillBase Interface
Every skill must implement:
class SkillBase(ABC):
@abstractmethod
def definition(self) -> SkillDefinition:
"""Return skill metadata."""
...
@abstractmethod
async def execute(self, **kwargs) -> SkillResult:
"""Execute the skill."""
...
SkillDefinition
@dataclass
class SkillDefinition:
name: str # Slug: lowercase, underscores
description: str # What it does (shown to LLM)
parameters: list[SkillParameter]
examples: list[dict] = field(default_factory=list)
enabled: bool = True
SkillParameter
@dataclass
class SkillParameter:
name: str
type: str # string, integer, number, boolean, array, object
required: bool
description: str = ""
default: Any = None
enum: list = None # Allowed values
SkillResult
@dataclass
class SkillResult:
output: str | None = None # Human-readable result (max 8k)
error: str | None = None # Error message if failed
metadata: dict = field(default_factory=dict) # Optional structured data
Best Practices
- Keep skills focused — one clear purpose per skill
- Handle errors gracefully — return
SkillResult(error=...)rather than raising - Limit output size — cap at 8,000 characters; LLM context is expensive
- Add good descriptions — the LLM uses descriptions to decide when to call the skill
- Include examples — helps the LLM call with correct parameters
- Choose the right capability level — use the minimum level needed
Dependency Management
For skills requiring external packages:
# In skill_manager (via self_improve.install)
self_improve(action="install", package="httpx")
# Or add to requirements.txt and rebuild
Custom skills in /data/skills/ run in the same Python environment as agent-core. All packages in requirements.txt are available.