Skip to navigation

Custom Skills

View as MarkdownOpen in Claude

Creating custom skills is worthwhile when you have functionality you want to reuse across multiple agents or share with your team. A skill packages a capability — functions, prompts, hints, and configuration — into a single reusable unit.

When to Create a Custom Skill

Create a skill when:

  • You’ll use the same functionality in multiple agents
  • You want to share a capability with your team
  • The functionality is complex enough to benefit from encapsulation
  • You want version-controlled, tested components

Just use define_tool() when:

  • The function is specific to one agent
  • You need quick iteration during development
  • The logic is simple and unlikely to be reused

Skill Structure

Create a directory with these files:

my_custom_skill/
__init__.py # Empty or exports skill class
skill.py # Skill implementation
requirements.txt # Optional dependencies

What each file does:

FilePurpose
__init__.pyMakes the directory a Python package. Can be empty or export the skill class
skill.pyContains the skill class that inherits from SkillBase
requirements.txtLists Python packages the skill needs (pip format)

Basic Custom Skill

## my_custom_skill/skill.py
from typing import List, Dict, Any
from signalwire.skills import SkillBase
from signalwire.core.function_result import FunctionResult
class GreetingSkill(SkillBase):
"""A skill that provides personalized greetings"""
# Required class attributes
SKILL_NAME = "greeting"
SKILL_DESCRIPTION = "Provides personalized greetings"
SKILL_VERSION = "1.0.0"
# Optional requirements
REQUIRED_PACKAGES = []
REQUIRED_ENV_VARS = []
def setup(self) -> bool:
"""Initialize the skill. Return True if successful."""
# Get configuration parameter with default
self.greeting_style = self.params.get("style", "friendly")
return True
def register_tools(self) -> None:
"""Register SWAIG tools with the agent."""
self.define_tool(
name="greet_user",
description="Generate a personalized greeting",
parameters={
"name": {
"type": "string",
"description": "Name of the person to greet"
}
},
handler=self.greet_handler
)
def greet_handler(self, args, raw_data):
"""Handle greeting requests."""
name = args.get("name", "friend")
if self.greeting_style == "formal":
greeting = f"Good day, {name}. How may I assist you?"
else:
greeting = f"Hey {name}! Great to hear from you!"
return FunctionResult(greeting)

Required Class Attributes

AttributeTypeDescription
SKILL_NAMEstrUnique identifier for the skill
SKILL_DESCRIPTIONstrHuman-readable description
SKILL_VERSIONstrSemantic version (default: “1.0.0”)

Optional Attributes:

AttributeTypeDescription
REQUIRED_PACKAGESList[str]Python packages needed
REQUIRED_ENV_VARSList[str]Environment variables needed
SUPPORTS_MULTIPLE_INSTANCESboolAllow multiple instances

Required Methods

setup()

Initialize the skill and validate requirements:

def setup(self) -> bool:
if not self.validate_packages():
return False
if not self.validate_env_vars():
return False
self.api_url = self.params.get("api_url", "https://api.example.com")
self.timeout = self.params.get("timeout", 30)
return True

register_tools()

Register SWAIG functions:

def register_tools(self) -> None:
self.define_tool(
name="my_function",
description="Does something useful",
parameters={
"param1": {"type": "string", "description": "First parameter"},
"param2": {"type": "integer", "description": "Second parameter"}
},
handler=self.my_handler
)

Optional Methods

get_hints()

def get_hints(self) -> List[str]:
return ["greeting", "hello", "hi", "welcome"]

get_prompt_sections()

def get_prompt_sections(self) -> List[Dict[str, Any]]:
return [{
"title": "Greeting Capability",
"body": "You can greet users by name.",
"bullets": ["Use greet_user when someone introduces themselves",
"Match the greeting style to the conversation tone"]
}]

get_global_data()

def get_global_data(self) -> Dict[str, Any]:
return {"greeting_skill_enabled": True, "greeting_style": self.greeting_style}

cleanup()

def cleanup(self) -> None:
if hasattr(self, "connection"):
self.connection.close()

Suppressing Prompt Injection

By default, skills inject POM sections into the agent’s prompt via get_prompt_sections(). To suppress this behavior, set skip_prompt in the skill parameters:

agent.add_skill("my_skill", {"skip_prompt": True, "api_key": "..."})

When building custom skills, override _get_prompt_sections() instead of get_prompt_sections():

class MySkill(SkillBase):
def _get_prompt_sections(self) -> List[Dict[str, Any]]:
return [{"title": "My Skill", "body": "Usage instructions..."}]

Namespaced Global Data Helpers

