Intermediate Openclaw 14 min read

Build Custom OpenClaw Skills: From SKILL.md to ClawHub

#openclaw #skills #skill-md #clawhub #publishing #yaml #tutorial

OpenClaw’s skill system lets you extend your AI agent with domain-specific capabilities written entirely in Markdown. No compiled plugin API, no SDK dependencies — just a SKILL.md file that describes what a skill does, what tools it exposes, and under what conditions it should load. This tutorial walks you through authoring your first custom skill from scratch, understanding how the runtime resolves skill conflicts, and publishing your work to ClawHub so the global community of over 13,000 skills can include yours.

By the end of this guide you will have two fully working skills — a stock-price fetcher and a cross-platform weather reporter — and you will know how to gate each skill by operating system and binary dependencies, install community skills with one command, and manage updates across your entire skill library.

What Are OpenClaw Skills?

A skill in OpenClaw is a Markdown document (SKILL.md) that teaches the agent a new capability. The document contains two parts: a YAML frontmatter block that configures how the runtime handles the skill, and a Markdown body that instructs the LLM on how to behave when the skill is active.

This design choice is deliberate. Rather than compiling plugin binaries or wiring into a proprietary hook system, OpenClaw treats skills as structured knowledge. The LLM reads the skill’s description to decide whether to invoke it, then follows the body instructions to decide how. The result is a system where skills are transparent, version-controllable, and writable by anyone who understands Markdown and basic YAML.

Skills differ from raw system-prompt customisation in one important way: they are modular and independently loadable. You can share them across machines, publish them to a registry, and override them per-project without touching any global configuration. That modularity makes skills the primary extension point for serious OpenClaw workflows.

A brief token note before diving in: at session start, OpenClaw snapshots every loaded skill and injects it into the context window. The baseline overhead is roughly 195 characters for the skill loader itself, plus 97 characters per skill. Keep your skill descriptions concise — every word you include costs tokens on every session start.

How Skills Are Loaded: The Priority System

OpenClaw resolves which SKILL.md to use when two or more skills share the same name by walking a six-level priority chain, from highest to lowest:

PriorityLocationScope
1 (highest)<workspace>/skills/This agent only
2<workspace>/.agents/skills/All agents in this project
3~/.agents/skills/All agents on this machine
4~/.openclaw/skills/CLI-managed shared skills
5Bundled skills~50 built-in skills (github, weather, 1password …)
6 (lowest)skills.load.extraDirs in openclaw.jsonExternal directories you configure

The chain means that a skill placed directly inside your workspace always wins over a machine-global skill of the same name. This lets you override a bundled skill for a single project without disturbing anything else.

A concrete example: OpenClaw ships a bundled weather skill. If you drop your own SKILL.md with name: weather into <workspace>/skills/weather/SKILL.md, your version takes priority for that workspace alone. Every other project still sees the bundled version.

When the runtime walks the chain it stops at the first match it finds. If no match exists at any level, the skill is simply absent — no error, no fallback.

Anatomy of a SKILL.md File

Every SKILL.md starts with a YAML frontmatter block delimited by ---. Here is a fully annotated example:

---
# Required fields
name: my-skill
description: "Fetches live stock prices from Yahoo Finance and formats them as a brief summary."

# Invocation control
user-invocable: true        # Allow /my-skill slash command to force-invoke this skill
command-dispatch: tool      # Bypass LLM reasoning; immediately trigger the tool mapping

# Environment guards — skill is skipped if conditions are not met
metadata:
  openclaw:
    os: ["darwin", "linux"]   # Only load on macOS and Linux
    requires:
      bins: ["curl", "jq"]    # Only load if these binaries are on PATH
---

Key fields explained

name — The unique identifier used in the priority chain collision resolution and in the /slash command. Use kebab-case. Once published to ClawHub this name forms the install slug.

description — The most important field. The LLM reads this string to decide whether to invoke the skill in response to a user message. Write it as a single sentence describing what the skill does for the user, not what it is. Bad: "Stock skill". Good: "Fetches a live stock price for a given ticker symbol and returns price, change, and volume.".

user-invocable — When true, the user can type /my-skill to force the skill into the active context regardless of whether the LLM would have chosen it. Defaults to false.

