Build Your First OpenClaw Skill
Learn to build custom OpenClaw skills. Skill structure, SKILL.md format, Python implementation, testing, and publishing to the marketplace.
Prerequisites
Python basics (3.8+) ยท OpenClaw installed ยท Familiarity with JSON
Build Your First OpenClaw Skill
OpenClaw's true power emerges when you build custom skills tailored to your specific needs. Pre-built skills are great, but a skill designed exactly for your use case can save hours every week. This guide walks through creating a complete, publishable OpenClaw skill from scratch.
What Is a Skill?
A skill is a Python module that extends OpenClaw's capabilities. It consists of:
- SKILL.md: Metadata and documentation
- main.py: Core skill logic
- requirements.txt: Python dependencies
- config.json: Configuration schema
- (optional) tests/: Unit tests
Let's build a real example: Weather Reporter Skill that provides detailed weather forecasts and severe weather alerts.
Understanding Skill Structure
weather-reporter-skill/
โโโ SKILL.md # Metadata and docs
โโโ main.py # Core logic
โโโ requirements.txt # Dependencies
โโโ config.json # Configuration schema
โโโ config.example.json # Example configuration
โโโ README.md # User-facing documentation
โโโ tests/
โ โโโ __init__.py
โ โโโ test_weather.py # Unit tests
โ โโโ test_alerts.py # Alert tests
โโโ .gitignore
Step 1: Set Up Your Development Environment
Create a project directory and initialize Git:
mkdir openclaw-weather-reporter
cd openclaw-weather-reporter
git init
Create virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
Create project structure
mkdir tests
touch main.py requirements.txt config.json SKILL.md README.md
touch tests/__init__.py tests/test_weather.py
Step 2: Define Skill Metadata (SKILL.md)
Create a comprehensive SKILL.md that describes your skill:
---
name: Weather Reporter
version: 1.0.0
author: Your Name
license: MIT
category: information
compatibility: ">= 1.0.0"
permissions:
- weather:read
- notifications:send
dependencies:
- requests>=2.28.0
- pytz>=2023.3
tags:
- weather
- forecasts
- alerts
- information
Weather Reporter Skill
Get real-time weather updates, detailed forecasts, and severe weather alerts.
Features
- Current weather conditions for any location
- 7-day forecast with hourly details
- Severe weather alerts
- Air quality reports
- UV index tracking
Configuration
Required:
- API Key: OpenWeatherMap API key (get free at openweathermap.org)
- Default Location: Your location (e.g., "San Francisco, CA")
Optional:
- Temperature Unit: Fahrenheit or Celsius
- Alert Threshold: What severity of alerts to notify on
Usage Examples
"What's the weather today?"
"Will it rain tomorrow?"
"Show me the forecast for next week"
"What severe weather alerts are active?"
"Is the UV index high today?"
Permissions
- weather:read: Read weather data
- notifications:send: Send weather alerts to user
Changelog
v1.0.0
- Initial release
- Current weather
- 7-day forecast
- Severe weather alerts
Step 3: Create Configuration Schema
Define what configuration your skill needs:
{
"name": "Weather Reporter",
"version": "1.0.0",
"required_config": [
{
"key": "api_key",
"type": "secret",
"label": "OpenWeatherMap API Key",
"description": "Get a free key at openweathermap.org",
"validation": "^[a-f0-9]{32}$"
},
{
"key": "default_location",
"type": "string",
"label": "Default Location",
"description": "City, State or City, Country",
"default": "San Francisco, CA"
}
],
"optional_config": [
{
"key": "temperature_unit",
"type": "select",
"label": "Temperature Unit",
"options": ["Fahrenheit", "Celsius"],
"default": "Fahrenheit"
},
{
"key": "alert_threshold",
"type": "select",
"label": "Alert Severity",
"options": ["All", "Moderate+", "Severe+"],
"default": "Severe+"
}
]
}
Step 4: Implement Core Logic (main.py)
"""Weather Reporter Skill for OpenClaw"""
import requests
from datetime import datetime
from typing import Dict, List, Optional
import os
class WeatherReporterSkill:
"""Provides weather information and alerts"""
def __init__(self, config: Dict):
self.api_key = config.get('api_key')
self.default_location = config.get('default_location', 'San Francisco, CA')
self.temperature_unit = config.get('temperature_unit', 'Fahrenheit')
self.alert_threshold = config.get('alert_threshold', 'Severe+')
self.base_url = "https://api.openweathermap.org/data/2.5"
def get_current_weather(self, location: Optional[str] = None) -> Dict:
"""Get current weather for a location"""
location = location or self.default_location
try:
response = requests.get(
f"{self.base_url}/weather",
params={
'q': location,
'appid': self.api_key,
'units': 'metric' # Always get metric, we'll convert
}
)
response.raise_for_status()
data = response.json()
# Convert temperature if needed
temp = data['main']['temp']
if self.temperature_unit == 'Fahrenheit':
temp = (temp * 9/5) + 32
return {
'location': data['name'],
'temperature': round(temp, 1),
'unit': self.temperature_unit,
'condition': data['weather'][0]['main'],
'description': data['weather'][0]['description'],
'humidity': data['main']['humidity'],
'wind_speed': data['wind']['speed'],
'feels_like': round((data['main']['feels_like'] * 9/5 + 32 if self.temperature_unit == 'Fahrenheit' else data['main']['feels_like']), 1)
}
except requests.RequestException as e:
return {'error': f'Failed to fetch weather: {str(e)}'}
def get_forecast(self, location: Optional[str] = None, days: int = 7) -> Dict:
"""Get weather forecast"""
location = location or self.default_location
try:
response = requests.get(
f"{self.base_url}/forecast",
params={
'q': location,
'appid': self.api_key,
'cnt': days * 8, # 8 forecasts per day (3-hourly)
'units': 'metric'
}
)
response.raise_for_status()
data = response.json()
forecasts = []
for item in data['list'][:days * 8]: # 7 days
temp = item['main']['temp']
if self.temperature_unit == 'Fahrenheit':
temp = (temp * 9/5) + 32
forecasts.append({
'datetime': item['dt_txt'],
'temperature': round(temp, 1),
'condition': item['weather'][0]['main'],
'precipitation': item.get('rain', {}).get('3h', 0),
'humidity': item['main']['humidity']
})
return {'location': location, 'forecasts': forecasts}
except requests.RequestException as e:
return {'error': f'Failed to fetch forecast: {str(e)}'}
def get_alerts(self, location: Optional[str] = None) -> List[Dict]:
"""Get severe weather alerts"""
location = location or self.default_location
try:
# Get coordinates first
geo_response = requests.get(
"https://api.openweathermap.org/geo/1.0/direct",
params={'q': location, 'appid': self.api_key}
)
if not geo_response.json():
return []
lat, lon = geo_response.json()[0]['lat'], geo_response.json()[0]['lon']
# Get alerts
alerts_response = requests.get(
f"{self.base_url}/weather",
params={
'lat': lat,
'lon': lon,
'appid': self.api_key
}
)
data = alerts_response.json()
alerts = data.get('alerts', [])
# Filter by threshold
filtered_alerts = []
for alert in alerts:
severity = alert.get('event', 'Unknown').lower()
if self.alert_threshold == 'All' or 'severe' in severity or 'extreme' in severity:
filtered_alerts.append({
'event': alert['event'],
'description': alert['description'],
'start': datetime.fromtimestamp(alert['start']).isoformat(),
'end': datetime.fromtimestamp(alert['end']).isoformat()
})
return filtered_alerts
except requests.RequestException as e:
return [{'error': f'Failed to fetch alerts: {str(e)}'}]
# Required interface methods for OpenClaw
def process_command(self, command: str, context: Dict) -> str:
"""Process natural language command"""
command_lower = command.lower()
location = context.get('location')
if 'current' in command_lower or 'right now' in command_lower:
result = self.get_current_weather(location)
if 'error' in result:
return result['error']
return (f"Current weather in {result['location']}: "
f"{result['temperature']}ยฐ{result['unit'][0]}, "
f"{result['condition']} ({result['description']}). "
f"Humidity: {result['humidity']}%")
elif 'forecast' in command_lower or '7-day' in command_lower:
result = self.get_forecast(location)
if 'error' in result:
return result['error']
return f"7-day forecast ready for {result['location']}"
elif 'alert' in command_lower:
alerts = self.get_alerts(location)
if not alerts:
return "No severe weather alerts"
return f"Found {len(alerts)} alert(s): " + "; ".join([a['event'] for a in alerts])
else:
# Default to current weather
return self.process_command("current weather", context)
def get_schema(self) -> Dict:
"""Return skill schema for OpenClaw"""
return {
'name': 'Weather Reporter',
'actions': [
{'name': 'get_current_weather', 'parameters': ['location']},
{'name': 'get_forecast', 'parameters': ['location', 'days']},
{'name': 'get_alerts', 'parameters': ['location']}
]
}
OpenClaw will instantiate and call this class
def create_skill(config: Dict) -> WeatherReporterSkill:
return WeatherReporterSkill(config)
Step 5: Define Dependencies
Create requirements.txt with your skill's Python dependencies:
requests>=2.28.0
pytz>=2023.3
Step 6: Write Unit Tests
Create comprehensive tests to ensure your skill works:
# tests/test_weather.py
import pytest
from main import WeatherReporterSkill
@pytest.fixture
def weather_skill():
config = {
'api_key': 'test_key_12345678901234567890',
'default_location': 'San Francisco, CA',
'temperature_unit': 'Fahrenheit',
'alert_threshold': 'Severe+'
}
return WeatherReporterSkill(config)
def test_skill_initialization(weather_skill):
assert weather_skill.default_location == 'San Francisco, CA'
assert weather_skill.temperature_unit == 'Fahrenheit'
def test_process_current_weather_command(weather_skill):
# Mock would be needed for actual testing
result = weather_skill.process_command("What's the current weather?", {})
assert result is not None
assert isinstance(result, str)
def test_schema(weather_skill):
schema = weather_skill.get_schema()
assert 'name' in schema
assert 'actions' in schema
assert len(schema['actions']) > 0
Run tests:
pytest tests/
Step 7: Create Examples and Documentation
Create config.example.json showing how to configure:
{
"api_key": "YOUR_OPENWEATHERMAP_API_KEY_HERE",
"default_location": "San Francisco, CA",
"temperature_unit": "Fahrenheit",
"alert_threshold": "Severe+"
}
Create README.md for users:
# Weather Reporter Skill
Get real-time weather, forecasts, and alerts in OpenClaw.
Installation
From OpenClaw dashboard: Settings > Skills Marketplace > Search "Weather Reporter" > Install
Configuration
- Get a free API key: https://openweathermap.org/api
- In OpenClaw: Settings > Skills > Weather Reporter > Configure
- Enter your API key and default location
- Click Test Connection
- Done!
Usage
- "What's the weather today?"
- "Show me the 7-day forecast"
- "Any severe weather alerts?"
- "Weather in New York"
Troubleshooting
"API key invalid": Check your key at openweathermap.org
"Location not found": Use "City, Country" format (e.g., "Paris, France")
No alerts: Severe weather alerts only show for current conditions. Check your location.
Step 8: Prepare for Publication
Initialize git and prepare for ClawHub:
git init
git add .
git commit -m "Initial commit: Weather Reporter Skill v1.0.0"
git remote add origin https://github.com/yourusername/openclaw-weather-reporter
git push -u origin main
Create .gitignore:
venv/
__pycache__/
*.pyc
.env
config.json
.pytest_cache/
build/
dist/
*.egg-info/
Step 9: Test Locally
Before publishing, test your skill in OpenClaw:
- Copy skill folder to OpenClaw:
cp -r openclaw-weather-reporter /path/to/openclaw/skills/ - Restart OpenClaw:
docker-compose restart - Go to Skills > Installed
- Configure and test your skill
- Verify it works with various commands
Step 10: Publish to ClawHub
Once tested and working:
- Go to ClawHub.io (OpenClaw Skills Marketplace)
- Click "Publish Skill"
- Upload your skill repository
- Fill in metadata
- Verify preview
- Submit for review
ClawHub team reviews for quality and security within 48 hours. Once approved, users can install your skill from the marketplace!
Best Practices
Code Quality
- Use type hints for all functions
- Write docstrings for all classes and methods
- Follow PEP 8 style guide
- Aim for >80% test coverage
Security
- Never hard-code API keys
- Validate all user input
- Use HTTPS for external API calls
- Don't log sensitive data
Performance
- Cache API responses when possible
- Use async/await for slow operations
- Implement request timeouts
- Handle rate limits gracefully
User Experience
- Clear, helpful error messages
- Sensible defaults
- Rich configuration options
- Detailed documentation
Skill Versioning
When updating your skill:
- Update version in SKILL.md:
version: 1.1.0 - Update CHANGELOG in SKILL.md
- Push to GitHub:
git tag v1.1.0 && git push --tags - Submit update to ClawHub
- Users receive update notification
Frequently Asked Questions
Q: Can my skill use external APIs?A: Yes, as long as you handle API errors gracefully and notify users about rate limits.
Q: How do I handle user authentication?A: Store tokens securely in the skill config. OpenClaw encrypts sensitive config values.
Q: Can I charge money for my skill?A: Yes. Set pricing in ClawHub. Free and paid skills are both supported.
Q: What languages can skills be written in?A: Currently Python. Other languages (Node.js, Go) coming soon.
Q: How do I update my skill after publishing?A: Push new version to GitHub, then submit update on ClawHub. Users get auto-updates.
Next Steps
You've built your first skill! Next steps:
- Publish your skill to ClawHub
- Build multi-agent systems using your custom skills
- Security hardening for production skills
- Join the OpenClaw community Slack for support and showcase your skill!