Building Custom OpenClaw Skills: From Idea to Implementation
While OpenClaw has many built-in skills, creating custom skills lets you extend functionality for your specific needs. This guide walks you through building a skill from scratch.
Skill Architecture
Basic Structure
my-skill/
βββ SKILL.md # Documentation
βββ config.yaml # Configuration schema
βββ index.js # Entry point
βββ package.json # Dependencies
βββ tests/ # Test files
βββ index.test.js
Skill Lifecycle
Load β Configure β Execute β Output β Cleanup
Step 1: Plan Your Skill
What Makes a Good Skill?
β Single Purpose: Does one thing well β Reusable: Solves a common problem β Configurable: Adapts to different needs β Documented: Clear usage instructions β Tested: Reliable and predictable
Skill Ideas
- Database connector for your specific DB
- Internal API integration
- Custom formatter for your data
- Business logic automation
- Proprietary tool integration
Step 2: Initialize the Skill
Using CLI
# Create new skill from template
openclaw skill create my-skill --template basic
# Or manually
mkdir my-skill
cd my-skill
npm init -y
Basic Files
package.json:
{
"name": "openclaw-skill-my-skill",
"version": "1.0.0",
"description": "My custom OpenClaw skill",
"main": "index.js",
"scripts": {
"test": "jest"
},
"dependencies": {
"@openclaw/skill-sdk": "^1.0.0"
}
}
SKILL.md:
# my-skill
Brief description of what this skill does.
## Configuration
```yaml
my-skill:
api_key: "your-api-key"
endpoint: "https://api.example.com"
Usage
openclaw skill run my-skill --param value
Actions
action-name
Description of the action.
Parameters:
param1(required): Descriptionparam2(optional): Description
## Step 3: Implement the Skill
### Basic Skill Template
**index.js:**
```javascript
const { Skill } = require('@openclaw/skill-sdk');
class MySkill extends Skill {
constructor(config) {
super(config);
this.name = 'my-skill';
this.version = '1.0.0';
}
async validateConfig() {
// Validate configuration
if (!this.config.api_key) {
throw new Error('API key is required');
}
}
async execute(action, params) {
switch (action) {
case 'fetch':
return this.fetchData(params);
case 'process':
return this.processData(params);
default:
throw new Error(`Unknown action: ${action}`);
}
}
async fetchData(params) {
// Implementation
const response = await fetch(
`${this.config.endpoint}/data`,
{
headers: {
'Authorization': `Bearer ${this.config.api_key}`
}
}
);
return response.json();
}
async processData(params) {
// Process the data
return {
processed: true,
data: params.data
};
}
}
module.exports = MySkill;
Advanced Features
Input Validation:
const { validate } = require('@openclaw/skill-sdk/validation');
const schema = {
url: { type: 'string', required: true, url: true },
timeout: { type: 'number', default: 5000 },
retries: { type: 'number', default: 3 }
};
async execute(action, params) {
validate(params, schema);
// ... rest of implementation
}
Error Handling:
async fetchData(params) {
try {
const response = await fetch(params.url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
this.logger.error('Failed to fetch data', error);
throw new SkillError('FETCH_FAILED', error.message);
}
}
Progress Reporting:
async processLargeDataset(params) {
const items = params.items;
const results = [];
for (let i = 0; i < items.length; i++) {
// Report progress
this.progress({
current: i + 1,
total: items.length,
percent: Math.round(((i + 1) / items.length) * 100)
});
const result = await this.processItem(items[i]);
results.push(result);
}
return results;
}
Step 4: Configuration
config.yaml:
my-skill:
# Required
api_key:
type: string
required: true
secret: true
description: "API key for the service"
# Optional with default
endpoint:
type: string
default: "https://api.example.com"
description: "API endpoint URL"
timeout:
type: number
default: 5000
min: 1000
max: 30000
description: "Request timeout in milliseconds"
retries:
type: number
default: 3
min: 0
max: 10
description: "Number of retry attempts"
Step 5: Testing
Unit Tests
tests/index.test.js:
const MySkill = require('../index');
describe('MySkill', () => {
let skill;
beforeEach(() => {
skill = new MySkill({
api_key: 'test-key',
endpoint: 'https://test.api.com'
});
});
test('validates config', async () => {
await expect(skill.validateConfig()).resolves.not.toThrow();
});
test('throws on missing api_key', async () => {
const badSkill = new MySkill({});
await expect(badSkill.validateConfig()).rejects.toThrow('API key');
});
test('executes fetch action', async () => {
const result = await skill.execute('fetch', { id: '123' });
expect(result).toBeDefined();
});
});
Integration Tests
describe('Integration', () => {
test('end-to-end workflow', async () => {
const skill = new MySkill({
api_key: process.env.TEST_API_KEY
});
const result = await skill.execute('fetch', {
url: 'https://httpbin.org/get'
});
expect(result).toHaveProperty('url');
});
});
Step 6: Local Development
Install Dependencies
npm install
Test Locally
# Run tests
npm test
# Test manually
openclaw skill install ./my-skill --local
openclaw skill run my-skill --action fetch --param value
Debug Mode
// Add debug logging
this.logger.debug('Fetching data', { url: params.url });
Step 7: Publishing
Prepare for Release
- Update version:
npm version patch # or minor/major
- Update documentation:
- Review SKILL.md
- Add changelog
- Update examples
- Test thoroughly:
npm test
npm run lint
Publish Options
Option 1: GitHub (Recommended)
# Push to GitHub
git init
git add .
git commit -m "Initial release"
git remote add origin https://github.com/username/openclaw-skill-my-skill.git
git push -u origin main
# Tag release
git tag v1.0.0
git push origin v1.0.0
Option 2: NPM Registry
npm publish --access public
Option 3: Private Registry
npm publish --registry https://your-registry.com
Step 8: Installation
From GitHub
openclaw skill install github:username/my-skill
From NPM
openclaw skill install openclaw-skill-my-skill
From Local
openclaw skill install ./path/to/my-skill
Best Practices
1. Error Handling
// Custom error types
class SkillError extends Error {
constructor(code, message) {
super(message);
this.code = code;
this.isSkillError = true;
}
}
// Usage
throw new SkillError('RATE_LIMITED', 'Too many requests');
2. Logging
// Use built-in logger
this.logger.info('Operation completed', { duration: 123 });
this.logger.warn('Deprecated feature used');
this.logger.error('Operation failed', error);
3. Caching
const { cache } = require('@openclaw/skill-sdk/cache');
async fetchData(params) {
const cacheKey = `data:${params.id}`;
return cache.getOrSet(cacheKey, async () => {
const response = await fetch(params.url);
return response.json();
}, {
ttl: 3600 // 1 hour
});
}
4. Rate Limiting
const { RateLimiter } = require('@openclaw/skill-sdk/rate-limit');
const limiter = new RateLimiter({
maxRequests: 100,
windowMs: 60000 // 1 minute
});
async execute(action, params) {
await limiter.acquire();
// ... execute
}
Example: Complete Skill
weather-api-skill/index.js:
const { Skill } = require('@openclaw/skill-sdk');
class WeatherAPISkill extends Skill {
constructor(config) {
super(config);
this.name = 'weather-api';
this.baseUrl = config.endpoint || 'https://api.weather.com';
}
async execute(action, params) {
switch (action) {
case 'current':
return this.getCurrentWeather(params.location);
case 'forecast':
return this.getForecast(params.location, params.days);
default:
throw new Error(`Unknown action: ${action}`);
}
}
async getCurrentWeather(location) {
const response = await fetch(
`${this.baseUrl}/current?location=${encodeURIComponent(location)}`,
{
headers: { 'X-API-Key': this.config.api_key }
}
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.status}`);
}
const data = await response.json();
return {
location: data.location,
temperature: data.temp,
conditions: data.conditions,
humidity: data.humidity,
updated: data.timestamp
};
}
async getForecast(location, days = 5) {
// Implementation
}
}
module.exports = WeatherAPISkill;
Resources
- Skill SDK Docs: docs.openclaw.ai/sdk
- Example Skills: github.com/openclaw/skills
- Community Discord: discord.gg/clawd
Build your own skills and share with the community. More developer guides available.