Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
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)
|