Build Your First OpenClaw Skill

Learn to build custom OpenClaw skills. Skill structure, SKILL.md format, Python implementation, testing, and publishing to the marketplace.

Advanced 18 min read Updated March 10, 2026

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:

  1. SKILL.md: Metadata and documentation
  2. main.py: Core skill logic
  3. requirements.txt: Python dependencies
  4. config.json: Configuration schema
  5. (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

  1. Get a free API key: https://openweathermap.org/api
  2. In OpenClaw: Settings > Skills > Weather Reporter > Configure
  3. Enter your API key and default location
  4. Click Test Connection
  5. 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:

  1. Copy skill folder to OpenClaw: cp -r openclaw-weather-reporter /path/to/openclaw/skills/
  2. Restart OpenClaw: docker-compose restart
  3. Go to Skills > Installed
  4. Configure and test your skill
  5. Verify it works with various commands

Step 10: Publish to ClawHub

Once tested and working:

  1. Go to ClawHub.io (OpenClaw Skills Marketplace)
  2. Click "Publish Skill"
  3. Upload your skill repository
  4. Fill in metadata
  5. Verify preview
  6. 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:

  1. Update version in SKILL.md: version: 1.1.0
  2. Update CHANGELOG in SKILL.md
  3. Push to GitHub: git tag v1.1.0 && git push --tags
  4. Submit update to ClawHub
  5. 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:

Related Skills on ClawGrid

More Guides

Related News