Why Build Custom Skills?
OpenClaw Skills are modular extensions that give your agent new capabilities — from calling APIs and manipulating files to controlling smart devices. Instead of relying on bundled skills, build your own to integrate proprietary APIs, internal systems, or domain-specific business logic.
This guide skips the basics. If you haven't set up OpenClaw yet, read the complete Clawdbot/OpenClaw setup guide first.
Standard Skill Directory Structure
All workspace skills live under ~/.openclaw/workspace/skills/:
skills/
your-skill-name/
SKILL.md # Metadata + documentation for the agent
scripts/ # Execution logic
main.py # (or index.js, run.sh)
tools/ # Tool definitions (JSON schema)
your-tool.json
requirements.txt # Python dependencies (if applicable)
Naming convention: Use kebab-case for directory names (price-checker, not PriceChecker). The agent reads this directory name as the skill identifier.
SKILL.md — The System Prompt for Your Skill
SKILL.md is not a standard README. It's the system prompt for your skill — the agent reads it to understand when and how to invoke the skill.
# Your Skill Name
Brief description of what the skill does (1-2 sentences).
## When to Use
- [trigger condition 1]
- [trigger condition 2]
## Tools
- tool-name: Short description of what the tool does
## Limitations
- Does not handle [edge case]
- Requires [dependency]
## Permissions
- domains: api.example.com
Write SKILL.md like instructions for a junior developer: clear triggers, listed constraints, declared allowed domains. The agent uses this file to decide whether to invoke the skill at all.
Each tool in the tools/ directory is a JSON schema describing its interface:
{
"name": "check-inventory",
"description": "Check stock quantity for a product by SKU",
"parameters": {
"type": "object",
"properties": {
"sku": {
"type": "string",
"description": "Product SKU code"
},
"warehouse_id": {
"type": "string",
"description": "Warehouse ID. Default: main warehouse",
"default": "main"
}
},
"required": ["sku"]
}
}
Schema writing principles:
description must be specific enough for the LLM to know when to pass which parameter
- Set
default for optional parameters — don't let the LLM guess
- Avoid
any type — the LLM needs to know the exact expected format
The script receives input via stdin (JSON) and writes results to stdout:
import sys
import json
def check_inventory(sku: str, warehouse_id: str = "main") -> dict:
result = query_database(sku, warehouse_id)
return {
"sku": sku,
"quantity": result["qty"],
"location": result["bin"],
"last_updated": result["timestamp"]
}
if __name__ == "__main__":
params = json.loads(sys.stdin.read())
tool_name = params.get("tool")
args = params.get("args", {})
if tool_name == "check-inventory":
output = check_inventory(**args)
print(json.dumps(output))
Optimizing token usage in output: The LLM reads all tool output — verbose output means more tokens and higher latency. Rules:
- Only return necessary fields — strip redundant metadata
- For list results, cap at 10–20 items and include
"total" so the LLM knows what's left
- Return integers instead of floats when precision isn't needed
Security: Least Privilege First
Skills run with current user permissions. A poorly written skill can read/write arbitrary files or make uncontrolled network calls. Apply Least Privilege:
Filesystem isolation:
import os
from pathlib import Path
ALLOWED_DIR = Path(os.environ.get("SKILL_DATA_DIR", "/tmp/skill-data"))
def safe_read(filename: str) -> str:
target = (ALLOWED_DIR / filename).resolve()
if not str(target).startswith(str(ALLOWED_DIR)):
raise PermissionError(f"Access outside allowed directory: {filename}")
return target.read_text()
Network control:
- Whitelist allowed domains in
SKILL.md under ## Permissions
- Apply timeouts to all HTTP requests (default: 10 seconds)
- Never log credentials or sensitive data
Use environment variables for secrets:
import os
api_key = os.environ["INVENTORY_API_KEY"]
Best Practices: Logging, Error Handling, Token Efficiency
Log to stderr — keeps stdout clean for tool output:
import sys
def log(msg: str):
print(f"[skill] {msg}", file=sys.stderr)
Structured error handling:
try:
result = call_external_api(sku)
except TimeoutError:
print(json.dumps({"error": "API timeout", "retry": True}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
The agent reads the exit code to classify results: exit 0 = success, exit 1 = retryable error, exit 2 = terminal failure.
Packaging and Installation
Install from a Git repo:
clawdbot skill install https://github.com/your-org/your-skill
Install from a local directory:
clawdbot skill install ./skills/your-skill-name
Publish to ClawHub:
clawdbot skills publish ./skills/your-skill-name
This packages your skill and submits it for review. Once approved, the community can install it with:
clawdbot skills install your-skill-name
Skill in Action
After installation, restart OpenClaw:
openclaw restart
Message your agent: "check inventory for SKU-123" — it automatically discovers the matching skill and calls the tool. No additional config needed. To debug, view skill logs:
openclaw logs --skill your-skill-name
A well-built skill is a reliable tool: clean output, explicit errors, minimal permissions. Your agent is only as good as the tools it has. Explore AI agent architecture in our AI Agent 2026 analysis and how MCP Protocol standardizes tool connections in MCP Protocol: The AI Connection Standard.