When building skills that store state in global_data, use the namespaced helper methods to avoid collisions between skill instances:

class StatefulSkill(SkillBase):
SKILL_NAME = "stateful_skill"
SUPPORTS_MULTIPLE_INSTANCES = True
def my_handler(self, args, raw_data):
state = self.get_skill_data(raw_data)
count = state.get("call_count", 0)
result = FunctionResult(f"Call #{count + 1}")
self.update_skill_data(result, {"call_count": count + 1})
return result

Available methods:

MethodDescription
get_skill_data(raw_data)Read this skill’s namespaced state from raw_data["global_data"]
update_skill_data(result, data)Write state under the skill’s namespace via result.update_global_data()
_get_skill_namespace()Get the namespace key (e.g., "skill:my_prefix" or "skill:instance_key")

Parameter Schema

Define parameters your skill accepts:

@classmethod
def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]:
schema = super().get_parameter_schema()
schema.update({
"style": {"type": "string", "description": "Greeting style", "default": "friendly",
"enum": ["friendly", "formal", "casual"], "required": False},
"api_key": {"type": "string", "description": "API key for external service",
"required": True, "hidden": True, "env_var": "MY_SKILL_API_KEY"}
})
return schema

Multi-Instance Skills

Support multiple instances with different configurations:

class MultiInstanceSkill(SkillBase):
SKILL_NAME = "multi_search"
SUPPORTS_MULTIPLE_INSTANCES = True
def get_instance_key(self) -> str:
tool_name = self.params.get("tool_name", self.SKILL_NAME)
return f"{self.SKILL_NAME}_{tool_name}"
def setup(self) -> bool:
self.tool_name = self.params.get("tool_name", "search")
return True
def register_tools(self) -> None:
self.define_tool(
name=self.tool_name,
description="Search function",
parameters={"query": {"type": "string", "description": "Search query"}},
handler=self.search_handler
)

Using Custom Skills

Register the skill directory and use the custom skill in your agent:

LanguageRegister & use custom skill
Pythonskill_registry.add_skill_directory("/path/to/skills") then agent.add_skill("product_search", {...})
TypeScriptawait SkillRegistry.getInstance().discoverFromDirectory('/path/to/skills') then await agent.addSkillByName('product_search', {...})
from signalwire.skills.registry import skill_registry
## Add your skills directory
skill_registry.add_skill_directory("/path/to/my_skills")
## Now use in agent
class MyAgent(AgentBase):
def __init__(self):
super().__init__(name="my-agent")
self.add_language("English", "en-US", "rime.spore")
self.add_skill("product_search", {
"api_url": "https://api.mystore.com",
"api_key": "secret"
})

Testing Custom Skills

1. Test the skill class directly:

from my_skills.product_search.skill import ProductSearchSkill
class MockAgent:
def define_tool(self, **kwargs):
print(f"Registered tool: {kwargs['name']}")
skill = ProductSearchSkill(MockAgent())
skill.params = {"api_url": "http://test", "api_key": "test"}
assert skill.setup() == True
skill.register_tools()

2. Test with a real agent using swaig-test:

swaig-test test_agent.py --dump-swml
swaig-test test_agent.py --exec search_products --query "test"

3. Validate skill structure:

from signalwire.skills.registry import skill_registry
skill_registry.add_skill_directory("/path/to/my_skills")
available = skill_registry.list_available_skills()
print(f"Available skills: {available}")

Publishing and Sharing Skills

Option 1: Git Repository

my_company_skills/
README.md
product_search/
__init__.py
skill.py
crm_integration/
__init__.py
skill.py
requirements.txt

Users clone and register:

skill_registry.add_skill_directory("/path/to/my_company_skills")

Option 2: Python Package

setup(
name="my_company_skills",
entry_points={
"signalwire.skills": [
"product_search = my_company_skills.product_search.skill:ProductSearchSkill",
"crm_integration = my_company_skills.crm_integration.skill:CRMSkill",
]
}
)

Option 3: Environment Variable

export SIGNALWIRE_SKILL_PATHS="/opt/company_skills:/home/user/my_skills"

Skill Development Best Practices

DO:

  • Use descriptive SKILL_NAME and SKILL_DESCRIPTION
  • Validate all parameters in setup()
  • Return user-friendly error messages
  • Log technical errors for debugging
  • Include speech hints for better recognition
  • Write clear prompt sections explaining usage
  • Handle network/API failures gracefully
  • Version your skills meaningfully

DON’T:

  • Hard-code configuration values
  • Expose internal errors to users
  • Skip parameter validation
  • Forget to handle edge cases
  • Make setup() do heavy work (defer to first use)
  • Use global state between instances