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

1## my_custom_skill/skill.py
2
3from typing import List, Dict, Any
4from signalwire.skills import SkillBase
5from signalwire.core.function_result import FunctionResult
6
7class GreetingSkill(SkillBase):
8 """A skill that provides personalized greetings"""
9
10 # Required class attributes
11 SKILL_NAME = "greeting"
12 SKILL_DESCRIPTION = "Provides personalized greetings"
13 SKILL_VERSION = "1.0.0"
14
15 # Optional requirements
16 REQUIRED_PACKAGES = []
17 REQUIRED_ENV_VARS = []
18
19 def setup(self) -> bool:
20 """Initialize the skill. Return True if successful."""
21 # Get configuration parameter with default
22 self.greeting_style = self.params.get("style", "friendly")
23 return True
24
25 def register_tools(self) -> None:
26 """Register SWAIG tools with the agent."""
27 self.define_tool(
28 name="greet_user",
29 description="Generate a personalized greeting",
30 parameters={
31 "name": {
32 "type": "string",
33 "description": "Name of the person to greet"
34 }
35 },
36 handler=self.greet_handler
37 )
38
39 def greet_handler(self, args, raw_data):
40 """Handle greeting requests."""
41 name = args.get("name", "friend")
42
43 if self.greeting_style == "formal":
44 greeting = f"Good day, {name}. How may I assist you?"
45 else:
46 greeting = f"Hey {name}! Great to hear from you!"
47
48 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_MULTIPLEboolAllow multiple instances

Required Methods

setup()

Initialize the skill and validate requirements:

1def setup(self) -> bool:
2 if not self.validate_packages():
3 return False
4 if not self.validate_env_vars():
5 return False
6 self.api_url = self.params.get("api_url", "https://api.example.com")
7 self.timeout = self.params.get("timeout", 30)
8 return True

register_tools()

Register SWAIG functions:

1def register_tools(self) -> None:
2 self.define_tool(
3 name="my_function",
4 description="Does something useful",
5 parameters={
6 "param1": {"type": "string", "description": "First parameter"},
7 "param2": {"type": "integer", "description": "Second parameter"}
8 },
9 handler=self.my_handler
10 )

Optional Methods

get_hints()

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

get_prompt_sections()

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

get_global_data()

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

cleanup()

1def cleanup(self) -> None:
2 if hasattr(self, "connection"):
3 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:

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

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

1class MySkill(SkillBase):
2 def _get_prompt_sections(self) -> List[Dict[str, Any]]:
3 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:

1class StatefulSkill(SkillBase):
2 SKILL_NAME = "stateful_skill"
3 SUPPORTS_MULTIPLE_INSTANCES = True
4
5 def my_handler(self, args, raw_data):
6 state = self.get_skill_data(raw_data)
7 count = state.get("call_count", 0)
8 result = FunctionResult(f"Call #{count + 1}")
9 self.update_skill_data(result, {"call_count": count + 1})
10 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:

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

Multi-Instance Skills

Support multiple instances with different configurations:

1class MultiInstanceSkill(SkillBase):
2 SKILL_NAME = "multi_search"
3 SUPPORTS_MULTIPLE_INSTANCES = True
4
5 def get_instance_key(self) -> str:
6 tool_name = self.params.get("tool_name", self.SKILL_NAME)
7 return f"{self.SKILL_NAME}_{tool_name}"
8
9 def setup(self) -> bool:
10 self.tool_name = self.params.get("tool_name", "search")
11 return True
12
13 def register_tools(self) -> None:
14 self.define_tool(
15 name=self.tool_name,
16 description="Search function",
17 parameters={"query": {"type": "string", "description": "Search query"}},
18 handler=self.search_handler
19 )

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", {...})
TypeScriptskillRegistry.addSkillDirectory('/path/to/skills') then agent.addSkill('product_search', {...})
1from signalwire.skills.registry import skill_registry
2
3## Add your skills directory
4skill_registry.add_skill_directory("/path/to/my_skills")
5
6## Now use in agent
7class MyAgent(AgentBase):
8 def __init__(self):
9 super().__init__(name="my-agent")
10 self.add_language("English", "en-US", "rime.spore")
11 self.add_skill("product_search", {
12 "api_url": "https://api.mystore.com",
13 "api_key": "secret"
14 })

Testing Custom Skills

1. Test the skill class directly:

1from my_skills.product_search.skill import ProductSearchSkill
2
3class MockAgent:
4 def define_tool(self, **kwargs):
5 print(f"Registered tool: {kwargs['name']}")
6
7skill = ProductSearchSkill(MockAgent())
8skill.params = {"api_url": "http://test", "api_key": "test"}
9assert skill.setup() == True
10skill.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:

1from signalwire.skills.registry import skill_registry
2skill_registry.add_skill_directory("/path/to/my_skills")
3available = skill_registry.list_available_skills()
4print(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:

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

Option 2: Python Package

1setup(
2 name="my_company_skills",
3 entry_points={
4 "signalwire.skills": [
5 "product_search = my_company_skills.product_search.skill:ProductSearchSkill",
6 "crm_integration = my_company_skills.crm_integration.skill:CRMSkill",
7 ]
8 }
9)

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