Skip to main content

Custom Skills

Skills are bundled tools and instructions that extend agent capabilities. SimpleClaw supports three skill types:
  • Bundled Skills - Shipped with SimpleClaw
  • Managed Skills - Installed from ClawHub registry
  • Workspace Skills - Custom local skills

Quick Start

1

Create Skill Directory

mkdir -p ~/.simpleclaw/workspace/skills/my-skill
cd ~/.simpleclaw/workspace/skills/my-skill
2

Create SKILL.md

Create SKILL.md with tool definitions:
---
name: My Custom Skill
description: Custom tools for my workflow
version: 1.0.0
---

# My Custom Skill

This skill provides custom tools for managing my workflow.

## Tools

### todo_add

Add a task to the todo list.

**Input:**
- `task` (string, required): Task description
- `priority` (string, optional): Priority level (low, medium, high)

**Example:**
```json
{
  "task": "Review pull request #123",
  "priority": "high"
}

todo_list

List all pending tasks.Output: Returns array of tasks with priorities.
</Step>

<Step title="Create Tool Implementation">
Create `tools.ts` (or `tools.js`):

```typescript
import { Type } from "@sinclair/typebox";
import type { AnyAgentTool } from "simpleclaw/plugin-sdk";

export const tools: AnyAgentTool[] = [
  {
    name: "todo_add",
    description: "Add a task to the todo list",
    input_schema: Type.Object({
      task: Type.String({ description: "Task description" }),
      priority: Type.Optional(
        Type.Union([
          Type.Literal("low"),
          Type.Literal("medium"),
          Type.Literal("high"),
        ])
      ),
    }),
    handler: async (input) => {
      // Read existing todos
      const todos = await readTodos();
      
      // Add new task
      todos.push({
        id: Date.now(),
        task: input.task,
        priority: input.priority || "medium",
        createdAt: new Date().toISOString(),
      });
      
      // Save
      await writeTodos(todos);
      
      return {
        success: true,
        message: `Added task: ${input.task}`,
      };
    },
  },

  {
    name: "todo_list",
    description: "List all pending tasks",
    input_schema: Type.Object({}),
    handler: async () => {
      const todos = await readTodos();
      return { todos };
    },
  },
];

// Helper functions
async function readTodos() {
  const fs = await import("fs/promises");
  const path = `${process.env.HOME}/.simpleclaw/todos.json`;
  
  try {
    const data = await fs.readFile(path, "utf-8");
    return JSON.parse(data);
  } catch {
    return [];
  }
}

async function writeTodos(todos: any[]) {
  const fs = await import("fs/promises");
  const path = `${process.env.HOME}/.simpleclaw/todos.json`;
  await fs.writeFile(path, JSON.stringify(todos, null, 2));
}
3

Test the Skill

simpleclaw agent --message "Add a task: Review documentation"
The agent can now use your custom tools!

Skill Structure

my-skill/
├── SKILL.md           # Skill documentation
├── tools.ts           # Tool implementations
├── package.json       # Dependencies (optional)
├── tsconfig.json      # TypeScript config (optional)
└── data/              # Skill data (optional)

Tool Schema

Use TypeBox for type-safe tool schemas:
import { Type } from "@sinclair/typebox";

const schema = Type.Object({
  // Required string
  name: Type.String({ description: "User name" }),
  
  // Optional number
  age: Type.Optional(Type.Number({ minimum: 0, maximum: 150 })),
  
  // Enum
  status: Type.Union([
    Type.Literal("active"),
    Type.Literal("inactive"),
  ]),
  
  // Array
  tags: Type.Array(Type.String()),
  
  // Nested object
  metadata: Type.Object({
    createdAt: Type.String(),
    updatedAt: Type.String(),
  }),
});

Advanced Tool Examples

HTTP API Tool

import { Type } from "@sinclair/typebox";

export const tools = [
  {
    name: "weather_get",
    description: "Get current weather for a location",
    input_schema: Type.Object({
      location: Type.String({ description: "City name or coordinates" }),
      units: Type.Optional(
        Type.Union([Type.Literal("celsius"), Type.Literal("fahrenheit")])
      ),
    }),
    handler: async (input) => {
      const apiKey = process.env.WEATHER_API_KEY;
      const units = input.units === "fahrenheit" ? "imperial" : "metric";
      
      const response = await fetch(
        `https://api.openweathermap.org/data/2.5/weather?q=${input.location}&units=${units}&appid=${apiKey}`
      );
      
      const data = await response.json();
      
      return {
        temperature: data.main.temp,
        conditions: data.weather[0].description,
        humidity: data.main.humidity,
        wind_speed: data.wind.speed,
      };
    },
  },
];

Database Tool

import { Type } from "@sinclair/typebox";
import Database from "better-sqlite3";

const db = new Database("~/.simpleclaw/my-skill.db");

// Initialize schema
db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
  )