command-dispatch: tool — Bypasses the LLM’s reasoning step entirely and maps the slash command directly to the first tool defined in the skill body. Useful for deterministic utility skills where you never want the model to second-guess the invocation.

metadata.openclaw.os — An array of platform strings (darwin, linux, win32). If the current platform is not in the list, the skill is silently skipped at load time. Omit the field to allow all platforms.

metadata.openclaw.requires.bins — An array of binary names. OpenClaw checks each name against PATH before loading. If any binary is missing, the skill is skipped. This prevents errors at runtime caused by missing system dependencies.

After the frontmatter block, the Markdown body of SKILL.md contains the actual instructions the LLM follows when the skill is active. You define tool call templates, response formatting rules, and step-by-step procedures here using standard Markdown headings and fenced code blocks.

Tutorial: Build a Stock Price Skill

This skill queries the Yahoo Finance unofficial API, parses the JSON response, and formats it as a clean one-liner summary. It requires curl and jq and is intended for macOS and Linux only.

Create the Directory Structure

Skills live in a folder named after the skill’s name field. Place the folder inside whichever level of the priority chain makes sense for your use case. For a personal skill that should be available across all your projects, use ~/.agents/skills/:

mkdir -p ~/.agents/skills/stock-price
touch ~/.agents/skills/stock-price/SKILL.md

If you want the skill limited to a single workspace, use <workspace>/skills/stock-price/ instead.

Write the SKILL.md

Open the file in your editor and paste the following:

---
name: stock-price
description: "Looks up a live stock price by ticker symbol using Yahoo Finance and returns price, daily change, and trading volume in a single line."
user-invocable: true
command-dispatch: tool
metadata:
  openclaw:
    os: ["darwin", "linux"]
    requires:
      bins: ["curl", "jq"]
---

## Tool: get_stock_price

Fetch the current price for a given ticker symbol.

### Parameters
- `ticker` (string, required) — The stock ticker symbol, e.g. `AAPL`, `TSLA`, `NVDA`.

### Implementation

Run the following shell command, substituting the ticker symbol:

```bash
curl -s "https://query1.finance.yahoo.com/v8/finance/chart/{{ticker}}?interval=1d&range=1d" \
  | jq -r '
      .chart.result[0]
      | {
          symbol: .meta.symbol,
          price:  (.meta.regularMarketPrice | tostring),
          change: ((.meta.regularMarketPrice - .meta.chartPreviousClose) | . * 100 | round / 100 | tostring),
          volume: (.meta.regularMarketVolume | tostring)
        }
      | "[\(.symbol)] $\(.price)  Δ\(.change)  Vol:\(.volume)"
    '

Return the single output line verbatim, then add one sentence of context if the change is more than 3 % in either direction.

Error handling

If curl returns an empty body or jq exits non-zero, respond: “Could not fetch price for {{ticker}}. The ticker may be invalid or Yahoo Finance may be temporarily unavailable.”


A few things worth noting in the body:

- `{{ticker}}` is OpenClaw's tool-parameter interpolation syntax. The runtime replaces it with the value extracted from the user's message before executing the shell command.
- The `## Tool:` heading is the convention for declaring a tool inside a skill body. You can define multiple tools in one `SKILL.md` by adding more `## Tool:` sections.
- Error handling instructions appear in plain Markdown — the LLM reads them and generates the appropriate response when the command fails.

### Test the Skill Locally

Restart your OpenClaw session so the new skill is picked up from disk:

```bash
openclaw restart

Then confirm the skill is visible:

openclaw skills check

You should see stock-price listed under ~/.agents/skills. If it is absent, check that the folder name exactly matches the name field in frontmatter and that curl and jq are both on your PATH.

Now test it in a session:

/stock-price AAPL

Because command-dispatch: tool is set, the agent immediately calls get_stock_price without an intermediate reasoning step. You should receive output like:

[AAPL] $189.72  Δ+1.43  Vol:52134200

Tutorial: Build a Weather Skill

The weather skill is simpler: cross-platform, no binary guards, and uses the free wttr.in API over plain HTTPS. It is a good template for skills that need to work on Windows as well as Unix-like systems.

mkdir -p ~/.agents/skills/weather-now
---
name: weather-now
description: "Returns the current weather conditions for a city using the wttr.in service. Provides temperature, conditions, and wind speed."
user-invocable: true
---

## Tool: get_weather

Retrieve current weather for a location.

### Parameters
- `location` (string, required) — City name or postal code, e.g. `Seoul`, `New York`, `SW1A 1AA`.

### Implementation

Fetch the one-line summary from the public wttr.in API:

```bash
curl -s "https://wttr.in/{{location}}?format=3"

