Skip to content

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 agent
  • skill.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 @tool decorator

Step 1: Create the Skill Directory

mkdir -p workspace/skills/my-skill

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:

workspace/skills/my-skill/SKILL.md
---
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:

workspace/skills/my-skill/skill.py
"""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:

workspace/skills/my-skill/SKILL.md (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:

requires_env:
  - "GROCERY_API_KEY"
  - "GROCERY_STORE_ID"

Require a gateway bridge

If your skill depends on a host bridge:

workspace/skills/my-skill/SKILL.md (frontmatter)
---
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:

make restart

Check the logs for confirmation:

make logs

You should see output similar to:

Skills directory: /app/workspace/skills (workspace-relative)

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

tests/test_grocery_skill.py
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:

workspace/skills/my-skill/skill.py
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