OpenClaw Complete Guide Part 4: Building Your First Custom Skill

OpenClaw Complete Guide Part 4: Building Your First Custom Skill

In Part 3 we learned that skills are Markdown instruction files that teach your agent how to use its tools for specific tasks. In this post we build one from scratch. By the end you will have a working custom skill that automates a real developer workflow, and you will understand the full authoring process well enough to build any skill you need.

We will build three progressively more complex examples: a simple daily standup logger, a Node.js project health checker, and a Python-based Azure deployment status reporter. All code is production-ready and tested against OpenClaw with the exec and file-write tools enabled.

How Custom Skills Fit Into OpenClaw

Before writing any code, here is where your custom skill lives and how OpenClaw discovers it.

flowchart TD
    A["You write a skill folder\nwith SKILL.md inside"]
    A --> B["Place it in your workspace\n~/openclaw/skills/your-skill-name/"]
    B --> C["OpenClaw loads it at startup\nor on gateway restart"]
    C --> D["Agent reads SKILL.md\ninto its context when relevant"]
    D --> E["You message your agent\nand it uses the skill"]
    E --> F["Agent executes steps\nusing available tools"]

The skill directory must follow a specific convention: the folder name becomes the skill name, and it must contain at minimum a SKILL.md file. You can also include supporting scripts, templates, and resource files inside the same folder.

Anatomy of a SKILL.md File

Every SKILL.md has two sections: a YAML frontmatter block that defines metadata and requirements, and a Markdown body that contains the natural language instructions your agent will follow.

flowchart TD
    F["SKILL.md"]
    F --> FM["YAML Frontmatter\n---\nname, description, version\nauthor, requires, tags\n---"]
    F --> MB["Markdown Body\nNatural language instructions\nTask examples\nSecurity guidelines\nEdge case handling"]
    FM --> REQ["requires block\n  tools: exec, file-write\n  binaries: node, python\n  config: API keys"]
    REQ --> EL["Eligibility check\nSkill only loads if ALL\nrequirements are present"]

Example 1: Daily Standup Logger (Beginner)

This skill teaches your agent to log your daily standup notes to a Markdown file and summarize the week on Fridays. It is a good first skill because it only needs the file-write and file-read tools.

Directory Structure


mkdir -p ~/openclaw/skills/standup-logger
touch ~/openclaw/skills/standup-logger/SKILL.md

SKILL.md


---
name: standup-logger
description: Log daily standup notes and generate weekly summaries
version: 1.0.0
author: chandan
requires:
  tools:
    - file-read
    - file-write
tags:
  - productivity
  - development
  - logging
---

# Standup Logger Skill

This skill helps the user log their daily standup notes and review progress over time.

## Storage Location

All standup logs are stored in ~/standup/ as individual Markdown files named by date:
- ~/standup/2026-03-17.md  (daily log)
- ~/standup/weekly-summary.md  (updated every Friday)

## Logging a Standup

When the user says anything like "log standup", "standup", "daily update", or provides
yesterday/today/blocker information, capture it in this format:

```
# Standup - {DATE}

## Yesterday
{what was done}

## Today
{what is planned}

## Blockers
{any blockers, or "None"}
```

Always confirm the file was written and show the user the saved content.

## Weekly Summary

Every Friday, or when the user asks for a "weekly summary" or "week review",
read all standup files from the current week (Monday to Friday) and produce a
summary showing: tasks completed, recurring blockers, and key accomplishments.

## Edge Cases

