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

MethodWhen CalledPurpose
onLoad()Skill is loaded/reloadedInitialize resources, validate config
execute(context)Message matches a triggerMain skill logic
onUnload()Skill is disabled/removedClean up resources

lib/ — Internal Modules

healthCheck()openclaw skills healthVerify 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-PatternProblemBetter Approach
Everything in index.tsHard to maintainSplit into lib/ modules
Hardcoded API keysSecurity riskUse config in manifest
No error handlingCrashes break the agentAlways return this.error()
No testsRegressions go unnoticedWrite tests from day one
No .gitignoreSecrets in version controlIgnore node_modules, .env

Next Steps

Giant manifestHard to readKeep minimal, document in README

Related Articles