Complete Guide to Building Custom OpenClaw Skills
OpenClaw skills are how you extend agent capabilities. Whether integrating with APIs, adding new tools, or packaging workflows, skills make functionality reusable and shareable. This guide covers everything: structure, best practices, publishing, and real examples.
What Is an OpenClaw Skill?
A skill is a self-contained package that adds functionality to OpenClaw agents. It typically includes:
- Documentation (SKILL.md)
- Scripts (TypeScript, Python, shell)
- Configuration files
- Assets (templates, data files)
- Optional dependencies
Skills live in
~/.openclaw/skills/ or project-specific skills/ directories.
Skill Anatomy
Minimal Skill Structure
my-skill/
├── SKILL.md # Documentation and usage
├── config.json # Optional: metadata
└── scripts/
└── main.ts # Primary script
Complete Skill Structure
my-skill/
├── SKILL.md
├── config.json
├── package.json # For npm dependencies
├── scripts/
│ ├── main.ts
│ ├── helper.ts
│ └── install.sh # Setup script
├── templates/
│ └── default.txt
├── data/
│ └── sample.json
└── README.md # For humans/GitHub
Creating Your First Skill
Step 1: Choose a Name
Good skill names are:
- Lowercase with dashes:
my-api-skill - Descriptive:
weather-forecastnotwf - Unique: check ClawHub first
Step 2: Create Directory
mkdir -p skills/my-first-skill
cd skills/my-first-skill
Step 3: Write SKILL.md
SKILL.md is the entry point. It tells OpenClaw what the skill does and how to use it.
Minimal SKILL.md:
# My First Skill
A simple skill that greets the user.
## Usage
Run the greeting script:
\`\`\`bash
node skills/my-first-skill/scripts/greet.ts
\`\`\`
## Configuration
No configuration needed.
Complete SKILL.md:
# Weather Forecast Skill
Fetch weather forecasts via OpenWeather API.
## Description
Provides current weather and 5-day forecasts for any location.
## Installation
1. Get an API key from openweathermap.org
2. Set environment variable:
\`\`\`bash
export OPENWEATHER_API_KEY=your_key_here
\`\`\`
## Usage
### Get current weather
\`\`\`bash
node skills/weather-forecast/scripts/current.ts "New York"
\`\`\`
### Get 5-day forecast
\`\`\`bash
node skills/weather-forecast/scripts/forecast.ts "London"
\`\`\`
## Configuration
Optional: Set default location in \`config.json\`:
\`\`\`json
{
"defaultLocation": "San Francisco"
}
\`\`\`
## Examples
See \`examples/\` directory for usage patterns.
## Troubleshooting
- **API key invalid**: Verify your key at openweathermap.org
- **Location not found**: Try city name with country code (\"London,UK\")
Step 4: Write Script
scripts/greet.ts:
const name = process.argv[2] || "Agent";
console.log(`Hello, ${name}! Welcome to OpenClaw skills.`);
Test it:
node scripts/greet.ts Optimus
# Output: Hello, Optimus! Welcome to OpenClaw skills.
Step 5: Add config.json (Optional)
{
"name": "my-first-skill",
"version": "1.0.0",
"description": "A simple greeting skill",
"author": "Your Name",
"license": "MIT"
}
Real-World Example: GitHub Issue Sync
Let's build a practical skill that syncs GitHub issues to a local file.
Structure
github-sync/
├── SKILL.md
├── config.json
├── package.json
├── scripts/
│ ├── sync.ts
│ └── auth.ts
└── data/
└── issues.json
SKILL.md
# GitHub Issue Sync
Sync GitHub repository issues to local JSON file.
## Installation
\`\`\`bash
cd skills/github-sync
npm install
\`\`\`
## Configuration
Set GitHub token:
\`\`\`bash
export GITHUB_TOKEN=ghp_your_token_here
\`\`\`
## Usage
\`\`\`bash
node scripts/sync.ts owner/repo
\`\`\`
Issues are saved to \`data/issues.json\`.
package.json
{
"name": "github-sync",
"version": "1.0.0",
"dependencies": {
"@octokit/rest": "^19.0.0"
}
}
scripts/sync.ts
import { Octokit } from "@octokit/rest";
import fs from "fs/promises";
import path from "path";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const repo = process.argv[2];
if (!repo) {
console.error("Usage: node sync.ts owner/repo");
process.exit(1);
}
const [owner, repoName] = repo.split("/");
async function syncIssues() {
const { data: issues } = await octokit.issues.listForRepo({
owner,
repo: repoName,
state: "open"
});
const simplified = issues.map(issue => ({
number: issue.number,
title: issue.title,
state: issue.state,
labels: issue.labels.map(l => l.name),
created_at: issue.created_at
}));
const dataPath = path.join(__dirname, "../data/issues.json");
await fs.writeFile(dataPath, JSON.stringify(simplified, null, 2));
console.log(`Synced ${simplified.length} issues to ${dataPath}`);
}
syncIssues().catch(console.error);
Advanced Patterns
Pattern 1: Multi-Command Skill
Skills with multiple commands:
api-toolkit/
├── SKILL.md
└── scripts/
├── get.ts # GET request
├── post.ts # POST request
├── put.ts # PUT request
└── delete.ts # DELETE request
SKILL.md:
# API Toolkit
## Commands
- \`node scripts/get.ts <url>\`
- \`node scripts/post.ts <url> <data>\`
- \`node scripts/put.ts <url> <data>\`
- \`node scripts/delete.ts <url>\`
Pattern 2: Interactive Setup
Skills that need configuration:
scripts/setup.ts:
import readline from "readline";
import fs from "fs/promises";
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function ask(question: string): Promise<string> {
return new Promise(resolve => {
rl.question(question, resolve);
});
}
async function setup() {
console.log("Setting up My Skill...");
const apiKey = await ask("Enter API key: ");
const endpoint = await ask("Enter API endpoint: ");
const config = {
apiKey,
endpoint,
createdAt: new Date().toISOString()
};
await fs.writeFile(
"./config.json",
JSON.stringify(config, null, 2)
);
console.log("Setup complete!");
rl.close();
}
setup();
Pattern 3: Template-Based Generation
Skills that generate files from templates:
templates/component.tsx:
import React from "react";
interface {{ComponentName}}Props {
// Props here
}
export function {{ComponentName}}(props: {{ComponentName}}Props) {
return (
<div>
{{ComponentName}} component
</div>
);
}
scripts/generate.ts:
import fs from "fs/promises";
import path from "path";
const componentName = process.argv[2];
if (!componentName) {
console.error("Usage: node generate.ts ComponentName");
process.exit(1);
}
const templatePath = path.join(__dirname, "../templates/component.tsx");
let template = await fs.readFile(templatePath, "utf-8");
template = template.replace(/{{ComponentName}}/g, componentName);
const outputPath = `./src/components/${componentName}.tsx`;
await fs.writeFile(outputPath, template);
console.log(`Generated ${outputPath}`);
Publishing to ClawHub
Prepare for Publishing
Package Structure
my-skill/
├── SKILL.md
├── README.md
├── LICENSE
├── config.json
├── package.json
├── scripts/
├── examples/
└── .gitignore
Publish
# Using clawhub CLI (if installed)
clawhub publish ./my-skill
# Or manually
tar -czf my-skill.tar.gz my-skill/
# Upload to clawhub.com
Best Practices
1. Clear Documentation
Bad:
# Thing
Does stuff.
Good:
# Weather API Skill
Fetches weather data from OpenWeather API for any location.
## Prerequisites
- API key from openweathermap.org
- Node.js 18+
## Installation
[step-by-step instructions]
## Usage
[examples with expected output]
## Troubleshooting
[common issues and fixes]
2. Handle Errors Gracefully
try {
const result = await fetchData();
console.log(JSON.stringify(result, null, 2));
} catch (error) {
if (error.code === "ENOTFOUND") {
console.error("Network error: Check your connection");
} else if (error.response?.status === 401) {
console.error("Authentication failed: Check your API key");
} else {
console.error(`Error: ${error.message}`);
}
process.exit(1);
}
3. Use Environment Variables for Secrets
const apiKey = process.env.MY_API_KEY;
if (!apiKey) {
console.error("Error: MY_API_KEY environment variable not set");
console.error("Set it with: export MY_API_KEY=your_key");
process.exit(1);
}
4. Provide Sensible Defaults
const config = {
timeout: process.env.TIMEOUT || 30000,
retries: process.env.RETRIES || 3,
endpoint: process.env.API_ENDPOINT || "https://api.example.com"
};
5. Log Progress for Long Operations
console.log("Fetching data...");
const data = await fetchLargeDataset();
console.log("Processing...");
const processed = await processData(data);
console.log("Saving...");
await saveResults(processed);
console.log("Done!");
Testing Skills
Manual Testing
# Test from skill directory
cd skills/my-skill
node scripts/main.ts test-input
# Test from workspace root
node skills/my-skill/scripts/main.ts test-input
Automated Testing
Add tests:
// scripts/test.ts
import { fetchWeather } from "./weather";
async function test() {
console.log("Testing weather fetch...");
const result = await fetchWeather("London");
if (!result.temperature) {
throw new Error("Missing temperature");
}
if (!result.description) {
throw new Error("Missing description");
}
console.log("✓ All tests passed");
}
test().catch(error => {
console.error("✗ Test failed:", error.message);
process.exit(1);
});
Common Skill Types
API Wrapper
Simplifies API access:
// scripts/api.ts
import fetch from "node-fetch";
const API_KEY = process.env.API_KEY;
const BASE_URL = "https://api.example.com";
export async function get(endpoint: string) {
const response = await fetch(`${BASE_URL}${endpoint}`, {
headers: { Authorization: `Bearer ${API_KEY}` }
});
return await response.json();
}
export async function post(endpoint: string, data: any) {
const response = await fetch(`${BASE_URL}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`
},
body: JSON.stringify(data)
});
return await response.json();
}
Data Transformer
Converts data between formats:
// scripts/transform.ts
import fs from "fs/promises";
import yaml from "yaml";
const input = await fs.readFile(process.argv[2], "utf-8");
const data = JSON.parse(input);
const yamlOutput = yaml.stringify(data);
await fs.writeFile(process.argv[3], yamlOutput);
console.log("Converted JSON to YAML");
Workflow Automation
Chains multiple operations:
// scripts/workflow.ts
import { fetchData } from "./fetch";
import { processData } from "./process";
import { saveResults } from "./save";
import { sendNotification } from "./notify";
async function runWorkflow() {
const data = await fetchData();
const processed = await processData(data);
await saveResults(processed);
await sendNotification("Workflow complete");
}
runWorkflow().catch(console.error);
Wrapping Up
Building OpenClaw skills is straightforward. Start with a SKILL.md and a script. Test it, refine it, then share it on ClawHub.
The best skills solve real problems. They have clear docs, handle errors gracefully, and work out of the box. Your skill could be the one that unlocks new capabilities for agents everywhere.
Start building. The ecosystem needs your contribution.