{"slug":"agents-skills-system","title":"Agents Skills System — Prefabs, SkillBase, Multi-Instance, and Discovery","tags":["signalwire","agents-sdk","skills","prefabs","skillbase"],"agent_summary":"Pre-built agent prefabs (ConciergeAgent, FAQBotAgent, InfoGathererAgent, ReceptionistAgent, SurveyAgent) and the custom SkillBase pattern. Covers `add_skill`, multi-instance keying, parameter schemas, REQUIRED_PACKAGES/ENV_VARS validation, and skill discovery directories.","trigger_phrases":["SignalWire skills system","ConciergeAgent FAQBotAgent","custom SignalWire skill","SkillBase REQUIRED_ENV_VARS","add_skill multi instance","register_skill add_skill_directory"],"runnable":true,"markdown":"\n# Agents Skills System\n\nA \"skill\" in the SignalWire Agents SDK is a reusable bundle of: prompt sections, SWAIG functions, parameter schemas, env-var/package validation, and lifecycle hooks. Skills come in two forms — **prefabs** that ship with the SDK, and **custom skills** you write by subclassing `SkillBase`.\n\n## Prefabs — ready-made agents\n\n```python\nfrom signalwire.prefabs import (\n    ConciergeAgent, FAQBotAgent, InfoGathererAgent,\n    ReceptionistAgent, SurveyAgent,\n)\n```\n\n### ConciergeAgent — venue/hospitality concierge\n\n```python\nagent = ConciergeAgent(\n    venue_name=\"The Riverside Resort\",\n    services=[\"room service\", \"spa treatments\", \"restaurant reservations\"],\n    amenities={\n        \"pool\": {\"hours\": \"6 AM - 10 PM\", \"location\": \"Ground Floor, East Wing\"},\n        \"spa\":  {\"hours\": \"9 AM - 9 PM\",  \"location\": \"Level 3, East Wing\"},\n    },\n    hours_of_operation={\"front desk\": \"24 hours\", \"concierge\": \"7 AM - 11 PM\"},\n    special_instructions=[\"Mention the daily happy hour at the pool bar (4-6 PM).\"],\n    welcome_message=\"Welcome to The Riverside Resort! How may I assist you?\",\n)\n```\n\nBuilt-in tools: `check_availability(service, date, time)`, `get_directions(location)`.\n\n### FAQBotAgent — knowledge-base FAQ bot\n\n```python\nagent = FAQBotAgent(\n    faqs=[\n        {\"question\": \"What is the warranty period?\",\n         \"answer\": \"All products come with a 2-year warranty.\",\n         \"categories\": [\"warranty\"]},\n        {\"question\": \"How do I return a product?\",\n         \"answer\": \"Start a return within 30 days at returns.example.com.\",\n         \"categories\": [\"returns\"]},\n    ],\n    suggest_related=True,\n    persona=\"You are a helpful product specialist for TechGadgets Inc.\",\n)\n```\n\nBest for up to 50 FAQs. For larger knowledge bases, use the `native_vector_search` skill instead.\n\n### InfoGathererAgent — sequential question collection\n\n```python\nagent = InfoGathererAgent(\n    questions=[\n        {\"key_name\": \"name\", \"question_text\": \"What is your name?\"},\n        {\"key_name\": \"email\", \"question_text\": \"What is your email?\", \"confirm\": True},\n        {\"key_name\": \"issue\", \"question_text\": \"Describe your issue.\"},\n    ]\n)\n```\n\n`confirm: True` makes the agent read the answer back for critical fields. Answers land in `global_data` and are accessible from SWAIG handlers.\n\nDynamic mode — questions decided at request time:\n\n```python\ndef get_questions(query_params, body_params, headers):\n    if query_params.get(\"type\") == \"support\":\n        return [\n            {\"key_name\": \"name\", \"question_text\": \"What is your name?\"},\n            {\"key_name\": \"issue\", \"question_text\": \"Describe your issue.\"},\n        ]\n    return [\n        {\"key_name\": \"name\", \"question_text\": \"What is your name?\"},\n        {\"key_name\": \"message\", \"question_text\": \"How can I help?\"},\n    ]\n\nagent = InfoGathererAgent()\nagent.set_question_callback(get_questions)\n```\n\n### ReceptionistAgent and SurveyAgent\n\n`ReceptionistAgent` — basic phone receptionist with routing. `SurveyAgent` — outbound or inbound surveys with structured response capture. Same constructor pattern as the others.\n\n## Adding skills to a custom agent — `add_skill`\n\n```python\nfrom signalwire import AgentBase\n\nagent = AgentBase(name=\"demo\")\nagent.add_skill(\"notify\")\nagent.add_skill(\"native_vector_search\", {\"index_path\": \"docs.index\"})\nagent.add_skill(\"datetime\")\n```\n\nEach `add_skill` call:\n\n1. Looks up the skill class.\n2. Runs `setup()` — validates env vars and packages, opens API clients.\n3. Calls `register_tools()` — adds SWAIG functions to the agent.\n4. Injects prompt sections from `_get_prompt_sections()` unless `skip_prompt: True`.\n\n## Multi-instance skills — `tool_name`\n\nSome skills can be added more than once (different config per instance). The `tool_name` parameter creates unique instance keys.\n\n```python\nagent.add_skill(\"notify\")                            # instance key: \"notify\"\nagent.add_skill(\"notify\", {\"tool_name\": \"email\"})    # instance key: \"notify_email\"\nagent.add_skill(\"notify\", {\"tool_name\": \"sms\"})      # instance key: \"notify_sms\"\n```\n\nEach instance gets its own SWAIG function namespace.\n\n## Writing a custom skill — `SkillBase`\n\n```python\nimport os\nfrom signalwire.core.skill_base import SkillBase\n\nclass WeatherSkill(SkillBase):\n    SKILL_NAME = \"weather\"\n    SKILL_DESCRIPTION = \"Provides weather information\"\n    REQUIRED_PACKAGES = [\"requests\"]\n    REQUIRED_ENV_VARS = [\"WEATHER_API_KEY\"]\n\n    @classmethod\n    def get_parameter_schema(cls):\n        schema = super().get_parameter_schema()\n        schema.update({\n            \"units\": {\n                \"type\": \"string\",\n                \"description\": \"Temperature units\",\n                \"default\": \"fahrenheit\",\n                \"enum\": [\"fahrenheit\", \"celsius\"],\n            },\n            \"api_key\": {\n                \"type\": \"string\",\n                \"description\": \"Weather API key\",\n                \"required\": True,\n                \"hidden\": True,\n                \"env_var\": \"WEATHER_API_KEY\",\n            },\n        })\n        return schema\n\n    def setup(self) -> bool:\n        if not self.validate_packages():\n            return False\n        if not self.validate_env_vars():\n            return False\n        self.api_key = os.getenv(\"WEATHER_API_KEY\")\n        return True\n\n    def _get_prompt_sections(self):\n        return [\n            {\"title\": \"Weather\", \"body\": \"You can check weather using the get_weather tool.\"},\n            {\"title\": \"Weather Guidelines\", \"bullets\": [\n                \"Always confirm the location before checking.\",\n                \"Report in the user's preferred units.\",\n            ]},\n        ]\n\n    def register_tools(self):\n        @self.agent.tool(description=\"Get current weather for a city\")\n        def get_weather(args, raw_data=None):\n            from signalwire import FunctionResult\n            import requests\n            r = requests.get(\"https://api.weatherapi.com/v1/current.json\",\n                             params={\"key\": self.api_key, \"q\": args.get(\"city\")})\n            data = r.json()\n            return FunctionResult(\n                f\"Weather in {args.get('city')}: {data['current']['condition']['text']}, \"\n                f\"{data['current']['temp_f']}F\"\n            )\n```\n\n### Required SkillBase methods\n\n| Method | Required | Purpose |\n|---|---|---|\n| `setup()` | Yes | Validate env, packages; init clients. Return True on success. |\n| `register_tools()` | Yes | Register SWAIG functions on `self.agent`. |\n| `_get_prompt_sections()` | No | Return list of POM sections. Respects `skip_prompt`. |\n| `get_parameter_schema()` (class method) | No | Document parameters for UIs / discovery. |\n\n### Class attributes\n\n| Attribute | Notes |\n|---|---|\n| `SKILL_NAME` | String key used in `add_skill(\"name\")`. |\n| `SKILL_DESCRIPTION` | Human-readable summary. |\n| `REQUIRED_PACKAGES` | List of pip packages required at setup time. |\n| `REQUIRED_ENV_VARS` | List of env var names required. |\n\n### Validators on `SkillBase`\n\n```python\nself.validate_packages()  # True/False; logs missing packages\nself.validate_env_vars()  # True/False; logs missing env vars\n```\n\n## Built-in parameter fields\n\nEvery skill accepts these:\n\n| Parameter | Notes |\n|---|---|\n| `swaig_fields` | Extra SWAIG metadata merged into every tool definition this skill registers. |\n| `skip_prompt` | If True, suppress prompt-section injection. |\n| `tool_name` | Custom name for multi-instance skills. |\n\n## Skill discovery\n\nPoint an agent at a directory of skill modules to auto-register them:\n\n```python\nfrom signalwire import register_skill, add_skill_directory\n\n# Register a single skill class\nregister_skill(WeatherSkill)\n\n# Auto-discover everything in a directory (each .py file = one skill class)\nadd_skill_directory(\"/path/to/my-skills\")\n```\n\nUseful for packaging organisational tooling (CRM lookup, scheduling, knowledge bases) as reusable skill libraries.\n\n## Anti-patterns\n\n- Putting SWAIG handler imports at module top instead of inside `register_tools()` — slow startup, import errors leak before validation.\n- Skipping `REQUIRED_ENV_VARS` declaration — skill loads with missing config, fails at runtime.\n- Forgetting to return `True` from `setup()` — skill silently fails to load.\n- Mixing `add_skill` calls with manual `@agent.tool` decorators on the same domain — fights for tool name space.\n- Registering skills inside an FAQBotAgent or other prefab — prefabs lock down their tool space.\n\n## See also\n\n- [Python Agents SDK](/topic/signalwire-python-agents-sdk)\n- [SWAIG functions](/topic/swaig-functions)\n- [POM Builder](/topic/agents-pom-builder)\n- [MCP Gateway](/topic/agents-mcp-gateway)\n","html":"<h1>Agents Skills System</h1>\n<p>A \"skill\" in the SignalWire Agents SDK is a reusable bundle of: prompt sections, SWAIG functions, parameter schemas, env-var/package validation, and lifecycle hooks. Skills come in two forms — <strong>prefabs</strong> that ship with the SDK, and <strong>custom skills</strong> you write by subclassing <code>SkillBase</code>.</p>\n<h2>Prefabs — ready-made agents</h2>\n<pre><code class=\"language-python\">from signalwire.prefabs import (\n    ConciergeAgent, FAQBotAgent, InfoGathererAgent,\n    ReceptionistAgent, SurveyAgent,\n)\n</code></pre>\n<h3>ConciergeAgent — venue/hospitality concierge</h3>\n<pre><code class=\"language-python\">agent = ConciergeAgent(\n    venue_name=\"The Riverside Resort\",\n    services=[\"room service\", \"spa treatments\", \"restaurant reservations\"],\n    amenities={\n        \"pool\": {\"hours\": \"6 AM - 10 PM\", \"location\": \"Ground Floor, East Wing\"},\n        \"spa\":  {\"hours\": \"9 AM - 9 PM\",  \"location\": \"Level 3, East Wing\"},\n    },\n    hours_of_operation={\"front desk\": \"24 hours\", \"concierge\": \"7 AM - 11 PM\"},\n    special_instructions=[\"Mention the daily happy hour at the pool bar (4-6 PM).\"],\n    welcome_message=\"Welcome to The Riverside Resort! How may I assist you?\",\n)\n</code></pre>\n<p>Built-in tools: <code>check_availability(service, date, time)</code>, <code>get_directions(location)</code>.</p>\n<h3>FAQBotAgent — knowledge-base FAQ bot</h3>\n<pre><code class=\"language-python\">agent = FAQBotAgent(\n    faqs=[\n        {\"question\": \"What is the warranty period?\",\n         \"answer\": \"All products come with a 2-year warranty.\",\n         \"categories\": [\"warranty\"]},\n        {\"question\": \"How do I return a product?\",\n         \"answer\": \"Start a return within 30 days at returns.example.com.\",\n         \"categories\": [\"returns\"]},\n    ],\n    suggest_related=True,\n    persona=\"You are a helpful product specialist for TechGadgets Inc.\",\n)\n</code></pre>\n<p>Best for up to 50 FAQs. For larger knowledge bases, use the <code>native_vector_search</code> skill instead.</p>\n<h3>InfoGathererAgent — sequential question collection</h3>\n<pre><code class=\"language-python\">agent = InfoGathererAgent(\n    questions=[\n        {\"key_name\": \"name\", \"question_text\": \"What is your name?\"},\n        {\"key_name\": \"email\", \"question_text\": \"What is your email?\", \"confirm\": True},\n        {\"key_name\": \"issue\", \"question_text\": \"Describe your issue.\"},\n    ]\n)\n</code></pre>\n<p><code>confirm: True</code> makes the agent read the answer back for critical fields. Answers land in <code>global_data</code> and are accessible from SWAIG handlers.</p>\n<p>Dynamic mode — questions decided at request time:</p>\n<pre><code class=\"language-python\">def get_questions(query_params, body_params, headers):\n    if query_params.get(\"type\") == \"support\":\n        return [\n            {\"key_name\": \"name\", \"question_text\": \"What is your name?\"},\n            {\"key_name\": \"issue\", \"question_text\": \"Describe your issue.\"},\n        ]\n    return [\n        {\"key_name\": \"name\", \"question_text\": \"What is your name?\"},\n        {\"key_name\": \"message\", \"question_text\": \"How can I help?\"},\n    ]\n\nagent = InfoGathererAgent()\nagent.set_question_callback(get_questions)\n</code></pre>\n<h3>ReceptionistAgent and SurveyAgent</h3>\n<p><code>ReceptionistAgent</code> — basic phone receptionist with routing. <code>SurveyAgent</code> — outbound or inbound surveys with structured response capture. Same constructor pattern as the others.</p>\n<h2>Adding skills to a custom agent — <code>add_skill</code></h2>\n<pre><code class=\"language-python\">from signalwire import AgentBase\n\nagent = AgentBase(name=\"demo\")\nagent.add_skill(\"notify\")\nagent.add_skill(\"native_vector_search\", {\"index_path\": \"docs.index\"})\nagent.add_skill(\"datetime\")\n</code></pre>\n<p>Each <code>add_skill</code> call:</p>\n<ol>\n<li>Looks up the skill class.</li>\n<li>Runs <code>setup()</code> — validates env vars and packages, opens API clients.</li>\n<li>Calls <code>register_tools()</code> — adds SWAIG functions to the agent.</li>\n<li>Injects prompt sections from <code>_get_prompt_sections()</code> unless <code>skip_prompt: True</code>.</li>\n</ol>\n<h2>Multi-instance skills — <code>tool_name</code></h2>\n<p>Some skills can be added more than once (different config per instance). The <code>tool_name</code> parameter creates unique instance keys.</p>\n<pre><code class=\"language-python\">agent.add_skill(\"notify\")                            # instance key: \"notify\"\nagent.add_skill(\"notify\", {\"tool_name\": \"email\"})    # instance key: \"notify_email\"\nagent.add_skill(\"notify\", {\"tool_name\": \"sms\"})      # instance key: \"notify_sms\"\n</code></pre>\n<p>Each instance gets its own SWAIG function namespace.</p>\n<h2>Writing a custom skill — <code>SkillBase</code></h2>\n<pre><code class=\"language-python\">import os\nfrom signalwire.core.skill_base import SkillBase\n\nclass WeatherSkill(SkillBase):\n    SKILL_NAME = \"weather\"\n    SKILL_DESCRIPTION = \"Provides weather information\"\n    REQUIRED_PACKAGES = [\"requests\"]\n    REQUIRED_ENV_VARS = [\"WEATHER_API_KEY\"]\n\n    @classmethod\n    def get_parameter_schema(cls):\n        schema = super().get_parameter_schema()\n        schema.update({\n            \"units\": {\n                \"type\": \"string\",\n                \"description\": \"Temperature units\",\n                \"default\": \"fahrenheit\",\n                \"enum\": [\"fahrenheit\", \"celsius\"],\n            },\n            \"api_key\": {\n                \"type\": \"string\",\n                \"description\": \"Weather API key\",\n                \"required\": True,\n                \"hidden\": True,\n                \"env_var\": \"WEATHER_API_KEY\",\n            },\n        })\n        return schema\n\n    def setup(self) -> bool:\n        if not self.validate_packages():\n            return False\n        if not self.validate_env_vars():\n            return False\n        self.api_key = os.getenv(\"WEATHER_API_KEY\")\n        return True\n\n    def _get_prompt_sections(self):\n        return [\n            {\"title\": \"Weather\", \"body\": \"You can check weather using the get_weather tool.\"},\n            {\"title\": \"Weather Guidelines\", \"bullets\": [\n                \"Always confirm the location before checking.\",\n                \"Report in the user's preferred units.\",\n            ]},\n        ]\n\n    def register_tools(self):\n        @self.agent.tool(description=\"Get current weather for a city\")\n        def get_weather(args, raw_data=None):\n            from signalwire import FunctionResult\n            import requests\n            r = requests.get(\"https://api.weatherapi.com/v1/current.json\",\n                             params={\"key\": self.api_key, \"q\": args.get(\"city\")})\n            data = r.json()\n            return FunctionResult(\n                f\"Weather in {args.get('city')}: {data['current']['condition']['text']}, \"\n                f\"{data['current']['temp_f']}F\"\n            )\n</code></pre>\n<h3>Required SkillBase methods</h3>\n<p>| Method | Required | Purpose |\n|---|---|---|\n| <code>setup()</code> | Yes | Validate env, packages; init clients. Return True on success. |\n| <code>register_tools()</code> | Yes | Register SWAIG functions on <code>self.agent</code>. |\n| <code>_get_prompt_sections()</code> | No | Return list of POM sections. Respects <code>skip_prompt</code>. |\n| <code>get_parameter_schema()</code> (class method) | No | Document parameters for UIs / discovery. |</p>\n<h3>Class attributes</h3>\n<p>| Attribute | Notes |\n|---|---|\n| <code>SKILL_NAME</code> | String key used in <code>add_skill(\"name\")</code>. |\n| <code>SKILL_DESCRIPTION</code> | Human-readable summary. |\n| <code>REQUIRED_PACKAGES</code> | List of pip packages required at setup time. |\n| <code>REQUIRED_ENV_VARS</code> | List of env var names required. |</p>\n<h3>Validators on <code>SkillBase</code></h3>\n<pre><code class=\"language-python\">self.validate_packages()  # True/False; logs missing packages\nself.validate_env_vars()  # True/False; logs missing env vars\n</code></pre>\n<h2>Built-in parameter fields</h2>\n<p>Every skill accepts these:</p>\n<p>| Parameter | Notes |\n|---|---|\n| <code>swaig_fields</code> | Extra SWAIG metadata merged into every tool definition this skill registers. |\n| <code>skip_prompt</code> | If True, suppress prompt-section injection. |\n| <code>tool_name</code> | Custom name for multi-instance skills. |</p>\n<h2>Skill discovery</h2>\n<p>Point an agent at a directory of skill modules to auto-register them:</p>\n<pre><code class=\"language-python\">from signalwire import register_skill, add_skill_directory\n\n# Register a single skill class\nregister_skill(WeatherSkill)\n\n# Auto-discover everything in a directory (each .py file = one skill class)\nadd_skill_directory(\"/path/to/my-skills\")\n</code></pre>\n<p>Useful for packaging organisational tooling (CRM lookup, scheduling, knowledge bases) as reusable skill libraries.</p>\n<h2>Anti-patterns</h2>\n<ul>\n<li>Putting SWAIG handler imports at module top instead of inside <code>register_tools()</code> — slow startup, import errors leak before validation.</li>\n<li>Skipping <code>REQUIRED_ENV_VARS</code> declaration — skill loads with missing config, fails at runtime.</li>\n<li>Forgetting to return <code>True</code> from <code>setup()</code> — skill silently fails to load.</li>\n<li>Mixing <code>add_skill</code> calls with manual <code>@agent.tool</code> decorators on the same domain — fights for tool name space.</li>\n<li>Registering skills inside an FAQBotAgent or other prefab — prefabs lock down their tool space.</li>\n</ul>\n<h2>See also</h2>\n<ul>\n<li><a href=\"/topic/signalwire-python-agents-sdk\">Python Agents SDK</a></li>\n<li><a href=\"/topic/swaig-functions\">SWAIG functions</a></li>\n<li><a href=\"/topic/agents-pom-builder\">POM Builder</a></li>\n<li><a href=\"/topic/agents-mcp-gateway\">MCP Gateway</a></li>\n</ul>\n"}