`);

export const tools = [
  {
    name: "note_create",
    description: "Create a new note",
    input_schema: Type.Object({
      title: Type.String(),
      content: Type.String(),
    }),
    handler: async (input) => {
      const stmt = db.prepare(
        "INSERT INTO notes (title, content) VALUES (?, ?)"
      );
      const result = stmt.run(input.title, input.content);
      
      return {
        id: result.lastInsertRowid,
        message: `Created note: ${input.title}`,
      };
    },
  },

  {
    name: "note_search",
    description: "Search notes by keyword",
    input_schema: Type.Object({
      query: Type.String(),
    }),
    handler: async (input) => {
      const stmt = db.prepare(
        "SELECT * FROM notes WHERE title LIKE ? OR content LIKE ?"
      );
      const pattern = `%${input.query}%`;
      const notes = stmt.all(pattern, pattern);
      
      return { notes };
    },
  },
];

File System Tool

import { Type } from "@sinclair/typebox";
import { promises as fs } from "fs";
import path from "path";

export const tools = [
  {
    name: "file_backup",
    description: "Create a backup of a file",
    input_schema: Type.Object({
      filePath: Type.String({ description: "Path to file" }),
    }),
    handler: async (input) => {
      const backupDir = path.join(process.env.HOME!, ".simpleclaw", "backups");
      await fs.mkdir(backupDir, { recursive: true });
      
      const fileName = path.basename(input.filePath);
      const timestamp = new Date().toISOString().replace(/:/g, "-");
      const backupPath = path.join(backupDir, `${fileName}.${timestamp}.bak`);
      
      await fs.copyFile(input.filePath, backupPath);
      
      return {
        success: true,
        backupPath,
        message: `Backed up ${fileName}`,
      };
    },
  },
];

Skill Configuration

Skills can read configuration from ~/.simpleclaw/simpleclaw.json:
{
  "skills": {
    "my-skill": {
      "enabled": true,
      "apiKey": "your-api-key",
      "customOption": "value"
    }
  }
}
Access in your tool:
import { readFile } from "fs/promises";

async function getSkillConfig() {
  const configPath = `${process.env.HOME}/.simpleclaw/simpleclaw.json`;
  const data = await readFile(configPath, "utf-8");
  const config = JSON.parse(data);
  return config.skills?.["my-skill"] || {};
}

export const tools = [
  {
    name: "example_tool",
    handler: async (input) => {
      const config = await getSkillConfig();
      const apiKey = config.apiKey;
      // Use config...
    },
  },
];

Managing Skills

List Installed Skills

simpleclaw skills list

Install from ClawHub

simpleclaw skills install web-scraper

Update Skills

simpleclaw skills update --all

Disable a Skill

simpleclaw config set skills.my-skill.enabled false --json

Per-Agent Skills

Install skills for specific agents:
simpleclaw skills install typescript-expert --agent coding
Or in config:
{
  "agents": {
    "coding": {
      "skills": {
        "typescript-expert": { "enabled": true },
        "my-skill": { "enabled": false }
      }
    }
  }
}

Publishing Skills

Share your skills on ClawHub:
1

Create package.json

{
  "name": "@simpleclaw/skill-my-skill",
  "version": "1.0.0",
  "description": "Custom workflow tools",
  "main": "tools.js",
  "keywords": ["simpleclaw", "skill"],
  "simpleclaw": {
    "skill": true
  }
}
2

Test Locally

cd ~/.simpleclaw/workspace/skills/my-skill
npm pack
simpleclaw skills install ./simpleclaw-skill-my-skill-1.0.0.tgz
3

Publish to npm

npm login
npm publish --access public
4

Submit to ClawHub

Create a PR to ClawHub registry with your skill metadata.

Best Practices

Clear Descriptions

Write detailed tool descriptions and input schemas

Error Handling

Always catch errors and return helpful messages

Type Safety

Use TypeBox for input validation

Documentation

Include examples in SKILL.md

Troubleshooting

  1. Check skill directory exists:
    ls -la ~/.simpleclaw/workspace/skills/
    
  2. Verify SKILL.md format:
    cat ~/.simpleclaw/workspace/skills/my-skill/SKILL.md
    
  3. Check for TypeScript errors:
    cd ~/.simpleclaw/workspace/skills/my-skill
    npx tsc --noEmit
    
  1. Verify skill is enabled:
    simpleclaw config get skills.my-skill
    
  2. Check tool catalog:
    simpleclaw tools list
    
  3. Restart Gateway:
    simpleclaw gateway restart
    
  1. Check Gateway logs:
    simpleclaw gateway logs --tail 50 --level error
    
  2. Add debug logging to your tool:
    handler: async (input) => {
      console.log("Tool called with:", input);
      try {
        // ... tool logic
      } catch (error) {
        console.error("Tool error:", error);
        throw error;
      }
    }
    

Next Steps

Webhooks

Trigger skills from external events

Multi-Agent Setup

Different skills for different agents

Plugin SDK

Build advanced plugin extensions

Troubleshooting

Common skill issues

Build docs developers (and LLMs) love