mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-06 19:32:58 +08:00
Source-grounded rewrite of 529 published docs pages with per-unit information-loss verification: 1,713 factual corrections cited to src/**, generated surfaces regenerated, frontmatter titles preserved for i18n, release notes pages untouched. All docs gates green. Closes #100141
410 lines
13 KiB
Markdown
410 lines
13 KiB
Markdown
---
|
|
summary: "Build simple typed agent tools with defineToolPlugin and openclaw plugins init/build/validate"
|
|
title: "Tool plugins"
|
|
sidebarTitle: "Tool Plugins"
|
|
read_when:
|
|
- You want to build a simple OpenClaw plugin that only adds agent tools
|
|
- You want to use defineToolPlugin instead of hand-writing plugin manifest metadata
|
|
- You need to scaffold, generate, validate, test, or publish a tool-only plugin
|
|
---
|
|
|
|
`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](/plugins/building-plugins), [Channel Plugins](/plugins/sdk-channel-plugins),
|
|
or [Provider Plugins](/plugins/sdk-provider-plugins) instead.
|
|
|
|
## Requirements
|
|
|
|
- Node 22.19+, Node 23.11+, or Node 24+.
|
|
- TypeScript ESM package output.
|
|
- `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`.
|
|
- A package root that ships `dist/`, `openclaw.plugin.json`, and
|
|
`package.json`.
|
|
|
|
## Quickstart
|
|
|
|
```bash
|
|
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:
|
|
|
|
```text
|
|
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 |
|
|
|
|
## Write a tool
|
|
|
|
`defineToolPlugin` takes plugin identity, an optional config schema, and a
|
|
static list of tools. Parameter and config types are inferred from the
|
|
TypeBox schemas.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
## Optional and factory tools
|
|
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
## Return values
|
|
|
|
`defineToolPlugin` wraps plain return values into the OpenClaw tool-result
|
|
format:
|
|
|
|
- Return a string when the model should see that exact text.
|
|
- Return a JSON-compatible value when you want the model to see formatted JSON
|
|
and OpenClaw to keep the original value in `details`.
|
|
|
|
```typescript
|
|
tool({
|
|
name: "echo_text",
|
|
description: "Echo input text.",
|
|
parameters: Type.Object({
|
|
input: Type.String(),
|
|
}),
|
|
execute: ({ input }) => input,
|
|
});
|
|
```
|
|
|
|
```typescript
|
|
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.
|
|
|
|
## Configuration
|
|
|
|
`configSchema` is optional. Omit it and OpenClaw applies a strict empty object
|
|
schema; the generated manifest still includes `configSchema`.
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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.
|
|
|
|
## Generated metadata
|
|
|
|
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:
|
|
|
|
```bash
|
|
npm run build
|
|
openclaw plugins build --entry ./dist/index.js
|
|
```
|
|
|
|
Generated manifest for a one-tool plugin:
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
## Package metadata
|
|
|
|
`openclaw plugins build` also aligns `package.json` to the selected runtime
|
|
entry:
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
## Validate in CI
|
|
|
|
`plugins build --check` fails without rewriting files when generated metadata
|
|
is stale:
|
|
|
|
```bash
|
|
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.
|
|
- The current entry exports `defineToolPlugin` metadata.
|
|
- Generated manifest fields match the entry metadata.
|
|
- `contracts.tools` matches the declared tool names.
|
|
- `package.json` points `openclaw.extensions` at the selected runtime entry.
|
|
|
|
## Install and inspect locally
|
|
|
|
From a separate OpenClaw checkout or installed CLI, install the package path:
|
|
|
|
```bash
|
|
openclaw plugins install ./stock-quotes
|
|
openclaw plugins inspect stock-quotes --runtime
|
|
```
|
|
|
|
For a packaged smoke test, pack first and install the tarball:
|
|
|
|
```bash
|
|
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](#troubleshooting)).
|
|
|
|
## Publish
|
|
|
|
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.
|
|
|
|
```bash
|
|
clawhub package publish ./stock-quotes --dry-run
|
|
clawhub package publish ./stock-quotes
|
|
```
|
|
|
|
Install with an explicit ClawHub locator:
|
|
|
|
```bash
|
|
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](/clawhub/publishing) for owner scope and
|
|
release review.
|
|
|
|
## Troubleshooting
|
|
|
|
### `plugin entry not found: ./dist/index.js`
|
|
|
|
The 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 metadata`
|
|
|
|
The 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 stale`
|
|
|
|
The manifest no longer matches the entry metadata. Run:
|
|
|
|
```bash
|
|
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.js`
|
|
|
|
The 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.
|
|
|
|
### Tool does not appear after install
|
|
|
|
Check these in order:
|
|
|
|
1. `openclaw plugins inspect <plugin-id> --runtime`
|
|
2. `openclaw plugins validate --root <plugin-root> --entry ./dist/index.js`
|
|
3. `openclaw.plugin.json` has `contracts.tools` with the expected tool names.
|
|
4. `package.json` has `openclaw.extensions: ["./dist/index.js"]`.
|
|
5. The Gateway was restarted or reloaded after installing the plugin.
|
|
|
|
## See also
|
|
|
|
- [Building plugins](/plugins/building-plugins)
|
|
- [Plugin entry points](/plugins/sdk-entrypoints)
|
|
- [Plugin SDK subpaths](/plugins/sdk-subpaths)
|
|
- [Plugin manifest](/plugins/manifest)
|
|
- [Plugins CLI](/cli/plugins)
|
|
- [ClawHub publishing](/clawhub/publishing)
|