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.
  • typebox inside dependencies (not only devDependencies because the generated plugin imports it during execution).
  • openclaw >=2026.5.17, which is the earliest version that exposes openclaw/plugin-sdk/tool-plugin.
  • A package root delivering dist/, openclaw.plugin.json, and package.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:

FilePurpose
src/index.tsdefineToolPlugin entry containing a single echo tool
src/index.test.tsMetadata test that confirms the tool list
tsconfig.jsonNodeNext TypeScript output targeting dist/
vitest.config.tsVitest configuration for src/**/*.test.ts
package.jsonScripts, runtime dependencies, openclaw.extensions: ["./dist/index.js"]
openclaw.plugin.jsonGenerated 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>:

FlagDefaultEffect
--directory <path><id>Directory for output
--name <name>Title cased <id>Name shown to users
--type <type>toolScaffold variant: tool or provider
--forceoffOverwrite 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.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:

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:

  1. openclaw plugins inspect <plugin-id> --runtime
  2. openclaw plugins validate --root <plugin-root> --entry ./dist/index.js
  3. openclaw.plugin.json contains contracts.tools with the expected tool names.
  4. package.json includes openclaw.extensions: ["./dist/index.js"].
  5. The Gateway was restarted or reloaded once the plugin was installed.

See also