Build Typed Agent Tools with defineToolPlugin and OpenClaw SDK
Learn how to create simple typed agent tools using defineToolPlugin and the openclaw plugins init, build, and validate commands. This page is for developers building plugins that exclusively provide agent callable tools.
Read this 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 creates a plugin that exclusively provides agent callable tools, without any channel, model provider, hook, service, or setup backend. The manifest metadata that OpenClaw requires to find tools without loading plugin runtime code is generated by this command.
For plugins that include providers, channels, hooks, services, or a combination of capabilities, refer to Building plugins, Channel Plugins, or Provider Plugins instead.
Requirements
- Node versions 22.22.3+, 24.15+, or 25.9+.
- TypeScript ESM package output.
typeboxinsidedependencies(not onlydevDependenciesbecause the generated plugin imports it during execution).openclaw >=2026.5.17, which is the earliest version that exposesopenclaw/plugin-sdk/tool-plugin.- A package root delivering
dist/,openclaw.plugin.json, andpackage.json.
Quickstart
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 generates the following structure:
| File | Purpose |
|---|---|
src/index.ts | defineToolPlugin entry containing a single echo tool |
src/index.test.ts | Metadata test that confirms the tool list |
tsconfig.json | NodeNext TypeScript output targeting dist/ |
vitest.config.ts | Vitest configuration for src/**/*.test.ts |
package.json | Scripts, runtime dependencies, openclaw.extensions: ["./dist/index.js"] |
openclaw.plugin.json | Generated manifest metadata for the first tool |
npm run plugin:build executes npm run build (tsc) and then openclaw plugins build --entry ./dist/index.js. npm run plugin:validate rebuilds and launches openclaw plugins validate --entry ./dist/index.js. When validation passes, the following output appears:
Plugin stock-quotes is valid.
Options for openclaw plugins init <id>:
| Flag | Default | Effect |
|---|---|---|
--directory <path> | <id> | Directory for output |
--name <name> | Title cased <id> | Name shown to users |
--type <type> | tool | Scaffold variant: tool or provider |
--force | off | Overwrite an existing output directory |
Write a tool
defineToolPlugin accepts a plugin identity, an optional configuration schema, and a fixed list of tools. The TypeBox schemas determine parameter and configuration types through inference.
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." }),
}),
outputSchema: Type.Object(
{
symbol: Type.String(),
configured: Type.Boolean(),
baseUrl: Type.String(),
},
{ additionalProperties: false },
),
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 represent the stable API. Choose names that are unique, lowercase, and sufficiently specific to prevent collisions with core tools or other plugins.
Optional and factory tools
Set optional: true when users must explicitly allowlist the tool before it reaches a model. openclaw plugins build generates the corresponding toolMetadata.<tool>.optional manifest entry, enabling OpenClaw to recognize the tool as 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 }),
});
Apply factory when a tool requires the runtime tool context before construction, such as opting out for a particular run, inspecting sandbox state, or attaching runtime helpers. The metadata remains static even though the actual 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 upfront. Use definePluginEntry directly when the plugin computes tool names dynamically or integrates tools with hooks, services, providers, or commands.
Return values
defineToolPlugin converts 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 the model should see formatted JSON and OpenClaw should preserve the original value in
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.
Output contracts
Attach outputSchema when a tool returns stable JSON-compatible data. It describes
the original value stored in AgentToolResult.details, not the formatted text
in content:
tool({
name: "shipment_list",
description: "List shipments.",
parameters: Type.Object({
buyer: Type.Optional(Type.String()),
}),
outputSchema: Type.Array(
Type.Object(
{
id: Type.String(),
buyer: Type.String(),
paid: Type.Boolean(),
tons: Type.Number(),
},
{ additionalProperties: false },
),
),
execute: ({ buyer }) => listShipments(buyer),
});
Code Mode and Tool Search convert this schema into a bounded TypeScript-style output hint. This allows a model to call and transform a known result in a single program rather than using another model turn to inspect its structure.
Before running a catalog call, OpenClaw compiles the schema. After tool hooks
execute, it validates the final details value before returning it through the
bridge. A tool cannot run with an invalid schema; a result mismatch causes the
completed call to fail. Include every non-throwing result variant, including
structured error variants, or leave out the schema when the result is
unpredictable. Do not place secrets or sensitive values in schema descriptions,
since trusted output metadata may become visible to the model.
Apply { additionalProperties: false } on object layers when you want a full
compact output hint; open or truncated schemas stay accessible through
tools.describe(...) but are not advertised as complete quick-index contracts.
Factory tools declare outputSchema on the concrete AnyAgentTool they
return. The static tool({ factory }) declaration does not accept a separate
output schema because it could diverge from the runtime tool.
Configuration
configSchema is optional. Leave it out 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 configuration from the plugin's entry in the Gateway config. Do not embed secrets in source or documentation examples; use config, environment variables, or SecretRefs according to 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:
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.
Package metadata
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.
Validate in CI
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
OpenClaw SDK compatibility fields carry TypeScript @deprecated annotations,
which editors surface as migration warnings. To enforce them in CI, enable a
type-aware rule such as
@typescript-eslint/no-deprecated.
Oxlint is not type-aware, so it cannot enforce these annotations. The generated
plugins init scaffold therefore does not add a deprecation lint config.
plugins validate checks that:
openclaw.plugin.jsonexists and passes the normal manifest loader.- The current entry exports
defineToolPluginmetadata. - Generated manifest fields match the entry metadata.
contracts.toolsmatches the declared tool names.package.jsonpointsopenclaw.extensionsat the selected runtime entry.
Install and inspect locally
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
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.
Troubleshooting
plugin entry not found: ./dist/index.js
The entry file you selected cannot be found. Execute npm run build first, then run
openclaw plugins build --entry ./dist/index.js or
openclaw plugins validate --entry ./dist/index.js again.
plugin entry does not expose defineToolPlugin metadata
No value created by defineToolPlugin was exported from the entry. Verify that the
module's default export matches the defineToolPlugin(...) result, or supply the
correct entry using --entry.
openclaw.plugin.json generated metadata is stale
The manifest and entry metadata are out of sync. Execute:
npm run build
openclaw plugins build --entry ./dist/index.js
Commit updates to both openclaw.plugin.json and package.json.
package.json openclaw.extensions must include ./dist/index.js
The package metadata points to a different runtime entry. Run
openclaw plugins build --entry ./dist/index.js so the generator synchronizes
package metadata with the entry you plan to ship.
Cannot find package 'typebox'
The built plugin imports typebox at runtime. Leave it inside dependencies,
reinstall, rebuild, and run validation again.
Tool does not appear after install
Inspect these items in sequence:
openclaw plugins inspect <plugin-id> --runtimeopenclaw plugins validate --root <plugin-root> --entry ./dist/index.jsopenclaw.plugin.jsoncontainscontracts.toolswith the expected tool names.package.jsonincludesopenclaw.extensions: ["./dist/index.js"].- The Gateway was restarted or reloaded once the plugin was installed.