{"id":622,"date":"2026-03-21T22:52:57","date_gmt":"2026-03-21T14:52:57","guid":{"rendered":"https:\/\/pa.yingzhi8.cn\/index.php\/2026\/03\/21\/plugins-building-plugins\/"},"modified":"2026-03-21T23:08:56","modified_gmt":"2026-03-21T15:08:56","slug":"plugins-building-plugins","status":"publish","type":"post","link":"https:\/\/pa.yingzhi8.cn\/index.php\/2026\/03\/21\/plugins-building-plugins\/","title":{"rendered":"Building Plugins"},"content":{"rendered":"<h1>Building Plugins<\/h1>\n<p>Plugins extend OpenClaw with new capabilities: channels, model providers, speech,<br \/>\nimage generation, web search, agent tools, or any combination. A single plugin<br \/>\ncan register multiple capabilities.<\/p>\n<p>OpenClaw encourages <strong>external plugin development<\/strong>. You do not need to add your<br \/>\nplugin to the OpenClaw repository. Publish your plugin on npm, and users install<br \/>\nit with <code>openclaw plugins install &lt;npm-spec&gt;<\/code>. OpenClaw also maintains a set of<br \/>\ncore plugins in-repo, but the plugin system is designed for independent ownership<br \/>\nand distribution.<\/p>\n<h2>Prerequisites<\/h2>\n<ul>\n<li>Node &gt;= 22 and a package manager (npm or pnpm)<\/li>\n<li>Familiarity with TypeScript (ESM)<\/li>\n<li>For in-repo plugins: OpenClaw repository cloned and <code>pnpm install<\/code> done<\/li>\n<\/ul>\n<h2>Plugin capabilities<\/h2>\n<p>A plugin can register one or more capabilities. The capability you register<br \/>\ndetermines what your plugin provides to OpenClaw:<\/p>\n<table>\n<thead>\n<tr>\n<th>Capability<\/th>\n<th>Registration method<\/th>\n<th>What it adds<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Text inference<\/td>\n<td><code>api.registerProvider(...)<\/code><\/td>\n<td>Model provider (LLM)<\/td>\n<\/tr>\n<tr>\n<td>Channel \/ messaging<\/td>\n<td><code>api.registerChannel(...)<\/code><\/td>\n<td>Chat channel (e.g. Slack, IRC)<\/td>\n<\/tr>\n<tr>\n<td>Speech<\/td>\n<td><code>api.registerSpeechProvider(...)<\/code><\/td>\n<td>Text-to-speech \/ STT<\/td>\n<\/tr>\n<tr>\n<td>Media understanding<\/td>\n<td><code>api.registerMediaUnderstandingProvider(...)<\/code><\/td>\n<td>Image\/audio\/video analysis<\/td>\n<\/tr>\n<tr>\n<td>Image generation<\/td>\n<td><code>api.registerImageGenerationProvider(...)<\/code><\/td>\n<td>Image generation<\/td>\n<\/tr>\n<tr>\n<td>Web search<\/td>\n<td><code>api.registerWebSearchProvider(...)<\/code><\/td>\n<td>Web search provider<\/td>\n<\/tr>\n<tr>\n<td>Agent tools<\/td>\n<td><code>api.registerTool(...)<\/code><\/td>\n<td>Tools callable by the agent<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>A plugin that registers zero capabilities but provides hooks or services is a<br \/>\n<strong>hook-only<\/strong> plugin. That pattern is still supported.<\/p>\n<h2>Plugin structure<\/h2>\n<p>Plugins follow this layout (whether in-repo or standalone):<\/p>\n<pre><code>my-plugin\/\n\u251c\u2500\u2500 package.json          # npm metadata + openclaw config\n\u251c\u2500\u2500 openclaw.plugin.json  # Plugin manifest\n\u251c\u2500\u2500 index.ts              # Entry point\n\u251c\u2500\u2500 setup-entry.ts        # Setup wizard (optional)\n\u251c\u2500\u2500 api.ts                # Public exports (optional)\n\u251c\u2500\u2500 runtime-api.ts        # Internal exports (optional)\n\u2514\u2500\u2500 src\/\n    \u251c\u2500\u2500 provider.ts       # Capability implementation\n    \u251c\u2500\u2500 runtime.ts        # Runtime wiring\n    \u2514\u2500\u2500 *.test.ts         # Colocated tests\n<\/code><\/pre>\n<h2>Create a plugin<\/h2>\n<p>    Create <code>package.json<\/code> with the <code>openclaw<\/code> metadata block. The structure<br \/>\n    depends on what capabilities your plugin provides.<\/p>\n<pre><code>**Channel plugin example:**\n\n```json  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\n{\n  \"name\": \"@myorg\/openclaw-my-channel\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"openclaw\": {\n    \"extensions\": [\".\/index.ts\"],\n    \"channel\": {\n      \"id\": \"my-channel\",\n      \"label\": \"My Channel\",\n      \"blurb\": \"Short description of the channel.\"\n    }\n  }\n}\n```\n\n**Provider plugin example:**\n\n```json  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\n{\n  \"name\": \"@myorg\/openclaw-my-provider\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"openclaw\": {\n    \"extensions\": [\".\/index.ts\"],\n    \"providers\": [\"my-provider\"]\n  }\n}\n```\n\nThe `openclaw` field tells the plugin system what your plugin provides.\nA plugin can declare both `channel` and `providers` if it provides multiple\ncapabilities.\n<\/code><\/pre>\n<\/p>\n<p>\n    The entry point registers your capabilities with the plugin API.<\/p>\n<pre><code>**Channel plugin:**\n\n```typescript  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\nimport { defineChannelPluginEntry } from \"openclaw\/plugin-sdk\/core\";\n\nexport default defineChannelPluginEntry({\n  id: \"my-channel\",\n  name: \"My Channel\",\n  description: \"Connects OpenClaw to My Channel\",\n  plugin: {\n    \/\/ Channel adapter implementation\n  },\n});\n```\n\n**Provider plugin:**\n\n```typescript  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\nimport { definePluginEntry } from \"openclaw\/plugin-sdk\/core\";\n\nexport default definePluginEntry({\n  id: \"my-provider\",\n  name: \"My Provider\",\n  register(api) {\n    api.registerProvider({\n      \/\/ Provider implementation\n    });\n  },\n});\n```\n\n**Multi-capability plugin** (provider + tool):\n\n```typescript  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\nimport { definePluginEntry } from \"openclaw\/plugin-sdk\/core\";\n\nexport default definePluginEntry({\n  id: \"my-plugin\",\n  name: \"My Plugin\",\n  register(api) {\n    api.registerProvider({ \/* ... *\/ });\n    api.registerTool({ \/* ... *\/ });\n    api.registerImageGenerationProvider({ \/* ... *\/ });\n  },\n});\n```\n\nUse `defineChannelPluginEntry` for channel plugins and `definePluginEntry`\nfor everything else. A single plugin can register as many capabilities as needed.\n<\/code><\/pre>\n<\/p>\n<p>\n    Always import from specific <code>openclaw\/plugin-sdk\/&lt;subpath&gt;<\/code> paths. The old<br \/>\n    monolithic import is deprecated (see <a href=\"\/plugins\/sdk-migration\">SDK Migration<\/a>).<\/p>\n<pre><code>If older plugin code still imports `openclaw\/extension-api`, treat that as a\ntemporary compatibility bridge only. New code should use injected runtime\nhelpers such as `api.runtime.agent.*` instead of importing host-side agent\nhelpers directly.\n\n```typescript  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\n\/\/ Correct: focused subpaths\nimport { definePluginEntry } from \"openclaw\/plugin-sdk\/core\";\nimport { createPluginRuntimeStore } from \"openclaw\/plugin-sdk\/runtime-store\";\nimport { buildOauthProviderAuthResult } from \"openclaw\/plugin-sdk\/provider-oauth\";\n\n\/\/ Wrong: monolithic root (lint will reject this)\nimport { ... } from \"openclaw\/plugin-sdk\";\n\n\/\/ Deprecated: legacy host bridge\nimport { runEmbeddedPiAgent } from \"openclaw\/extension-api\";\n```\n\n&lt;Accordion title=\"Common subpaths reference\"&gt;\n  | Subpath                             | Purpose                                 |\n  | ----------------------------------- | --------------------------------------- |\n  | `plugin-sdk\/core`                   | Plugin entry definitions and base types |\n  | `plugin-sdk\/channel-setup`          | Setup wizard adapters                   |\n  | `plugin-sdk\/channel-pairing`        | DM pairing primitives                   |\n  | `plugin-sdk\/channel-reply-pipeline` | Reply prefix + typing wiring            |\n  | `plugin-sdk\/channel-config-schema`  | Config schema builders                  |\n  | `plugin-sdk\/channel-policy`         | Group\/DM policy helpers                 |\n  | `plugin-sdk\/secret-input`           | Secret input parsing\/helpers            |\n  | `plugin-sdk\/webhook-ingress`        | Webhook request\/target helpers          |\n  | `plugin-sdk\/runtime-store`          | Persistent plugin storage               |\n  | `plugin-sdk\/allow-from`             | Allowlist resolution                    |\n  | `plugin-sdk\/reply-payload`          | Message reply types                     |\n  | `plugin-sdk\/provider-oauth`         | OAuth login + PKCE helpers              |\n  | `plugin-sdk\/provider-onboard`       | Provider onboarding config patches      |\n  | `plugin-sdk\/testing`                | Test utilities                          |\n&lt;\/Accordion&gt;\n\nUse the narrowest subpath that matches the job.\n<\/code><\/pre>\n<\/p>\n<p>\n    Within your plugin, create local module files for internal code sharing<br \/>\n    instead of re-importing through the plugin SDK:<\/p>\n<pre><code>```typescript  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\n\/\/ api.ts \u2014 public exports for this plugin\nexport { MyConfig } from \".\/src\/config.js\";\nexport { MyRuntime } from \".\/src\/runtime.js\";\n\n\/\/ runtime-api.ts \u2014 internal-only exports\nexport { internalHelper } from \".\/src\/helpers.js\";\n```\n\n&lt;Warning&gt;\n  Never import your own plugin back through its published SDK path from\n  production files. Route internal imports through local files like `.\/api.ts`\n  or `.\/runtime-api.ts`. The SDK path is for external consumers only.\n&lt;\/Warning&gt;\n<\/code><\/pre>\n<\/p>\n<p>\n    Create <code>openclaw.plugin.json<\/code> in your plugin root:<\/p>\n<pre><code>```json  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\n{\n  \"id\": \"my-plugin\",\n  \"kind\": \"provider\",\n  \"name\": \"My Plugin\",\n  \"description\": \"Adds My Provider to OpenClaw\"\n}\n```\n\nFor channel plugins, set `\"kind\": \"channel\"` and add `\"channels\": [\"my-channel\"]`.\n\nSee [Plugin Manifest](\/plugins\/manifest) for the full schema.\n<\/code><\/pre>\n<\/p>\n<p>\n    <strong>External plugins:<\/strong> run your own test suite against the plugin SDK contracts.<\/p>\n<pre><code>**In-repo plugins:** OpenClaw runs contract tests against all registered plugins:\n\n```bash  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\npnpm test:contracts:channels   # channel plugins\npnpm test:contracts:plugins    # provider plugins\n```\n\nFor unit tests, import test helpers from the testing surface:\n\n```typescript  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\nimport { createTestRuntime } from \"openclaw\/plugin-sdk\/testing\";\n```\n<\/code><\/pre>\n<\/p>\n<p>\n    <strong>External plugins:<\/strong> publish to npm, then install:<\/p>\n<pre><code>```bash  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\nnpm publish\nopenclaw plugins install @myorg\/openclaw-my-plugin\n```\n\n**In-repo plugins:** place the plugin under `extensions\/` and it is\nautomatically discovered during build.\n\nUsers can browse and install community plugins with:\n\n```bash  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}\nopenclaw plugins search &lt;query&gt;\nopenclaw plugins install &lt;npm-spec&gt;\n```\n<\/code><\/pre>\n<\/p>\n<h2>Registering agent tools<\/h2>\n<p>Plugins can register <strong>agent tools<\/strong> \u2014 typed functions the LLM can call. Tools<br \/>\ncan be required (always available) or optional (users opt in via allowlists).<\/p>\n<p>&#8220;`typescript  theme={&#8220;theme&#8221;:{&#8220;light&#8221;:&#8221;min-light&#8221;,&#8221;dark&#8221;:&#8221;min-dark&#8221;}}<br \/>\nimport { Type } from &#8220;@sinclair\/typebox&#8221;;<\/p>\n<p>export default definePluginEntry({<br \/>\n  id: &#8220;my-plugin&#8221;,<br \/>\n  name: &#8220;My Plugin&#8221;,<br \/>\n  register(api) {<br \/>\n    \/\/ Required tool (always available)<br \/>\n    api.registerTool({<br \/>\n      name: &#8220;my_tool&#8221;,<br \/>\n      description: &#8220;Do a thing&#8221;,<br \/>\n      parameters: Type.Object({ input: Type.String() }),<br \/>\n      async execute(_id, params) {<br \/>\n        return { content: [{ type: &#8220;text&#8221;, text: params.input }] };<br \/>\n      },<br \/>\n    });<\/p>\n<pre><code>\/\/ Optional tool (user must add to allowlist)\napi.registerTool(\n  {\n    name: \"workflow_tool\",\n    description: \"Run a workflow\",\n    parameters: Type.Object({ pipeline: Type.String() }),\n    async execute(_id, params) {\n      return { content: [{ type: \"text\", text: params.pipeline }] };\n    },\n  },\n  { optional: true },\n);\n<\/code><\/pre>\n<p>},<br \/>\n});<\/p>\n<pre><code>\nEnable optional tools in config:\n\n```json5  theme={&quot;theme&quot;:{&quot;light&quot;:&quot;min-light&quot;,&quot;dark&quot;:&quot;min-dark&quot;}}\n{\n  tools: { allow: [&quot;workflow_tool&quot;] },\n}\n<\/code><\/pre>\n<p>Tips:<\/p>\n<ul>\n<li>Tool names must not clash with core tool names (conflicts are skipped)<\/li>\n<li>Use <code>optional: true<\/code> for tools that trigger side effects or require extra binaries<\/li>\n<li>Users can enable all tools from a plugin by adding the plugin id to <code>tools.allow<\/code><\/li>\n<\/ul>\n<h2>Lint enforcement (in-repo plugins)<\/h2>\n<p>Three scripts enforce SDK boundaries for plugins in the OpenClaw repository:<\/p>\n<ol>\n<li><strong>No monolithic root imports<\/strong> \u2014 <code>openclaw\/plugin-sdk<\/code> root is rejected<\/li>\n<li><strong>No direct src\/ imports<\/strong> \u2014 plugins cannot import <code>..\/..\/src\/<\/code> directly<\/li>\n<li><strong>No self-imports<\/strong> \u2014 plugins cannot import their own <code>plugin-sdk\/&lt;name&gt;<\/code> subpath<\/li>\n<\/ol>\n<p>Run <code>pnpm check<\/code> to verify all boundaries before committing.<\/p>\n<p>External plugins are not subject to these lint rules, but following the same<br \/>\npatterns is strongly recommended.<\/p>\n<h2>Pre-submission checklist<\/h2>\n<p><strong>package.json<\/strong> has correct <code>openclaw<\/code> metadata<br \/>\nEntry point uses <code>defineChannelPluginEntry<\/code> or <code>definePluginEntry<\/code><br \/>\nAll imports use focused <code>plugin-sdk\/&lt;subpath&gt;<\/code> paths<br \/>\nInternal imports use local modules, not SDK self-imports<br \/>\n<code>openclaw.plugin.json<\/code> manifest is present and valid<br \/>\nTests pass<br \/>\n<code>pnpm check<\/code> passes (in-repo plugins)<\/p>\n<h2>Related<\/h2>\n<ul>\n<li><a href=\"\/plugins\/sdk-migration\">Plugin SDK Migration<\/a> \u2014 migrating from deprecated compat surfaces<\/li>\n<li><a href=\"\/plugins\/architecture\">Plugin Architecture<\/a> \u2014 internals and capability model<\/li>\n<li><a href=\"\/plugins\/manifest\">Plugin Manifest<\/a> \u2014 full manifest schema<\/li>\n<li><a href=\"\/plugins\/building-plugins#registering-agent-tools\">Plugin Agent Tools<\/a> \u2014 adding agent tools in a plugin<\/li>\n<li><a href=\"\/plugins\/community\">Community Plugins<\/a> \u2014 listing and quality bar<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Building Plugins Plugins extend OpenClaw with new capab [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-622","post","type-post","status-publish","format-standard","hentry","category-docs"],"_links":{"self":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts\/622","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/comments?post=622"}],"version-history":[{"count":2,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts\/622\/revisions"}],"predecessor-version":[{"id":729,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts\/622\/revisions\/729"}],"wp:attachment":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/media?parent=622"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/categories?post=622"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/tags?post=622"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}