- If the user provides partial information (e.g. only today's tasks), log what
  is available and mark missing sections as "Not provided".
- Never overwrite an existing daily log without asking for confirmation.
- Always use ISO date format (YYYY-MM-DD) for filenames.

Testing It

After saving the file, restart your gateway and message your agent:


openclaw gateway restart

Then in Telegram: “Log standup: Yesterday I finished the auth module. Today I am working on the API gateway. No blockers.”

Your agent should create ~/standup/2026-03-17.md with the formatted content and confirm the save.

Example 2: Node.js Project Health Checker (Intermediate)

This skill runs a set of health checks on a Node.js project and reports issues. It uses the exec tool to run commands and file-read to inspect project files. It includes a supporting Node.js script for the actual checks.

Directory Structure


mkdir -p ~/openclaw/skills/node-health
touch ~/openclaw/skills/node-health/SKILL.md
touch ~/openclaw/skills/node-health/check.js

check.js


#!/usr/bin/env node
// node-health/check.js
// Run with: node check.js /path/to/project

const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");

const projectPath = process.argv[2] || process.cwd();
const results = { path: projectPath, checks: [], score: 0, total: 0 };

function check(label, passed, message) {
  results.total++;
  if (passed) results.score++;
  results.checks.push({ label, passed, message });
}

// 1. package.json exists
const pkgPath = path.join(projectPath, "package.json");
const pkgExists = fs.existsSync(pkgPath);
check("package.json exists", pkgExists, pkgExists ? "Found" : "Missing package.json");

if (pkgExists) {
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));

  // 2. Has test script
  check(
    "Test script defined",
    !!pkg.scripts?.test && pkg.scripts.test !== "echo \"Error: no test specified\"",
    pkg.scripts?.test ? `test: ${pkg.scripts.test}` : "No test script defined"
  );

  // 3. Has engines field
  check(
    "Node.js engine specified",
    !!pkg.engines?.node,
    pkg.engines?.node ? `engines.node: ${pkg.engines.node}` : "engines.node not specified"
  );

  // 4. No known vulnerable packages (basic check via npm audit)
  try {
    execSync("npm audit --json --audit-level=high", {
      cwd: projectPath,
      stdio: "pipe",
    });
    check("npm audit (high)", true, "No high-severity vulnerabilities found");
  } catch (e) {
    const audit = JSON.parse(e.stdout?.toString() || "{}");
    const high = audit?.metadata?.vulnerabilities?.high || 0;
    const critical = audit?.metadata?.vulnerabilities?.critical || 0;
    check(
      "npm audit (high)",
      false,
      `Found ${high} high and ${critical} critical vulnerabilities`
    );
  }
}

// 5. .env.example exists if .env exists
const envExists = fs.existsSync(path.join(projectPath, ".env"));
const envExampleExists = fs.existsSync(path.join(projectPath, ".env.example"));
if (envExists) {
  check(
    ".env.example present",
    envExampleExists,
    envExampleExists ? "Found .env.example" : ".env exists but .env.example is missing"
  );
}

// 6. .gitignore includes node_modules
const gitignorePath = path.join(projectPath, ".gitignore");
if (fs.existsSync(gitignorePath)) {
  const gi = fs.readFileSync(gitignorePath, "utf8");
  check(
    ".gitignore covers node_modules",
    gi.includes("node_modules"),
    gi.includes("node_modules") ? "node_modules ignored" : "node_modules NOT in .gitignore"
  );
}

// Output
console.log(JSON.stringify(results, null, 2));

SKILL.md


---
name: node-health
description: Run health checks on a Node.js project and report issues
version: 1.0.0
author: chandan
requires:
  tools:
    - exec
    - file-read
  binaries:
    - node
    - npm
tags:
  - development
  - node.js
  - quality
---

# Node.js Project Health Checker

This skill runs automated health checks on a Node.js project directory.

## Running a Health Check

When the user asks to "check project health", "audit my node project", or "run health check on [path]":

1. Identify the project path from the user's message. If not provided, ask for it.
2. Run the check script:
   node ~/openclaw/skills/node-health/check.js /path/to/project
3. Parse the JSON output and present results in a readable format showing:
   - Overall score (passed / total checks)
   - Each check with pass/fail status and the message
   - A list of recommended fixes for any failed checks
4. Suggest specific remediation steps for common failures.

## Remediation Suggestions

- Missing test script: "Add a test script to package.json, for example: jest or vitest"
- No engines.node: "Add engines: { node: '>=22.0.0' } to package.json"
- npm audit failures: "Run npm audit fix or review and update vulnerable packages"
- Missing .env.example: "Create .env.example with placeholder values for all required env vars"
- node_modules not gitignored: "Add node_modules/ to .gitignore immediately"

## Security Notes

Never run this check on paths outside the user's home directory without explicit confirmation.
Do not expose any secrets found in .env files in your response.

Testing It

Message your agent: “Check project health on ~/projects/my-api”

The agent will run the Node.js script, parse the JSON output, and return a formatted health report with specific remediation steps for any failures.

Example 3: Azure Deployment Status Reporter (Advanced)

This skill uses the Azure CLI to fetch the status of your Azure resource groups and recent deployments, then formats the results into a status report. It requires the Azure CLI installed and authenticated.

