back to docs
ai

Custom Plugins

Build and load custom plugins to extend OpenClaw with your own tools

OpenClaw supports custom plugins, TypeScript modules that register new tools the agent can call. This guide documents how to build one correctly, using a self-hosted SearXNG instance as the example.

1. Set up SearXNG with Docker

SearXNG is a self-hosted meta search engine. This section covers running it locally with Docker Compose and Redis.

a. Create the directories

mkdir -p ~/searxng/config ~/searxng/data
cd ~/searxng

b. docker-compose.yml

services:
  searxng:
    image: docker.io/searxng/searxng:latest
    container_name: searxng
    restart: unless-stopped
    ports:
      - "127.0.0.1:8888:8080"
    volumes:
      - ./config:/etc/searxng
      - ./data:/var/cache/searxng
    environment:
      FORCE_OWNERSHIP: "true"
      SEARXNG_VALKEY_URL: valkey://redis:6379/0

  redis:
    image: redis:7-alpine
    container_name: searxng-redis
    restart: unless-stopped
    ports:
      - "127.0.0.1:6379:6379"

c. config/limiter.toml

Create this file to disable bot detection. Otherwise the API will block requests from the plugin:

nano ~/searxng/config/limiter.toml
[botdetection.ip_limit]
link_token = true

d. Start the containers

docker compose up -d

On first run, SearXNG generates a config/settings.yml automatically. You can leave it as-is or customize it later.

Verify it’s running:

curl "http://127.0.0.1:8888/search?q=test&format=json"
JSON format must be enabled

If the curl above returns an error about format=json not being allowed, open config/settings.yml and find the search section, then add json to the formats list:

search:
  formats:
    - html
    - json

Restart the containers after saving.

2. Plugin structure

A plugin lives in its own directory with three files:

~/.openclaw/plugins/my-plugin/
├── index.ts
├── package.json
└── openclaw.plugin.json

a. openclaw.plugin.json

The plugin manifest. The configSchema field is required. The gateway rejects any plugin without it.

{
  "id": "my-plugin",
  "name": "My Plugin",
  "description": "What this plugin does",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  }
}

b. package.json

{
  "name": "my-plugin",
  "version": "1.0.0",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"]
  }
}

c. index.ts

Plugins export a default object with an id, metadata, and a register function. The execute return format must be { content: [...], details: {...} }, not a plain array.

const plugin = {
  id: "my-plugin",
  name: "My Plugin",
  description: "What this plugin does",
  configSchema: { type: "object", additionalProperties: false, properties: {} },

  register(api: any) {
    api.registerTool({
      name: "my_tool",
      label: "My Tool",
      description: "What this tool does",
      parameters: {
        type: "object",
        properties: {
          query: { type: "string", description: "Input for the tool" },
        },
        required: ["query"],
      },

      async execute(toolCallId: string, params: { query: string }) {
        // ... your logic here

        return {
          content: [{ type: "text", text: "Result text here" }],
          details: { query: params.query },
        };
      },
    });
  },
};

export default plugin;
Common mistakes
  • File must be .ts, not .js. OpenClaw transpiles TypeScript at load time
  • Export must be a default object, not export default function register(api)
  • execute must return { content, details }, not a plain array
  • configSchema is mandatory in both the manifest and the plugin object

3. Install the plugin

Use the --link flag so OpenClaw points directly to your source directory instead of copying files. This avoids a timing issue where config validation runs before the directory scan completes.

openclaw plugins install --link ~/.openclaw/plugins/my-plugin

Restart the gateway:

openclaw gateway restart

Verify the plugin loaded:

openclaw plugins list

4. Enable plugin tools in the agent

The agent doesn’t include plugin tools by default. Add them via tools.alsoAllow in ~/.openclaw/openclaw.json:

{
  "tools": {
    "profile": "coding",
    "alsoAllow": ["group:plugins"]
  }
}

Restart the gateway again. Your tool should now appear when the agent runs.

5. Live reloading

Because you installed with --link, OpenClaw reads directly from the source directory. Any changes to index.ts take effect after a gateway restart. No reinstall needed.

openclaw gateway restart

6. Real example: SearXNG search tool

The built-in web search only supports Brave, Perplexity, Grok, Gemini, and Kimi. This plugin adds a self-hosted SearXNG instance as a search tool.

a. openclaw.plugin.json

{
  "id": "searxng",
  "name": "SearXNG Web Search",
  "description": "Web search via self-hosted SearXNG instance at 127.0.0.1:8888",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  }
}

b. package.json

{
  "name": "searxng",
  "version": "1.0.0",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"]
  }
}

c. index.ts

const BASE_URL = "http://127.0.0.1:8888";

const plugin = {
  id: "searxng",
  name: "SearXNG Web Search",
  description: "Web search via self-hosted SearXNG instance at 127.0.0.1:8888",
  configSchema: { type: "object", additionalProperties: false, properties: {} },

  register(api: any) {
    api.registerTool({
      name: "searxng_search",
      label: "SearXNG Search",
      description: "Search the web using the self-hosted SearXNG instance.",
      parameters: {
        type: "object",
        properties: {
          query: { type: "string", description: "The search query" },
          count: { type: "number", description: "Number of results (1-10, default 5)" },
        },
        required: ["query"],
      },

      async execute(toolCallId: string, params: { query: string; count?: number }) {
        const count = params.count ?? 5;
        const urlParams = new URLSearchParams({
          q: params.query,
          format: "json",
          categories: "general",
        });

        const res = await fetch(`${BASE_URL}/search?${urlParams}`, {
          signal: AbortSignal.timeout(15000),
        });

        if (!res.ok) {
          return {
            content: [{ type: "text", text: `SearXNG error: HTTP ${res.status}` }],
            details: { error: res.status },
          };
        }

        const data = await res.json() as {
          results?: Array<{ title: string; url: string; content?: string }>;
        };
        const results = (data.results ?? []).slice(0, count);

        if (results.length === 0) {
          return {
            content: [{ type: "text", text: `No results found for: ${params.query}` }],
            details: { query: params.query, count: 0 },
          };
        }

        const formatted = results
          .map((r, i) => `[${i + 1}] ${r.title}\nURL: ${r.url}\n${r.content ?? ""}`)
          .join("\n\n");

        return {
          content: [{ type: "text", text: `Search results for "${params.query}":\n\n${formatted}` }],
          details: { query: params.query, count: results.length },
        };
      },
    });
  },
};

export default plugin;

d. Install

openclaw plugins install --link ~/.openclaw/plugins/searxng
openclaw gateway restart

Add alsoAllow to openclaw.json:

{
  "tools": {
    "alsoAllow": ["group:plugins"]
  }
}

Restart once more and searxng_search will appear as an available tool in the agent.

7. Verification

Once the plugin is loaded and alsoAllow is set, you can test it by asking the agent to search for something:

openclaw agent --agent main \
  --message "Use searxng_search to find 'AI trends 2026'" \
  --json
SearXNG returning live web results inside the agent
Command Palette
Search for a command to run...