🔍
DevelopmentChapter 24 of 33· 5 min read

Chapter 24: Building Custom Skills

ClawHub has hundreds of community skills, but sometimes you need something that doesn't exist yet — your company's internal API, a proprietary data source, or a custom workflow. This chapter walks you through building a complete custom skill from scratch.


Skill Architecture

An OpenClaw skill is a Node.js package that exposes one or more tools following the Model Context Protocol (MCP). Each tool has:

  • A name and description (used by the AI to decide when to call it)
  • An input schema (JSON Schema defining the parameters)
  • A handler function (the actual implementation)

When the AI decides to use a tool, OpenClaw calls your handler with the validated input and returns the result to the AI.


Project Setup

mkdir my-openclaw-skill
cd my-openclaw-skill
npm init -y
npm install @openclaw/skill-sdk

Minimal Skill Example

This skill adds a single tool that fetches weather data:

// index.js
const { defineSkill } = require('@openclaw/skill-sdk');

module.exports = defineSkill({
  name: 'weather',
  version: '1.0.0',
  description: 'Get current weather for any city',

  config: {
    apiKey: { type: 'string', required: true, envVar: 'WEATHER_API_KEY' }
  },

  tools: [
    {
      name: 'get-weather',
      description: 'Get the current weather and forecast for a city',
      inputSchema: {
        type: 'object',
        properties: {
          city: {
            type: 'string',
            description: 'The city name, e.g. "Karachi" or "London, UK"'
          },
          units: {
            type: 'string',
            enum: ['metric', 'imperial'],
            default: 'metric',
            description: 'Temperature units'
          }
        },
        required: ['city']
      },
      handler: async ({ city, units = 'metric' }, { config }) => {
        const res = await fetch(
          `https://api.weatherapi.com/v1/current.json?key=${config.apiKey}&q=${encodeURIComponent(city)}`
        );
        const data = await res.json();

        if (data.error) {
          return { error: data.error.message };
        }

        const temp = units === 'metric' ? data.current.temp_c : data.current.temp_f;
        const unit = units === 'metric' ? '°C' : '°F';

        return {
          city: data.location.name,
          country: data.location.country,
          temperature: `${temp}${unit}`,
          condition: data.current.condition.text,
          humidity: `${data.current.humidity}%`,
          wind: `${data.current.wind_kph} km/h`
        };
      }
    }
  ]
});

Tool Design Best Practices

Write Clear Descriptions

The AI reads your tool name and description to decide whether to use it. Be specific:

// Bad
description: 'Get data'

// Good
description: 'Fetch customer order history from the internal orders API. Returns order IDs, dates, amounts, and status.'

Return Structured Data

Return objects rather than strings when possible — the AI can reason better over structured data:

// Less good
return `The weather in Karachi is 32°C and sunny`;

// Better
return {
  city: 'Karachi',
  temperature: '32°C',
  condition: 'Sunny',
  humidity: '65%'
};

Handle Errors Gracefully

Never throw uncaught exceptions — return an error object the AI can communicate to the user:

handler: async (input, ctx) => {
  try {
    const result = await callApi(input);
    return result;
  } catch (err) {
    return { error: `Failed to fetch data: ${err.message}` };
  }
}

Multi-Tool Skills

A single skill can expose many tools:

module.exports = defineSkill({
  name: 'company-crm',
  tools: [
    {
      name: 'search-customers',
      description: 'Search for customers by name, email, or company',
      inputSchema: { ... },
      handler: async ({ query }, ctx) => { ... }
    },
    {
      name: 'get-customer',
      description: 'Get full details for a customer by their ID',
      inputSchema: { ... },
      handler: async ({ customerId }, ctx) => { ... }
    },
    {
      name: 'create-note',
      description: 'Add a note to a customer record',
      inputSchema: { ... },
      handler: async ({ customerId, note }, ctx) => { ... }
    }
  ]
});

Accessing Context

The handler receives a second argument ctx with runtime context:

handler: async (input, ctx) => {
  ctx.config      // Your skill's config values
  ctx.userId      // The user who triggered this tool call
  ctx.workspaceId // The current workspace
  ctx.log         // Logging: ctx.log.info(), ctx.log.error()
  ctx.cache       // Simple key-value cache (per-user, persists for session)
}

Using the Cache

handler: async ({ customerId }, ctx) => {
  const cached = await ctx.cache.get(`customer:${customerId}`);
  if (cached) return cached;

  const customer = await fetchCustomerFromApi(customerId, ctx.config.apiKey);
  await ctx.cache.set(`customer:${customerId}`, customer, { ttl: 300 });
  return customer;
}

Testing Your Skill

// test.js
const skill = require('./index');
const { testSkill } = require('@openclaw/skill-sdk/testing');

testSkill(skill, {
  config: { apiKey: 'test-key' },
  tools: {
    'get-weather': [
      {
        input: { city: 'Karachi' },
        expect: (result) => {
          console.assert(result.city === 'Karachi', 'City should be Karachi');
          console.assert(result.temperature, 'Should have temperature');
        }
      }
    ]
  }
});

Run tests:

node test.js

Installing Your Custom Skill Locally

# From the skill directory
openclaw hub install ./my-openclaw-skill

# Or install by path in config
{
  "skills": {
    "weather": {
      "enabled": true,
      "path": "/path/to/my-openclaw-skill",
      "apiKey": "${WEATHER_API_KEY}"
    }
  }
}

Publishing to ClawHub

# Log in
openclaw hub login

# Verify package.json has "openclaw-skill" keyword
cat package.json | grep openclaw-skill

# Publish
openclaw hub publish

Your skill appears on hub.openclaw.dev within a few minutes after the automated security scan completes.


Next: Chapter 25 — API Integration — How to call OpenClaw's REST API programmatically to send messages, manage workspaces, and build integrations.