wttr.in?format=3 returns a single line in the form:

Seoul: ⛅️  +14°C

Return the line verbatim, then append: “Would you like a full forecast or humidity details?”

Error handling

If the response body is empty or contains “Unknown location”, respond: “I could not find weather data for ‘{{location}}’. Please check the city name and try again.”


Because there is no `metadata.openclaw.os` key, this skill loads on all platforms including Windows. There is also no `requires.bins` — `curl` is available on all modern operating systems by default. This makes `weather-now` a good candidate for later publication to ClawHub.

## Load-Time Filters: OS and Binary Guards

The `metadata.openclaw` block is evaluated at session start, before any LLM call is made. Think of it as a compile-time gate rather than a runtime check.

**OS filter in practice.** Suppose you maintain a skill that wraps `osascript` (the macOS scripting bridge). You would gate it like this:

```yaml
metadata:
  openclaw:
    os: ["darwin"]

On a Linux or Windows machine this skill is silently skipped. Its token cost is zero. Other developers can safely clone your dotfiles without the skill causing errors on their systems.

Binary guard in practice. A transcription skill that relies on ffmpeg and the uv Python runner should declare:

metadata:
  openclaw:
    requires:
      bins: ["ffmpeg", "uv"]

If either binary is absent, the skill is not loaded. No skill description lands in the context, so the LLM never offers transcription as an option — which is exactly the right behaviour.

Combining both filters. Filters are AND-ed together. A skill with both os: ["darwin"] and requires.bins: ["ffmpeg"] only loads on macOS machines that also have ffmpeg installed.

metadata:
  openclaw:
    os: ["darwin", "linux"]
    requires:
      bins: ["ffmpeg", "uv", "whisper"]

This pattern is common for audio and video processing skills that depend on heavyweight system tools.

You can also combine binary guards with the enabled flag (covered in Managing Skills) to temporarily disable a skill without removing it from disk.

Install and Discover Skills from ClawHub

ClawHub is the official OpenClaw skill registry. At the time of writing it hosts over 13,000 community skills covering everything from Jira ticket management to PostgreSQL query generation.

Check what skills are currently loaded:

openclaw skills check

This command lists every skill the runtime found during the last session start, grouped by priority level, along with the resolved file path and whether any load-time filters were applied.

Search for a skill on ClawHub:

The npx clawhub runner requires no global installation. Use it to browse the registry directly from your terminal:

npx clawhub search "github pull request"
npx clawhub search "vector database pinecone"

Results include the skill slug, description, author, weekly install count, and last updated date — enough information to decide whether a skill is worth installing.

Install a skill:

npx clawhub install github-pr-review
npx clawhub install pinecone-query

By default, clawhub install places skills in ~/.openclaw/skills/ (priority level 4). That means a workspace-local skill of the same name (levels 1–3) will still take priority.

Update all installed skills:

npx clawhub update --all

This pulls the latest version of every skill in ~/.openclaw/skills/ and re-validates frontmatter. Skills with breaking changes in their frontmatter schema are held back and reported in the output.

Sync across machines. If you keep your skills in a dotfiles repository, sync writes the current remote state back to your local skills directory:

npx clawhub sync --all

Use --dry-run to preview what would change without writing any files:

npx clawhub sync --all --dry-run

Publish Your Skill to ClawHub

Publishing is a four-step workflow: authenticate, validate, publish, and sync.

Step 1 — Authenticate

npx clawhub login

This opens a browser window to the ClawHub OAuth flow. Authenticate with your GitHub account. Once complete, a token is written to ~/.clawhub/credentials.json.

Step 2 — Validate your SKILL.md locally

Before publishing, check that your frontmatter parses correctly and that all required fields are present:

npx clawhub validate ./weather-now/SKILL.md

Common validation errors include missing name or description fields, invalid os values (must be darwin, linux, or win32), and non-string entries in requires.bins.

Step 3 — Publish

clawhub publish ./weather-now \
  --slug weather-now \
  --name "Weather Now" \
  --version 1.0.0 \
  --changelog "Initial release — cross-platform weather via wttr.in"

Flag reference:

FlagRequiredDescription
--slugYesURL-safe identifier. Must be globally unique on ClawHub.
--nameYesHuman-readable display name shown in search results.
--versionYesSemantic version string (MAJOR.MINOR.PATCH).
--changelogRecommendedShort description of what changed in this version.
--visibilityNopublic (default) or unlisted.

On success the CLI prints the ClawHub URL for your skill page, e.g.:

✓ Published [email protected]
  https://clawhub.dev/skills/weather-now

Step 4 — Push an update

When you improve the skill, increment the version number and re-run publish:

clawhub publish ./weather-now \
  --slug weather-now \
  --name "Weather Now" \
  --version 1.1.0 \
  --changelog "Add humidity and wind details to output format"

ClawHub stores all historical versions. Users who have pinned @1.0.0 in their install command are not automatically updated.

Managing Skills: Enable, Disable, Update

Disabling a bundled skill

OpenClaw ships around 50 bundled skills. If one of them occupies a name you want to use, or if it is adding token overhead for context you never need, you can disable it in openclaw.json:

{
  "skills": {
    "bundled": {
      "weather": {
        "enabled": false
      },
      "1password": {
        "enabled": false
      }
    }
  }
}

Disabled skills are excluded from context injection entirely, saving their ~97-character per-skill token cost.

Pinning an external directory

The lowest-priority level in the chain — skills.load.extraDirs — lets you point OpenClaw at any directory on disk, such as a shared network drive or a monorepo’s company-skills/ folder:

{
  "skills": {
    "load": {
      "extraDirs": [
        "/mnt/shared/company-skills",
        "../../shared/agent-skills"
      ]
    }
  }
}

Relative paths in extraDirs are resolved relative to the openclaw.json file. Absolute paths work on all platforms.

Updating CLI-installed skills

npx clawhub update --all          # update everything in ~/.openclaw/skills/
npx clawhub update weather-now    # update a single skill by slug

After updating, restart your OpenClaw session to pick up the new versions:

openclaw restart

Checking for outdated skills

npx clawhub outdated

This compares every installed skill’s local version against the latest published version on ClawHub and prints a table of available upgrades — similar to npm outdated.

Frequently Asked Questions

How does OpenClaw decide which skill to invoke?

At session start, OpenClaw injects the description field of every loaded skill into the system prompt. When you send a message, the LLM reads those descriptions and scores each skill against your intent. The highest-scoring skill (if above a confidence threshold) is activated. If you set user-invocable: true, you can also force invocation with a /skill-name slash command regardless of scoring.

The practical implication is that your description field is the single most important line in your SKILL.md. Keep it specific enough that the LLM can confidently distinguish your skill from similar ones, and general enough that it matches a broad range of natural phrasings.

Can a skill call another skill?

Not directly — a skill is a set of instructions for the LLM, not executable code that can import or invoke other modules. However, you can design your skill body to instruct the LLM to use tools from multiple skills in sequence. As long as all the relevant skills are loaded in the session, the LLM can follow multi-skill workflows.

If you need strict multi-step automation with guaranteed execution order, consider writing a single skill that defines multiple tools and documents the expected tool-call sequence in the Markdown body.

What happens if two skills have the same name?

The six-level priority chain resolves the conflict deterministically. The skill found at the highest priority level wins; all lower-level skills with the same name are silently ignored. There is no merge: only one SKILL.md file is injected into the context for any given skill name.

If you want to see which version of a conflicting skill is actually loaded, run openclaw skills check — it shows the resolved file path for every skill so you can confirm which level won.

Advanced Patterns: Multi-Tool Skills and Parameter Validation

Once you are comfortable with single-tool skills, you can push the pattern further in two directions: bundling multiple related tools into one SKILL.md, and adding explicit parameter validation logic that the LLM enforces before executing a shell command.

Bundling multiple tools

A single SKILL.md can contain any number of ## Tool: sections. This is useful when a set of tools shares a common description — you want the LLM to load them all together or not at all.

Here is a portfolio-tracker skill that groups three related tools:

---
name: portfolio-tracker
description: "Tracks a personal stock portfolio — look up individual prices, show the full portfolio summary, or add a new ticker to the watch list."
user-invocable: true
metadata:
  openclaw:
    os: ["darwin", "linux"]
    requires:
      bins: ["curl", "jq", "sqlite3"]
---

## Tool: portfolio_add

Add a ticker symbol to the local SQLite watch list.

### Parameters
- `ticker` (string, required) — Ticker symbol to add, e.g. `MSFT`.

### Implementation

```bash
sqlite3 ~/.portfolio.db \
  "CREATE TABLE IF NOT EXISTS tickers (symbol TEXT PRIMARY KEY, added_at TEXT); \
   INSERT OR IGNORE INTO tickers VALUES ('{{ticker}}', datetime('now'));"

