Add a Skill¶
This guide shows you how to create a skill -- a self-contained package of tools that the agent discovers automatically at startup.
Introduction¶
Skills are the easiest way to extend CianaParrot. A skill is a folder inside workspace/skills/ containing two files:
SKILL.md-- a markdown file with YAML frontmatter that describes the skill to the agentskill.py-- a Python file with@tool-decorated functions that auto-register as agent tools
DeepAgents discovers skills at startup by scanning the skills directory. The agent receives the skill description as context and can call the skill's tools during conversations.
Prerequisites¶
- A working CianaParrot installation (Installation Guide)
- The skills system enabled in config (default:
skills.enabled: true) - Basic familiarity with LangChain's
@tooldecorator
Step 1: Create the Skill Directory¶
Path inside Docker
The workspace/ directory is mounted into the container at /app/workspace. The agent sees skills at the virtual path skills/my-skill/ relative to the workspace root.
Step 2: Write the Skill Description¶
Create SKILL.md with YAML frontmatter:
---
name: My Skill
description: Provides tools for managing grocery lists
---
# My Skill
This skill helps manage grocery lists. Use it when the user asks about
shopping, groceries, or meal planning.
## Available Tools
- `add_grocery_item` -- Add an item to the grocery list
- `list_groceries` -- Show the current grocery list
- `clear_groceries` -- Clear the grocery list
The YAML frontmatter fields:
| Field | Required | Description |
|---|---|---|
name |
Yes | Display name for the skill |
description |
Yes | Short description shown to the agent |
requires_env |
No | Environment variable(s) that must be set |
requires_bridge |
No | Bridge(s) that must be configured in the gateway |
Step 3: Implement the Tool Functions¶
Create skill.py with @tool-decorated functions:
"""Grocery list skill."""
import json
from pathlib import Path
from langchain_core.tools import tool
# Skills run inside the agent's workspace sandbox.
# Use a path relative to the skill directory for data storage.
_DATA_FILE = Path(__file__).parent / "groceries.json"
def _load() -> list[str]:
if _DATA_FILE.exists():
return json.loads(_DATA_FILE.read_text())
return []
def _save(items: list[str]) -> None:
_DATA_FILE.write_text(json.dumps(items, indent=2))
@tool
def add_grocery_item(item: str) -> str:
"""Add an item to the grocery list.
Args:
item: The grocery item to add (e.g. "milk", "2 avocados").
"""
items = _load()
items.append(item)
_save(items)
return f"Added '{item}'. List now has {len(items)} item(s)."
@tool
def list_groceries() -> str:
"""Show all items currently on the grocery list."""
items = _load()
if not items:
return "The grocery list is empty."
numbered = [f"{i+1}. {item}" for i, item in enumerate(items)]
return "Grocery list:\n" + "\n".join(numbered)
@tool
def clear_groceries() -> str:
"""Clear all items from the grocery list."""
_save([])
return "Grocery list cleared."
Tool docstrings matter
The agent sees the function name, docstring, and parameter annotations. Write clear, specific docstrings so the agent knows when and how to use each tool.
Step 4: (Optional) Add Conditional Requirements¶
Require an environment variable¶
If your skill needs an API key, declare it in the frontmatter:
---
name: My Skill
description: Provides tools for managing grocery lists
requires_env: "GROCERY_API_KEY"
---
If GROCERY_API_KEY is not set in the environment, the skill is silently skipped at startup. You can also require multiple variables:
Require a gateway bridge¶
If your skill depends on a host bridge:
---
name: My Skill
description: Controls the smart fridge via host bridge
requires_bridge: "smart-fridge"
---
The middleware (src/middleware.py) checks bridge availability at startup and filters out skills whose bridges are not configured in gateway.bridges.
Step 5: Verify Discovery¶
Restart the container to trigger skill discovery:
Check the logs for confirmation:
You should see output similar to:
No registration code needed
Unlike tools (which require manual wiring in agent.py), skills are auto-discovered by DeepAgents from the workspace/skills/ directory. Just drop the folder in place and restart.
Step 6: Test It¶
Manual test via Telegram¶
Send a message to the bot:
Add milk, eggs, and bread to my grocery list
The agent should recognize the intent, call add_grocery_item three times, and confirm each addition.
Unit test¶
import sys
from pathlib import Path
# Add the skill directory to the path for direct import
sys.path.insert(0, str(Path("workspace/skills/my-skill")))
from skill import add_grocery_item, list_groceries, clear_groceries
def test_add_and_list(tmp_path, monkeypatch):
"""Test adding items and listing them."""
monkeypatch.setattr("skill._DATA_FILE", tmp_path / "groceries.json")
result = add_grocery_item.invoke({"item": "milk"})
assert "Added 'milk'" in result
result = list_groceries.invoke({})
assert "milk" in result
def test_clear(tmp_path, monkeypatch):
"""Test clearing the list."""
monkeypatch.setattr("skill._DATA_FILE", tmp_path / "groceries.json")
add_grocery_item.invoke({"item": "eggs"})
clear_groceries.invoke({})
result = list_groceries.invoke({})
assert "empty" in result
Async Skills¶
If your skill needs to make async calls (HTTP requests, database queries), use async def:
import httpx
from langchain_core.tools import tool
@tool
async def fetch_recipe(query: str) -> str:
"""Search for a recipe online.
Args:
query: What to search for (e.g. "pasta carbonara").
"""
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(
"https://api.example.com/recipes",
params={"q": query},
)
resp.raise_for_status()
data = resp.json()
if not data["results"]:
return "No recipes found."
recipe = data["results"][0]
return f"**{recipe['title']}**\n{recipe['url']}"
Summary¶
| Step | What You Did |
|---|---|
| 1 | Created workspace/skills/my-skill/ directory |
| 2 | Wrote SKILL.md with YAML frontmatter description |
| 3 | Implemented @tool functions in skill.py |
| 4 | (Optional) Added requires_env or requires_bridge gating |
| 5 | Verified discovery in logs after restart |
| 6 | Tested via Telegram and unit tests |