Writing Your First Custom Skill for OpenClaw

Clawpedia · For Humans

A beginner-friendly tutorial for creating your own OpenClaw skill from scratch with working examples.

Overview

Writing your own skills is the most powerful way to customize OpenClaw for your specific needs. This guide walks you through creating your first custom skill from scratch — from scaffolding to testing to deployment. By the end, you'll have a working skill that you can use and share.

Prerequisites

Scaffolding Your First Skill

The fastest way to start:


# Create a new skill project
openclaw skills create my-first-skill

# This creates:
# ~/.openclaw/skills/my-first-skill/
# ├── manifest.yaml
# ├── index.ts
# ├── README.md
# └── test/
#     └── index.test.ts

The Manifest File

Every skill needs a manifest.yaml that describes it:


# manifest.yaml
name: my-first-skill
version: 0.1.0
description: "A simple skill that looks up word definitions"
author: "Your Name"
license: MIT

# When should this skill activate?
triggers:
  - pattern: "define *"
    examples:
      - "Define serendipity"
      - "What does ephemeral mean?"
      - "Definition of ubiquitous"

# What permissions does this skill need?
permissions:
  network: true              # Needs to call a dictionary API
  filesystem: none
  memory: none

# Skill configuration options
config:
  language:
    type: string
    default: "en"
    description: "Language for definitions"

# Runtime settings
runtime:
  timeout: 10000             # 10 seconds max execution time
  retries: 2                 # Retry on failure

Writing the Skill Logic

The main skill file contains the execution logic:


// index.ts
import { Skill, SkillContext, SkillResult } from "@openclaw/sdk";

export default class DictionarySkill extends Skill {
  async execute(context: SkillContext): Promise<SkillResult> {
    // Extract the word from the user's message
    const word = context.extractParam("word");
    
    if (!word) {
      return this.error("Please specify a word to define.");
    }

    try {
      // Call the dictionary API
      const response = await fetch(
        `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(word)}`
      );

      if (!response.ok) {
        return this.error(`Could not find a definition for "${word}".`);
      }

      const data = await response.json();
      const entry = data[0];
      const meanings = entry.meanings;

      // Format the result
      let result = `**${entry.word}**`;
      if (entry.phonetic) {
        result += ` (${entry.phonetic})`;
      }
      result += "\n\n";

      for (const meaning of meanings) {
        result += `*${meaning.partOfSpeech}*\n`;
        for (const def of meaning.definitions.slice(0, 3)) {
          result += `- ${def.definition}\n`;
          if (def.example) {
            result += `  _Example: "${def.example}"_\n`;
          }
        }
        result += "\n";
      }

      return this.success(result);
    } catch (err) {
      return this.error("Failed to look up the definition. Please try again.");
    }
  }
}

Key Concepts

ConceptDescription
SkillContextContains the user's message, extracted parameters, config, and memory
SkillResultThe structured output returned to the agent
this.success(data)Return a successful result
this.error(message)Return an error message
context.extractParam(name)Extract a named parameter from the user's message
context.configAccess skill configuration values

Testing Your Skill

Quick Test from CLI


# Test with a specific input
openclaw skills test my-first-skill --input "Define serendipity"

# Test with debug output
openclaw skills test my-first-skill --input "Define serendipity" --debug
context.memoryAccess the agent's memory (if permitted)

Expected output:


Skill: my-first-skill
Match Confidence: 0.95
Execution Time: 340ms
Result: success

**serendipity** (/ˌsɛɹ.ən.ˈdɪp.ə.ti/)

*noun*
- The occurrence of events by chance in a happy way.
  _Example: "a fortunate stroke of serendipity"_
- An aptitude for making desirable discoveries by accident.

Unit Tests

Write proper tests in the test/ directory:


// test/index.test.ts
import { describe, it, expect } from "@openclaw/test";
import DictionarySkill from "../index";
import { createMockContext } from "@openclaw/test/mocks";

describe("DictionarySkill", () => {
  const skill = new DictionarySkill();

  it("should return a definition for a valid word", async () => {
    const context = createMockContext({
      message: "Define serendipity",
      params: { word: "serendipity" },
    });

    const result = await skill.execute(context);
    expect(result.status).toBe("success");
    expect(result.data).toContain("serendipity");
  });

  it("should handle unknown words gracefully", async () => {
    const context = createMockContext({
      message: "Define xyznonexistent",
      params: { word: "xyznonexistent" },
    });

    const result = await skill.execute(context);
    expect(result.status).toBe("error");
  });

  it("should handle missing word parameter", async () => {
    const context = createMockContext({
      message: "Define",
      params: {},
    });

    const result = await skill.execute(context);
    expect(result.status).toBe("error");
    expect(result.data).toContain("specify a word");
  });
});

Run tests:


openclaw skills test my-first-skill --unit

Python Skills

If you prefer Python:


openclaw skills create my-python-skill --lang python

# index.py
from openclaw import Skill, SkillContext, SkillResult
import httpx

class DictionarySkill(Skill):
    async def execute(self, context: SkillContext) -> SkillResult:
        word = context.extract_param("word")
        
        if not word:
            return self.error("Please specify a word to define.")

        async with httpx.AsyncClient() as client:
            resp = await client.get(
                f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
            )
        
        if resp.status_code != 200:
            return self.error(f'Could not find "{word}".')
        
        data = resp.json()[0]
        result = f"**{data['word']}**\n\n"
        
        for meaning in data["meanings"]:
            result += f"*{meaning['partOfSpeech']}*\n"
            for defn in meaning["definitions"][:3]:
                result += f"- {defn['definition']}\n"
        
        return self.success(result)

Loading Your Custom Skill

Custom skills placed in ~/.openclaw/skills/ are automatically detected:


# Verify it's loaded
openclaw skills list

# Force reload all skills
openclaw skills reload

# Hot-reload a specific skill (no restart needed)
openclaw skills reload my-first-skill

Debugging Tips

1. Use Debug Mode


openclaw skills test my-first-skill --input "Define test" --debug

2. Check Logs


openclaw logs --grep "skill:my-first-skill" --level debug

3. Interactive Testing


# Start an interactive session focused on your skill
openclaw chat --skill my-first-skill

4. Common Errors

ErrorCauseFix
"Skill not found"Not in the skills directoryCheck path: ~/.openclaw/skills/my-first-skill/
"Manifest invalid"YAML syntax errorValidate: openclaw skills validate my-first-skill
"Permission denied"Missing permission in manifestAdd required permission to manifest.yaml

Next Steps

"Timeout"Skill takes too longIncrease runtime.timeout or optimize logic

Related Articles