Respond: “Added {{ticker}} to your portfolio watch list.”


Tool: portfolio_list

Show all tracked tickers with their current prices.

Implementation

sqlite3 -separator ',' ~/.portfolio.db "SELECT symbol FROM tickers;" \
  | while IFS=',' read -r sym; do
      result=$(curl -s "https://query1.finance.yahoo.com/v8/finance/chart/${sym}?interval=1d&range=1d" \
        | jq -r '.chart.result[0].meta | "[\(.symbol)] $\(.regularMarketPrice)"')
      echo "$result"
    done

Present the results as a Markdown table with columns: Symbol | Price.


This single file registers two callable tools. The LLM decides which tool to invoke based on the user's intent, using the tool descriptions as selectors.

### Adding parameter validation

You can instruct the LLM to validate parameters before executing a tool by adding a **Validation** subsection to any `## Tool:` block:

```markdown
## Tool: get_stock_price

Fetch the current price for a given ticker symbol.

### Parameters
- `ticker` (string, required) — The stock ticker symbol.

### Validation
- `ticker` must be 1–5 uppercase letters only. If the user provides a lowercase ticker, convert it to uppercase silently. If the input contains numbers or special characters, respond: "{{ticker}} does not look like a valid ticker symbol. Please provide a symbol like AAPL or TSLA."

### Implementation

This pattern moves input sanitisation into the skill definition rather than the shell command, keeping the implementation block clean and readable.

Skill versioning in your dotfiles

If you track your personal skills in a Git repository (recommended for reproducibility), adopt this layout:

dotfiles/
└── skills/
    ├── stock-price/
    │   ├── SKILL.md
    │   └── CHANGELOG.md
    ├── weather-now/
    │   ├── SKILL.md
    │   └── CHANGELOG.md
    └── portfolio-tracker/
        ├── SKILL.md
        └── CHANGELOG.md

Point extraDirs at the repository path:

{
  "skills": {
    "load": {
      "extraDirs": ["~/dotfiles/skills"]
    }
  }
}

Now your skills are version-controlled alongside the rest of your configuration, and a git pull on any machine keeps your skill library in sync without involving ClawHub.

Next Steps

You now have the foundation to build, test, and share OpenClaw skills. Two natural directions from here:

Persistent memory integration. Skills become dramatically more powerful when combined with OpenClaw’s persistent memory layer. An agent that can recall previous stock lookups, remember preferred city names for weather queries, or maintain a running research log across sessions behaves more like a domain expert than a one-shot command runner. See the OpenClaw Skills and Nodes guide for how the skill system integrates with the node graph.

Cross-framework tool design. The tool-definition pattern inside SKILL.md bodies shares design DNA with LangChain’s tool abstraction. If you are building multi-agent systems that combine OpenClaw with a LangChain orchestrator, reading LangChain Agents and Tools will give you vocabulary for reasoning about when to push logic into a skill versus into a Python tool callable.

The OpenClaw ecosystem is growing quickly. Publishing even a small utility skill to ClawHub contributes to a library that other developers can build on — and the 97 characters of context you give them may save hours of manual configuration.

Related Articles