Development

How to Build Custom OpenClaw Skills: A Developer's Guide

Build your own OpenClaw skills from scratch. Covers the SKILL.md spec, adding tools, testing locally, and publishing to ClawHub. With real examples.

March 22, 2026·10 min read

OpenClaw skills are the fastest way to extend your AI assistant without modifying core code. A skill adds new capabilities — weather lookup, CRM queries, image generation, anything with an API — in minutes. You write a SKILL.md file, define what it does, and OpenClaw loads it automatically.

This guide covers the full development cycle: from scaffolding to testing to publishing on ClawHub.

Want skills pre-installed? MrDelegate's managed OpenClaw comes with 12 production-ready skills configured out of the box. See what's included →

What a Skill Is

A skill is a directory with a SKILL.md file. The SKILL.md tells OpenClaw:

  • What the skill does (description that triggers activation)
  • How to use it (step-by-step instructions)
  • What scripts or reference files it includes

Skills can also include:

  • Shell scripts (scripts/ subdirectory)
  • Reference data (references/ subdirectory)
  • Config files the agent reads during execution

The simplest possible skill is a single SKILL.md file. No code required.

Skill Discovery: How OpenClaw Loads Skills

OpenClaw scans the skills directory on startup. Every folder with a valid SKILL.md gets registered. When you send a message, OpenClaw scans skill descriptions and activates the most relevant one.

The matching is semantic, not keyword-based. A skill described as "fetch current weather and forecasts" will activate when you ask "what's the weather in Miami" — even though "Miami" isn't in the description.

This means your description matters more than your code. Write descriptions that match how users will actually ask for the feature.

Scaffolding a New Skill

Directory structure for a new skill called crm-lookup:

skills/
  crm-lookup/
    SKILL.md          # Required — the skill definition
    scripts/
      lookup.sh       # Optional — shell script
    references/
      field-map.md    # Optional — reference data

Create the structure:

mkdir -p /usr/lib/node_modules/openclaw/skills/crm-lookup/scripts
mkdir -p /usr/lib/node_modules/openclaw/skills/crm-lookup/references
touch /usr/lib/node_modules/openclaw/skills/crm-lookup/SKILL.md

Writing the SKILL.md

The SKILL.md format follows a strict spec. Every field matters.

# CRM Lookup Skill

## Description
Look up customer records in HubSpot CRM by email, company, or deal name. Returns contact details, deal stage, last activity date, and assigned rep. Use when someone asks about a customer, prospect, or company in the CRM.

## Prerequisites
- HubSpot API key set as HUBSPOT_API_KEY environment variable
- jq installed (brew install jq / apt install jq)

## Usage

### Look up by email
```bash
bash scripts/lookup.sh --email "customer@company.com"
```

### Look up by company name
```bash
bash scripts/lookup.sh --company "Acme Corp"
```

## Output Format
Returns JSON with fields: id, email, company, deal_stage, last_activity, rep_name, deal_value

## Notes
- API rate limit: 100 requests per 10 seconds
- If contact not found, returns {"error": "not_found"}
- Deal values are in USD

The description paragraph (under ## Description) is what OpenClaw uses to decide if this skill applies. Make it specific and include the trigger phrases users will actually say.

Writing the Skill Script

For the crm-lookup skill, the shell script does the actual API call:

#!/bin/bash
# scripts/lookup.sh

set -euo pipefail

HUBSPOT_URL="https://api.hubapi.com"
HEADERS=(-H "Authorization: Bearer $HUBSPOT_API_KEY" -H "Content-Type: application/json")

lookup_by_email() {
  local email="$1"
  curl -s "${HEADERS[@]}"     "${HUBSPOT_URL}/crm/v3/objects/contacts/search"     -d "{"filterGroups":[{"filters":[{"propertyName":"email","operator":"EQ","value":"${email}"}]}]}"     | jq '{id: .results[0].id, email: .results[0].properties.email, company: .results[0].properties.company}'
}

lookup_by_company() {
  local company="$1"
  curl -s "${HEADERS[@]}"     "${HUBSPOT_URL}/crm/v3/objects/companies/search"     -d "{"filterGroups":[{"filters":[{"propertyName":"name","operator":"CONTAINS_TOKEN","value":"${company}"}]}]}"     | jq '{id: .results[0].id, name: .results[0].properties.name, domain: .results[0].properties.domain}'
}

# Parse arguments
while [[ $# -gt 0 ]]; do
  case $1 in
    --email) EMAIL="$2"; shift 2 ;;
    --company) COMPANY="$2"; shift 2 ;;
    *) echo "Unknown arg: $1"; exit 1 ;;
  esac
done

if [ -n "${EMAIL:-}" ]; then
  lookup_by_email "$EMAIL"
elif [ -n "${COMPANY:-}" ]; then
  lookup_by_company "$COMPANY"
else
  echo '{"error": "provide --email or --company"}'
  exit 1
fi

Testing Your Skill Locally

Three tests before shipping any skill:

Test 1: Script works in isolation

export HUBSPOT_API_KEY=your-key
bash skills/crm-lookup/scripts/lookup.sh --email "test@example.com"

Expected: JSON output. If you get an error — fix the script before moving on.

Test 2: Script works in cron context (no env)

env -i bash -c 'source /root/.env && bash skills/crm-lookup/scripts/lookup.sh --email test@example.com'

This simulates how the script runs when called by OpenClaw. Missing env variables show up here, not in direct execution.

Test 3: Skill activates correctly

Restart OpenClaw and ask it: "Look up customer john@acme.com in the CRM."

Check the OpenClaw log to confirm the crm-lookup skill was selected (not another skill). If a different skill activated — rewrite the description to be more specific.

The 4 Most Common Skill Bugs

1. Description too vague

A description like "helps with customer data" matches too many scenarios. Be specific: "Look up customer records in HubSpot CRM by email, company, or deal name."

2. Missing error handling

Scripts that crash silently are hard to debug. Always handle the "not found" case and return a parseable error: {"error": "not_found", "query": "john@acme.com"}

3. Hardcoded paths

Skills that reference /home/username/ break on different servers. Use environment variables for all paths.

4. No rate limit handling

If your API returns 429, a script without retry logic will fail silently. Add exponential backoff:

retry_with_backoff() {
  local n=1
  local max=3
  until "$@"; do
    if [ $n -ge $max ]; then
      echo "Failed after $max retries"
      return 1
    fi
    sleep $((2 ** n))
    n=$((n + 1))
  done
}

Publishing to ClawHub

ClawHub is the OpenClaw skill marketplace. Publishing makes your skill installable with one command by any OpenClaw user.

Prerequisites:

  1. clawhub CLI installed: npm install -g clawhub
  2. Account at clawhub.com
  3. Skill tested and working locally

Publish:

cd skills/crm-lookup
clawhub publish

ClawHub validates your SKILL.md format, packages the directory, and makes it available. Other users install it with:

clawhub install crm-lookup

For versioned updates:

clawhub publish --version 1.1.0
12 skills pre-configured. MrDelegate's managed hosting includes weather, Telegram, Discord, Supabase, SEO research, email, calendar, and more — all configured and tested. Start with managed hosting →

Skill Security Checklist

Before publishing any skill:

  • No API keys hardcoded in scripts — environment variables only
  • No sensitive data in SKILL.md or reference files
  • Input validation on all user-provided parameters (prevent injection)
  • Output sanitization if the result goes to a public channel
  • Rate limiting documented in SKILL.md → Notes section

Related Reading