docs/plugins/tool-plugins.md
defineToolPlugin builds a plugin that only adds agent-callable tools: no
channel, model provider, hook, service, or setup backend. It generates the
manifest metadata OpenClaw needs to discover tools without loading plugin
runtime code.
For provider, channel, hook, service, or mixed-capability plugins, start with Building plugins, Channel Plugins, or Provider Plugins instead.
typebox in dependencies (not just devDependencies - the generated
plugin imports it at runtime).openclaw >=2026.5.17, the first version that exports
openclaw/plugin-sdk/tool-plugin.dist/, openclaw.plugin.json, and
package.json.openclaw plugins init stock-quotes --name "Stock Quotes"
cd stock-quotes
npm install
npm run plugin:build
npm run plugin:validate
npm test
plugins init scaffolds:
| File | Purpose |
|---|---|
src/index.ts | defineToolPlugin entry with one echo tool |
src/index.test.ts | Metadata test asserting the tool list |
tsconfig.json | NodeNext TypeScript output to dist/ |
vitest.config.ts | Vitest config for src/**/*.test.ts |
package.json | Scripts, runtime deps, openclaw.extensions: ["./dist/index.js"] |
openclaw.plugin.json | Generated manifest metadata for the initial tool |
npm run plugin:build runs npm run build (tsc) then
openclaw plugins build --entry ./dist/index.js. npm run plugin:validate
rebuilds and runs openclaw plugins validate --entry ./dist/index.js.
Successful validation prints:
Plugin stock-quotes is valid.
openclaw plugins init <id> options:
| Flag | Default | Effect |
|---|---|---|
--directory <path> | <id> | Output directory |
--name <name> | Title-cased <id> | Display name |
--type <type> | tool | Scaffold type: tool or provider |
--force | off | Overwrite an existing output directory |
defineToolPlugin takes plugin identity, an optional config schema, and a
static list of tools. Parameter and config types are inferred from the
TypeBox schemas.
import { Type } from "typebox";
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
export default defineToolPlugin({
id: "stock-quotes",
name: "Stock Quotes",
description: "Fetch stock quote snapshots.",
configSchema: Type.Object({
apiKey: Type.Optional(Type.String({ description: "Quote API key." })),
baseUrl: Type.Optional(Type.String({ description: "Quote API base URL." })),
}),
tools: (tool) => [
tool({
name: "stock_quote",
label: "Stock Quote",
description: "Fetch a stock quote snapshot.",
parameters: Type.Object({
symbol: Type.String({ description: "Ticker symbol, for example OPEN." }),
}),
async execute({ symbol }, config, context) {
context.signal?.throwIfAborted();
return {
symbol: symbol.toUpperCase(),
configured: Boolean(config.apiKey),
baseUrl: config.baseUrl ?? "https://api.example.com",
};
},
}),
],
});
Tool names are the stable API. Pick names that are unique, lowercase, and specific enough to avoid collisions with core tools or other plugins.
Set optional: true when users should explicitly allowlist the tool before it
is sent to a model. openclaw plugins build writes the matching
toolMetadata.<tool>.optional manifest entry, so OpenClaw can see that the
tool is optional without loading plugin runtime code.
tool({
name: "workflow_run",
description: "Run an external workflow.",
parameters: Type.Object({ goal: Type.String() }),
optional: true,
execute: ({ goal }) => ({ queued: true, goal }),
});
Use factory when a tool needs the runtime tool context before it can be
created - to opt out for a specific run, inspect sandbox state, or bind
runtime helpers. Metadata stays static even though the concrete tool is built
at runtime.
tool({
name: "local_workflow",
description: "Run a local workflow outside sandboxed sessions.",
parameters: Type.Object({ goal: Type.String() }),
optional: true,
factory({ api, toolContext }) {
if (toolContext.sandboxed) {
return null;
}
return createLocalWorkflowTool(api);
},
});
Factories still declare a fixed tool name up front. Use definePluginEntry
directly when the plugin computes tool names dynamically or combines tools
with hooks, services, providers, or commands.
defineToolPlugin wraps plain return values into the OpenClaw tool-result
format:
details.tool({
name: "echo_text",
description: "Echo input text.",
parameters: Type.Object({
input: Type.String(),
}),
execute: ({ input }) => input,
});
tool({
name: "echo_json",
description: "Echo input as structured JSON.",
parameters: Type.Object({
input: Type.String(),
}),
execute: ({ input }) => ({ input, length: input.length }),
});
Use a factory tool when you need a custom AgentToolResult or want to reuse an
existing api.registerTool implementation.
configSchema is optional. Omit it and OpenClaw applies a strict empty object
schema; the generated manifest still includes configSchema.
export default defineToolPlugin({
id: "no-config-tools",
name: "No Config Tools",
description: "Adds tools that do not need configuration.",
tools: () => [],
});
With a configSchema, the second execute argument is typed from it:
const configSchema = Type.Object({
apiKey: Type.String(),
});
export default defineToolPlugin({
id: "configured-tools",
name: "Configured Tools",
description: "Adds configured tools.",
configSchema,
tools: (tool) => [
tool({
name: "configured_ping",
description: "Check whether configuration is available.",
parameters: Type.Object({}),
execute: (_params, config) => ({ hasKey: config.apiKey.length > 0 }),
}),
],
});
OpenClaw reads plugin config from the plugin's entry in the Gateway config. Do not hard-code secrets in source or docs examples; use config, environment variables, or SecretRefs per the plugin's security model.
OpenClaw must read the plugin manifest before importing plugin runtime code.
defineToolPlugin exposes static metadata for this, and
openclaw plugins build writes it into the package. Rerun the generator after
changing plugin id, name, description, config schema, activation, or tool
names:
npm run build
openclaw plugins build --entry ./dist/index.js
Generated manifest for a one-tool plugin:
{
"id": "stock-quotes",
"name": "Stock Quotes",
"description": "Fetch stock quote snapshots.",
"version": "0.1.0",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
},
"activation": {
"onStartup": true
},
"contracts": {
"tools": ["stock_quote"]
}
}
contracts.tools is the important discovery contract: it tells OpenClaw which
plugin owns each tool without loading every installed plugin's runtime. A
stale manifest means a tool can go missing from discovery, or a registration
error gets blamed on the wrong plugin.
openclaw plugins build also aligns package.json to the selected runtime
entry:
{
"type": "module",
"files": ["dist", "openclaw.plugin.json", "README.md"],
"dependencies": {
"typebox": "^1.1.38"
},
"peerDependencies": {
"openclaw": ">=2026.5.17"
},
"openclaw": {
"extensions": ["./dist/index.js"]
}
}
Ship built JavaScript (./dist/index.js), not a TypeScript source entry.
Source entries only work for workspace-local development.
plugins build --check fails without rewriting files when generated metadata
is stale:
npm run build
openclaw plugins build --entry ./dist/index.js --check
openclaw plugins validate --entry ./dist/index.js
npm test
plugins validate checks that:
openclaw.plugin.json exists and passes the normal manifest loader.defineToolPlugin metadata.contracts.tools matches the declared tool names.package.json points openclaw.extensions at the selected runtime entry.From a separate OpenClaw checkout or installed CLI, install the package path:
openclaw plugins install ./stock-quotes
openclaw plugins inspect stock-quotes --runtime
For a packaged smoke test, pack first and install the tarball:
npm pack
openclaw plugins install npm-pack:./openclaw-plugin-stock-quotes-0.1.0.tgz
openclaw plugins inspect stock-quotes --runtime --json
After installing, restart or reload the Gateway and ask the agent to use the tool. If the tool is not visible, inspect the plugin runtime and the effective tool catalog before changing code (see Troubleshooting).
Publish through ClawHub once the package is ready. clawhub package publish
takes a source: a local folder, a GitHub repo (owner/repo[@ref]), or a
tarball URL.
clawhub package publish ./stock-quotes --dry-run
clawhub package publish ./stock-quotes
Install with an explicit ClawHub locator:
openclaw plugins install clawhub:your-org/stock-quotes
Bare npm package specs still install from npm during the launch cutover, but ClawHub is the preferred discovery and distribution surface for OpenClaw plugins. See ClawHub publishing for owner scope and release review.
plugin entry not found: ./dist/index.jsThe selected entry file does not exist. Run npm run build, then rerun
openclaw plugins build --entry ./dist/index.js or
openclaw plugins validate --entry ./dist/index.js.
plugin entry does not expose defineToolPlugin metadataThe entry did not export a value created by defineToolPlugin. Confirm the
module's default export is the defineToolPlugin(...) result, or pass the
correct entry with --entry.
openclaw.plugin.json generated metadata is staleThe manifest no longer matches the entry metadata. Run:
npm run build
openclaw plugins build --entry ./dist/index.js
Commit both openclaw.plugin.json and package.json changes.
package.json openclaw.extensions must include ./dist/index.jsThe package metadata points at a different runtime entry. Run
openclaw plugins build --entry ./dist/index.js so the generator aligns
package metadata with the entry you intend to ship.
Cannot find package 'typebox'The built plugin imports typebox at runtime. Keep it in dependencies,
reinstall, rebuild, and rerun validation.
Check these in order:
openclaw plugins inspect <plugin-id> --runtimeopenclaw plugins validate --root <plugin-root> --entry ./dist/index.jsopenclaw.plugin.json has contracts.tools with the expected tool names.package.json has openclaw.extensions: ["./dist/index.js"].