tutorials

Building Custom OpenClaw Skills: From Idea to Implementation

LearnClub AI
February 28, 2026
6 min read

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): Description
  • param2 (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

  1. Update version:
npm version patch  # or minor/major
  1. Update documentation:
  • Review SKILL.md
  • Add changelog
  • Update examples
  1. 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


Build your own skills and share with the community. More developer guides available.

Share this article