Build OpenClaw skills — reusable tools your assistant can call
How to write, test and ship OpenClaw skills: typed tool calls, shell commands, HTTP APIs and the agent-skills registry.
A skill in OpenClaw is a small, typed tool the assistant can call. Skills are the difference between a chat toy and an actual agent that can do things — fetch a calendar event, open a PR, send a message, run a script. This tutorial shows you how to write, test and ship your own.
What a skill looks like
A skill is a folder with two files:
my-skill/
├── skill.yaml # name, description, parameters
└── handler.ts # the TypeScript that runs when called
The skill.yaml is the contract — the LLM reads it to decide when to call your skill. The handler.ts is the implementation.
A minimal skill
Create a folder:
mkdir -p ~/.openclaw/skills/greet
cd ~/.openclaw/skills/greet
Write skill.yaml:
name: greet
description: |
Say hello to a person by name in a chosen language.
Use when the user wants to send a quick greeting
(e.g. "say hi to Anna in German").
parameters:
type: object
required: [name, language]
properties:
name:
type: string
description: Person's first name
language:
type: string
enum: [en, de, zh, ja]
Write handler.ts:
import type { SkillContext } from "openclaw/sdk";
const greetings: Record = {
en: "Hello, {name}!",
de: "Hallo, {name}!",
zh: "{name},你好!",
ja: "こんにちは、{name}さん!",
};
export default async function greet(
input: { name: string; language: string },
ctx: SkillContext
) {
const template = greetings[input.language] ?? greetings.en;
const message = template.replace("{name}", input.name);
ctx.log.info(`greet → ${message}`);
return { message };
}
Reload the Gateway to pick it up:
openclaw skills reload greet
Now message the assistant:
say hi to Anna in German
It should call your greet skill and reply with Hallo, Anna!.
Calling HTTP APIs
Skills are a great fit for wrapping third-party APIs. Here is a weather skill:
name: weather
description: |
Look up the current weather for a city. Use when the user
asks "what's the weather in ".
parameters:
type: object
required: [city]
properties:
city: { type: string }
export default async function weather(
{ city }: { city: string },
ctx: SkillContext
) {
const url = `https://wttr.in/${encodeURIComponent(city)}?format=j1`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`weather: ${res.status} ${res.statusText}`);
}
const data = await res.json();
const cur = data.current_condition[0];
return {
city,
temperature_c: Number(cur.temp_C),
feels_like_c: Number(cur.FeelsLikeC),
description: cur.weatherDesc[0].value,
};
}
The Gateway automatically retries transient errors, surfaces typed exceptions to the LLM and includes the return value in the assistant's reply context.
Calling shell commands
For skills that need to touch the local filesystem or run a binary, use the sandboxed shell:
import { sh } from "openclaw/shell";
export default async function listDownloads(_: unknown, ctx: SkillContext) {
const out = await sh("ls -lh ~/Downloads", { cwd: process.env.HOME });
return { files: out.stdout };
}
The shell runs inside a filesystem sandbox. By default it can read ~/Documents, ~/Downloads, ~/Desktop and ~/.openclaw. To grant access to a custom path, opt in explicitly:
const out = await sh("git status", { cwd: "~/Code/myapp", allow: ["~/Code/myapp"] });
If the assistant tries to read outside the allowed roots, the call fails with a clear error rather than silently leaking data.
Sharing skills with the team
OpenClaw ships an agent-skills repository of canonical skills under github.com/openclaw/agent-skills. If you have a skill that other people will find useful, the workflow is:
openclaw skills publish greet
This:
- Bundles the folder into a tarball.
- Computes a content hash.
- Pushes it to ClawHub (the community skill registry at clawhub.ai).
- Returns a permanent URL you can paste in chat:
https://clawhub.ai/skills/greet@1.0.2
To install someone else's skill:
openclaw skills install https://clawhub.ai/skills/weather@1.4.0
Composing skills with Lobster
Lobster is the OpenClaw-native workflow shell. It composes skills and CLI tools into typed, local-first pipelines. A simple Lobster recipe that combines skills:
# ~/.openclaw/lobster/morning-briefing.yaml
name: morning-briefing
steps:
- skill: weather
args: { city: "Vienna" }
- skill: calendar.today
- skill: inbox.unread
args: { limit: 5 }
Run it on a schedule:
openclaw cron add \\
--name "morning briefing" \\
--at "0 7 * * *" \\
--lobster ~/.openclaw/lobster/morning-briefing.yaml
Every morning at 7 a.m. the assistant will fetch the weather, your calendar and your unread emails, and send you a single Telegram message summarising them.
Testing skills
The SDK ships a tiny test harness:
import greet from "./handler";
import { test } from "openclaw/test";
test("greet returns German template", async ({ run }) => {
const out = await run(greet, { name: "Anna", language: "de" });
expect(out.message).toBe("Hallo, Anna!");
});
openclaw skills test greet
Next steps
- Persist context across sessions with long-term memory.
- See how skills combine with channels in the Integrations tutorial.
- When something stops working, head to Troubleshooting.
Found an issue or have a correction? Get in touch.