Skill File Structure: Organizing an OpenClaw Skill
Clawpedia · For Humans
Understand the standard file and folder structure for well-organized, maintainable OpenClaw skills.
Overview
A well-organized skill is easier to maintain, test, and share. This article explains the recommended file structure for OpenClaw skills and the purpose of each file. Whether you're building a simple API wrapper or a complex multi-step automation, this structure scales with your needs.
Standard Skill Structure
A complete OpenClaw skill looks like this:
my-skill/
├── manifest.yaml # Skill metadata and configuration
├── index.ts # Main entry point
├── lib/ # Internal modules
│ ├── api.ts # API client
│ ├── formatter.ts # Output formatting
│ └── validator.ts # Input validation
├── test/ # Test files
│ ├── index.test.ts # Main tests
│ ├── api.test.ts # API client tests
│ └── fixtures/ # Test data
│ └── sample.json
├── assets/ # Static assets
│ └── icon.png # Skill icon for ClawHub
├── README.md # Documentation
├── CHANGELOG.md # Version history
├── LICENSE # License file
├── package.json # Dependencies (TypeScript)
└── .gitignore # Git ignore rules
Minimal skill (just the essentials):
my-skill/
├── manifest.yaml
└── index.ts
File-by-File Explanation
manifest.yaml — The Skill Identity
This is the most important file. It tells OpenClaw everything about your skill:
# Required fields
name: my-skill # Unique identifier (lowercase, hyphens)
version: 1.0.0 # SemVer version
description: "Brief description" # What does this skill do?
# Authorship
author: "Your Name"
license: MIT
homepage: https://github.com/you/my-skill
# Trigger configuration — when should this skill activate?
triggers:
- pattern: "define *"
confidence: 0.9 # How strongly this pattern matches
examples: # Training examples for the matcher
- "Define serendipity"
- "What does ephemeral mean?"
- "Meaning of ubiquitous"
- "Look up the word cryptic"
# Permissions — what access does this skill need?
permissions:
network: true # Can make HTTP requests
filesystem: none # No file access
memory: read # Can read agent memory
execute: none # Cannot run system commands
# User-configurable options
config:
api_key:
type: string
required: true
secret: true # Masked in output
description: "API key for the dictionary service"
language:
type: string
default: "en"
options: [en, de, fr, es]
description: "Language for definitions"
max_definitions:
type: number
default: 3
min: 1
max: 10
description: "Maximum definitions to show"
# Runtime settings
runtime:
timeout: 10000 # Max execution time (ms)
retries: 2 # Auto-retry on failure
retry_delay: 1000 # Delay between retries (ms)
rate_limit:
max_calls: 60
per_seconds: 60
# OpenClaw compatibility
engine: ">=1.5.0"
# Tags for ClawHub discovery
tags:
- dictionary
- language
- reference
index.ts — The Entry Point
The main file that OpenClaw loads. It must export a class extending Skill:
import { Skill, SkillContext, SkillResult } from "@openclaw/sdk";
import { DictionaryAPI } from "./lib/api";
import { formatDefinition } from "./lib/formatter";
import { validateWord } from "./lib/validator";
export default class DictionarySkill extends Skill {
private api: DictionaryAPI;
// Called once when the skill is loaded
async onLoad(): Promise<void> {
this.api = new DictionaryAPI(this.config.get("api_key"));
this.log.info("Dictionary skill loaded");
}
// Called for each matching request
async execute(context: SkillContext): Promise<SkillResult> {
const word = context.extractParam("word");
const validation = validateWord(word);
if (!validation.valid) {
return this.error(validation.message);
}
const language = this.config.get("language");
const maxDefs = this.config.get("max_definitions");
const definition = await this.api.lookup(word, language);
const formatted = formatDefinition(definition, maxDefs);
return this.success(formatted);
}
// Called when the skill is unloaded
async onUnload(): Promise<void> {
this.log.info("Dictionary skill unloaded");
}
// Optional: health check
async healthCheck(): Promise<boolean> {
return this.api.isAvailable();
}
}
Lifecycle Methods
| Method | When Called | Purpose |
|---|
onLoad() | Skill is loaded/reloaded | Initialize resources, validate config |
|---|
execute(context) | Message matches a trigger | Main skill logic |
|---|
onUnload() | Skill is disabled/removed | Clean up resources |
|---|
healthCheck() | openclaw skills health | Verify external dependencies |
|---|
Split complex logic into focused modules:
// lib/api.ts — API client
export class DictionaryAPI {
constructor(private apiKey: string) {}
async lookup(word: string, language: string): Promise<Definition> {
const response = await fetch(
`https://api.dictionary.com/v2/${language}/${word}`,
{ headers: { Authorization: `Bearer ${this.apiKey}` } }
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json();
}
async isAvailable(): Promise<boolean> {
try {
const resp = await fetch("https://api.dictionary.com/health");
return resp.ok;
} catch {
return false;
}
}
}
// lib/formatter.ts — Output formatting
import { Definition } from "./types";
export function formatDefinition(def: Definition, maxDefs: number): string {
let result = `**${def.word}**`;
if (def.phonetic) result += ` (${def.phonetic})`;
result += "\n\n";
for (const meaning of def.meanings) {
result += `*${meaning.partOfSpeech}*\n`;
for (const d of meaning.definitions.slice(0, maxDefs)) {
result += `- ${d.definition}\n`;
}
result += "\n";
}
return result;
}
// lib/validator.ts — Input validation
export function validateWord(word: string | undefined) {
if (!word || word.trim().length === 0) {
return { valid: false, message: "Please specify a word to define." };
}
if (word.length > 100) {
return { valid: false, message: "Word is too long (max 100 characters)." };
}
if (!/^[a-zA-Z\s-]+$/.test(word)) {
return { valid: false, message: "Word contains invalid characters." };
}
return { valid: true, message: "" };
}
test/ — Test Files
test/
├── index.test.ts # Integration tests for the main skill
├── api.test.ts # Unit tests for the API client
├── formatter.test.ts # Unit tests for the formatter
├── validator.test.ts # Unit tests for the validator
└── fixtures/ # Sample data for tests
├── sample-response.json
└── error-response.json
README.md — Documentation
Every skill should have clear documentation:
# Dictionary Skill
Look up word definitions through your OpenClaw agent.
## Installation
openclaw skills install dictionary
## Configuration
| Key | Required | Default | Description |
|-----|----------|---------|-------------|
| `api_key` | Yes | — | Dictionary API key |
| `language` | No | `en` | Language code |
| `max_definitions` | No | `3` | Max definitions shown |
## Usage Examples
- "Define serendipity"
- "What does ephemeral mean?"
- "Look up cryptic"
## License
MIT
package.json — Dependencies
{
"name": "@openclaw-skills/dictionary",
"version": "1.0.0",
"private": true,
"dependencies": {
"@openclaw/sdk": "^1.5.0"
},
"devDependencies": {
"@openclaw/test": "^1.5.0",
"typescript": "^5.3.0"
}
}
Organizing Complex Skills
For skills with many features, use a modular structure:
complex-skill/
├── manifest.yaml
├── index.ts
├── commands/ # Sub-commands
│ ├── search.ts
│ ├── create.ts
│ └── delete.ts
├── lib/
│ ├── api/
│ │ ├── client.ts
│ │ └── types.ts
│ ├── cache.ts
│ └── utils.ts
├── templates/ # Response templates
│ ├── result.md
│ └── error.md
└── test/
└── ...
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Better Approach |
|---|
Everything in index.ts | Hard to maintain | Split into lib/ modules |
|---|
| Hardcoded API keys | Security risk | Use config in manifest |
|---|
| No error handling | Crashes break the agent | Always return this.error() |
|---|
| No tests | Regressions go unnoticed | Write tests from day one |
|---|
No .gitignore | Secrets in version control | Ignore node_modules, .env |
|---|
| Giant manifest | Hard to read | Keep minimal, document in README |
|---|
- Write your first skill: Writing Your First Custom Skill for OpenClaw.
- Deploy to production: Deploying a Custom OpenClaw Skill: Best Practices.
- Test systematically: Testing and Debugging OpenClaw Skills.
Related Articles
- Deploying a Custom OpenClaw Skill: Best Practices — Learn deployment strategies and best practices for shipping reliable OpenClaw skills to production.
- Skill Dependencies: Managing Libraries and APIs — Handle external libraries, API keys, and third-party dependencies in your OpenClaw skills effectively.
- Finding and Installing OpenClaw Skills from ClawHub — Browse, evaluate, and install community-built skills from the ClawHub skill registry.
- Resolving Conflicts Between OpenClaw Skills — Fix skill conflicts and priority issues when multiple OpenClaw skills compete for the same triggers.
- How can I customize OpenClaw skills without modifying the code repository? — Override and customize skill behavior using configuration files without touching the OpenClaw source code.