Directory Structure


mkdir -p ~/openclaw/skills/azure-status
touch ~/openclaw/skills/azure-status/SKILL.md
touch ~/openclaw/skills/azure-status/status.py

status.py


#!/usr/bin/env python3
# azure-status/status.py
# Run with: python3 status.py [resource-group]

import subprocess
import json
import sys
from datetime import datetime, timezone

resource_group = sys.argv[1] if len(sys.argv) > 1 else None

def run_az(args):
    """Run an az CLI command and return parsed JSON output."""
    try:
        result = subprocess.run(
            ["az"] + args + ["--output", "json"],
            capture_output=True, text=True, timeout=30
        )
        if result.returncode != 0:
            return None, result.stderr.strip()
        return json.loads(result.stdout), None
    except subprocess.TimeoutExpired:
        return None, "Azure CLI timed out"
    except json.JSONDecodeError:
        return None, "Failed to parse Azure CLI output"

report = {"generated_at": datetime.now(timezone.utc).isoformat(), "resource_groups": []}

# Get resource groups
rg_filter = ["group", "show", "--name", resource_group] if resource_group else ["group", "list"]
rgs, err = run_az(rg_filter)

if err:
    print(json.dumps({"error": err}))
    sys.exit(1)

rg_list = [rgs] if resource_group else rgs

for rg in rg_list:
    rg_name = rg.get("name")
    rg_info = {
        "name": rg_name,
        "location": rg.get("location"),
        "provisioning_state": rg.get("properties", {}).get("provisioningState"),
        "recent_deployments": []
    }

    # Get recent deployments for this resource group
    deployments, dep_err = run_az([
        "deployment", "group", "list",
        "--resource-group", rg_name,
        "--query", "[0:5]"  # Last 5 deployments
    ])

    if deployments and not dep_err:
        for dep in deployments:
            rg_info["recent_deployments"].append({
                "name": dep.get("name"),
                "state": dep.get("properties", {}).get("provisioningState"),
                "timestamp": dep.get("properties", {}).get("timestamp"),
                "duration": dep.get("properties", {}).get("duration"),
                "mode": dep.get("properties", {}).get("mode"),
            })

    report["resource_groups"].append(rg_info)

print(json.dumps(report, indent=2))

SKILL.md


---
name: azure-status
description: Fetch and report Azure resource group and deployment status using Azure CLI
version: 1.0.0
author: chandan
requires:
  tools:
    - exec
  binaries:
    - az
    - python3
  config:
    - AZURE_SUBSCRIPTION_ID
tags:
  - azure
  - cloud
  - devops
  - deployment
---

# Azure Deployment Status Reporter

This skill queries Azure for resource group health and recent deployment status.

## Prerequisites

The user must be logged into Azure CLI:
  az login
  az account set --subscription $AZURE_SUBSCRIPTION_ID

## Running a Status Report

When the user asks for "Azure status", "deployment status", "check Azure", or
"what is the state of [resource group]":

1. If a specific resource group is mentioned, run:
   python3 ~/openclaw/skills/azure-status/status.py 

2. For a full account overview, run:
   python3 ~/openclaw/skills/azure-status/status.py

3. Parse the JSON output and present:
   - Each resource group with its location and provisioning state
   - Recent deployments with state (Succeeded / Failed / Running) and timestamp
   - Highlight any Failed deployments prominently
   - Calculate total deployments and success rate

## Alerting

If any deployment has state "Failed", proactively highlight it and suggest:
  az deployment group show --resource-group  --name 
to get detailed error information.

## Security Notes

Never print subscription IDs, storage account keys, or connection strings in responses.
If the user asks for secrets, redirect them to Azure Key Vault.
Always confirm before running any commands that modify resources (az group delete, etc).

The Full Custom Skill Authoring Workflow

flowchart TD
    A["Define the task\nWhat should the agent do?"]
    A --> B["Identify required tools\nexec? file-write? web-fetch?"]
    B --> C["Identify required binaries\nnode? python? az? gh?"]
    C --> D["Write supporting scripts\n.js or .py for complex logic"]
    D --> E["Write SKILL.md\nfrontmatter + natural language instructions"]
    E --> F["Place in ~/openclaw/skills/skill-name/"]
    F --> G["Restart gateway\nopenclaw gateway restart"]
    G --> H["Verify skill loaded\nopenclaw skills list --eligible"]
    H --> I["Test with a message\nto your agent via Telegram"]
    I --> J{Works correctly?}
    J -->|"No"| K["Check openclaw logs --follow\nRefine SKILL.md instructions"]
    K --> G
    J -->|"Yes"| L["Skill is ready"]

