Looking for an end-to-end tutorial on how to write a custom OpenClaw skill from scratch? This post walks through the exact file structure, YAML front-matter, natural-language prompts, tool wiring, local testing, and publishing. We’ll build a practical weather-checker skill you can drop into any OpenClaw agent running on your laptop or on ClawCloud.
Why write a skill instead of hard-coding logic?
Skills are self-contained capability packets. They can be loaded, unloaded, and versioned without touching your agent’s core prompt. You’ll want a skill when:
- You need reusable functionality (e.g. fetch weather, parse RSS, post to Slack).
- Different agents share the same capability set.
- You plan to publish the skill so the community can extend it.
Under the hood, a skill is just a folder with a SKILL.md manifesto and optionally some JavaScript/TypeScript helpers. Because OpenClaw is written in Node 22+, you can keep everything in one repo and import NPM dependencies freely.
Skill project scaffold
Create a new directory and initialise an NPM project:
$ mkdir openclaw-skill-weather
$ cd openclaw-skill-weather
$ npm init -y # Node 22+ required
You should now have:
package.jsonSKILL.md(we’ll create this next)src/(optional helper code)
Inside SKILL.md: YAML front-matter + plain English
OpenClaw parses SKILL.md exactly once at load time. The file has two parts:
- YAML front-matter (
--- ... ---), machine-readable. - Natural-language instructions (markdown body), large-language-model-readable.
OpenClaw merges the YAML into its registry and injects the markdown body into the agent prompt when the skill is active. Keep the YAML concise and the prose focused.
Minimal YAML schema
---
name: weather-checker
version: 0.1.0
license: MIT
author: "Jane Dev "
description: "Return current weather for a given city using Open-Meteo API."
tools:
- id: getWeather
runtime: node
entry: src/getWeather.js
timeout: 8000 # ms
description: "Fetch temperature and conditions for a city."
---
Key fields you’ll almost always want:
name&version: semantic versioning. The gateway UI surfaces these.tools: array of callable functions. Each tool needs anid,runtime(alwaysnodefor now), anentryfile, and a short human-readabledescription. Optionaltimeout,memory,scheduleetc. follow the daemon schema.
Natural-language instructions
Under the closing ---, write plain instructions in the voice you want the agent to use. Example:
### Weather Checker Skill
When a user asks about the weather, call getWeather with the requested city. Return the temperature in Celsius and a short condition summary (e.g. "12 °C, light rain"). If the city isn’t recognised, politely ask for clarification.
Examples:
- User: "weather paris" → call getWeather({ city: "Paris" })
- User: "temp ny" → call getWeather({ city: "New York" })
That’s it. No fancy prompt engineering. Keep examples succinct; the gateway automatically appends tool signature hints so the LLM can fill JSON correctly.
Implementing the tool in JavaScript
Create src/getWeather.js:
import fetch from "node-fetch";
/**
* getWeather
* @param {Object} input - { city: string }
* @returns {Promise
Dependencies:
$ npm install node-fetch@3
Because node-fetch is ESM-only, ensure your package.json contains "type": "module".
Local test loop
Before wiring to an agent, run the tool directly:
$ node src/getWeather.js <<'JSON'
{"city": "Berlin"}
JSON
Expect output similar to:
{
"city": "Berlin",
"temperature": "13 °C",
"condition": "clear"
}
If everything works, add the skill to your local OpenClaw gateway.
Linking a skill folder into the gateway
$ clawctl skill link ./openclaw-skill-weather
Restart the daemon or hit reload in the UI. Ask your agent:
User: What's the weather in Tokyo?
You should see the tool call fire, then the agent’s answer.
Writing automated tests
Skills are code, so test them. I like Vitest because it runs ESM without hoops:
$ npm install -D vitest@1
$ npx vitest --init
Create tests/getWeather.test.js:
import getWeather from "../src/getWeather.js";
import { expect, test, vi } from "vitest";
// Mock network calls for speed and determinism
vi.stubGlobal("fetch", async (url) => {
if (url.includes("nominatim")) return {
json: async () => [{ lat: 52.52, lon: 13.405, display_name: "Berlin" }]
};
return {
json: async () => ({ current_weather: { temperature: 42, weathercode: 0 } })
};
});
test("returns structured weather", async () => {
const result = await getWeather({ city: "Berlin" });
expect(result.temperature).toBe("42 °C");
});
Run:
$ npx vitest run
Publishing your skill
There are two ways to share a skill:
- NPM package (recommended). Anyone can
npm installthenclawctl skill link ./node_modules/your-skill. - Git URL. The gateway UI lets you paste a repo URL; it clones at
--depth 1and readsSKILL.md.
Prepare package.json
{
"name": "@jane/openclaw-skill-weather",
"version": "0.1.0",
"description": "OpenClaw skill: check weather via Open-Meteo",
"main": "src/getWeather.js",
"type": "module",
"keywords": ["openclaw", "skill", "weather"],
"peerDependencies": {
"openclaw": ">=2.3.0" // adjust to current gateway version
},
"files": ["SKILL.md", "src/"]
}
Login and publish:
$ npm login
$ npm publish --access public
Users can now:
$ npm install @jane/openclaw-skill-weather
$ clawctl skill link node_modules/@jane/openclaw-skill-weather
Versioning & breaking changes
Once people depend on your skill, changing the tool signature is a breaking change. Bump major. Non-breaking tweaks (e.g. better error messages) go into minor. The gateway shows semver diff warnings before it updates.
Going further: scheduled runs & memory
Weather is a request/response skill. For an RSS monitor you’d add:
tools:
- id: pollRSS
runtime: node
entry: src/pollRSS.js
schedule: "*/15 * * * *" # every 15 min
memory: rssMemory # named vector store bucket
The daemon will call pollRSS on schedule and you can write into rssMemory. The agent can later query past posts.
Security checkpoints
- Never store API keys in the code. Expect the operator to set env vars and list them in
docs/env.md. - Validate user input. LLM hallucinations can inject shell-like strings.
- Set realistic
timeoutandmemorylimits in YAML so your skill can’t DoS the host.
OpenClaw uses the Node vm sandbox with network on by default. If you require local file reads, add permissions: ["fs"] under the tool entry.
Takeaway
That’s the full life-cycle: write SKILL.md, implement your tool, test, link locally, publish. The weather checker clocks in at ~80 lines of code and one markdown file. Once you’re comfortable, try chaining skills (weather + calendar) or exposing shell access for power users. Feedback and PRs welcome in the OpenClaw GitHub repo.