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" 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; - File must be
.ts, not.js. OpenClaw transpiles TypeScript at load time - Export must be a default object, not
export default function register(api) executemust return{ content, details }, not a plain arrayconfigSchemais 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