Writing Effective Skill Instructions

The quality of your skill comes down to how well you write the Markdown instructions. Here are the principles that make the difference between a skill that works reliably and one that behaves unpredictably.

Be Explicit About Triggers

List the exact phrases or patterns that should activate the skill. The agent uses these to decide when to load the skill’s context. Vague descriptions lead to the skill either not loading when needed or loading when irrelevant.


## Triggers

Activate this skill when the user says:
- "log standup"
- "standup update"
- "daily update"
- Provides yesterday/today/blocker information in a single message

Define Exact Commands

Do not leave the agent to guess which command to run. Specify the exact command with argument patterns. This prevents the agent from improvising and potentially running unintended commands.

Handle Edge Cases Explicitly

Think through what could go wrong and write instructions for it. Missing input, failed commands, files that do not exist yet. The more edge cases you cover in the skill, the fewer times the agent will behave unexpectedly.

Include a Security Notes Section

Every skill that has system access should have explicit security guardrails written into it. Tell the agent what it must never do. This creates a behavioral constraint that applies every time the skill is loaded, regardless of what the user asks.

Keep Skills Focused

Resist the temptation to make one skill do everything. A skill that logs standups, checks Azure deployments, and sends Slack messages is harder to maintain and debug than three focused skills. Single responsibility applies here just as it does in code.

Skill Configuration in openclaw.json

Once your skill is in the workspace, you can configure it in openclaw.json to pass environment variables and control whether it is enabled:


{
  "skills": {
    "entries": {
      "standup-logger": {
        "enabled": true
      },
      "node-health": {
        "enabled": true
      },
      "azure-status": {
        "enabled": true,
        "env": {
          "AZURE_SUBSCRIPTION_ID": "your-subscription-id-here"
        }
      }
    }
  }
}

Debugging a Skill That Is Not Loading

flowchart TD
    P["Skill not appearing in eligible list?"]
    P --> Q1{"Correct folder structure?\nskills/skill-name/SKILL.md"}
    Q1 -->|"No"| A1["Create folder and SKILL.md\nwith correct naming"]
    Q1 -->|"Yes"| Q2{"YAML frontmatter valid?"}
    Q2 -->|"No"| A2["Check for syntax errors\nin --- block\nUse a YAML linter"]
    Q2 -->|"Yes"| Q3{"All required tools enabled?"}
    Q3 -->|"No"| A3["Enable missing tools\nin openclaw.json\nthen restart gateway"]
    Q3 -->|"Yes"| Q4{"All required binaries installed?"}
    Q4 -->|"No"| A4["Install missing binaries\ne.g. npm install -g gh\nor pip install azure-cli"]
    Q4 -->|"Yes"| A5["Run: openclaw doctor\nCheck: openclaw logs --follow\nLook for skill load errors"]

Publishing a Skill to ClawHub

Once your skill works well locally, you can share it with the community by submitting it to the official skills repository. The process is straightforward:

  1. Fork the repository at github.com/openclaw/skills
  2. Add your skill folder under the appropriate category directory
  3. Ensure your SKILL.md has complete frontmatter including name, description, version, author, requires, and tags
  4. Add a README.md inside the skill folder explaining setup steps
  5. Open a pull request with a description of what the skill does and any prerequisites

Before submitting, run your skill through the security-check skill yourself. The project maintainers review submissions but the volume means not every skill gets deep manual review. Community trust builds over time through stars, reviews, and active maintenance.

What Is Next

You can now write custom skills that extend your agent with any workflow you need. In Part 5 we move to production: deploying OpenClaw on a Linux VPS, setting it up as a systemd service, securing it with a Cloudflare tunnel, and running it reliably 24/7 without needing your laptop to be on.

References

Written by:

588 Posts

View All Posts
Follow Me :
How to whitelist website on AdBlocker?

How to whitelist website on AdBlocker?

  1. 1 Click on the AdBlock Plus icon on the top right corner of your browser
  2. 2 Click on "Enabled on this site" from the AdBlock Plus option
  3. 3 Refresh the page and start browsing the site