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
- OpenClaw installed and running
- Basic knowledge of JavaScript/TypeScript or Python
- A text editor or IDE
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
| Concept | Description |
|---|
SkillContext | Contains the user's message, extracted parameters, config, and memory |
|---|
SkillResult | The 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.config | Access skill configuration values |
|---|
context.memory | Access 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
| Error | Cause | Fix |
|---|
| "Skill not found" | Not in the skills directory | Check path: ~/.openclaw/skills/my-first-skill/ |
|---|
| "Manifest invalid" | YAML syntax error | Validate: openclaw skills validate my-first-skill |
|---|
| "Permission denied" | Missing permission in manifest | Add required permission to manifest.yaml |
|---|
| "Timeout" | Skill takes too long | Increase runtime.timeout or optimize logic |
|---|
- Understand the file structure: Skill File Structure: Organizing an OpenClaw Skill.
- Deploy to production: Deploying a Custom OpenClaw Skill: Best Practices.
- Publish to the community: Publishing Your Skill to the Community Skill Registry.
Related Articles
- Deploying a Custom OpenClaw Skill: Best Practices — Learn deployment strategies and best practices for shipping reliable OpenClaw skills to production.
- Creating a News Briefing Skill for OpenClaw — Build a custom skill that delivers personalized news briefings through your OpenClaw agent.
- What is OpenClaw and how does it work? — A beginner-friendly overview of OpenClaw, its architecture, and how it turns AI models into autonomous agents.