Primary Role
Construct an OpenClawPluginApi instance that plugins receive in their register() function.
How buildPluginApi() constructs the plugin registration facade
Facade Pattern 20+ Methods Noop Fallbacks TypeScript
The API Builder is the factory that constructs the OpenClawPluginApi object every plugin receives inside its register() function. It wires together handler overrides, noop fallbacks, plugin identity, and logger into a single cohesive API surface. Without this builder, each plugin would need to know about the internal registry, handler resolution, and identity binding directly.
Think of buildPluginApi() like a car rental desk at the airport. The customer (plugin) walks up and gets a fully prepared vehicle (API object) — keys bound to their identity, GPS pre-loaded (logger), insurance options set (handler overrides vs. noop defaults). The customer doesn’t need to visit the garage, talk to the mechanic, or know how the fleet system works. They just drive.
Construct an OpenClawPluginApi instance that plugins receive in their register() function.
BuildPluginApiParams — plugin id, name, config, runtime, logger, resolution path, handler overrides.
Which registration handlers to use (real vs noop), how to bind plugin identity to each call.
OpenClawPluginApi — fully wired API object with registerTool, registerHook, registerChannel, etc.
flowchart LR
Params["BuildPluginApiParams"] --> Build["buildPluginApi()"]
Build --> Identity["Bind plugin id/name"]
Build --> Handlers{"Handler overrides?"}
Handlers -->|Provided| Real["Use real handlers"]
Handlers -->|Missing| Noop["Use noop fallbacks"]
Real --> API["OpenClawPluginApi"]
Noop --> API
API --> Plugin["plugin.register(api)"]
The builder accepts a params object containing plugin metadata, configuration snapshot, runtime context, and optional handler overrides. It assembles these into a complete API surface that the plugin can call during registration.
The buildPluginApi() function in api-builder.ts accepts BuildPluginApiParams which contains the plugin’s metadata, a config snapshot, the runtime environment, and optional handler overrides for each registration method.
For each registration method (registerTool, registerHook, registerChannel, etc.), the builder checks if a real handler was provided in the overrides object. If a handler is present, it is used directly. If missing, the builder falls back to a typed noop function (e.g., noopRegisterTool, noopRegisterHook). This allows partial API construction for different loading modes — for example, CLI-only mode may only wire up registerCommand and registerCliBackend while leaving other handlers as noops.
The builder binds the plugin’s identity (id, name, version) to each registration call so the registry knows which plugin registered what. This means when a plugin calls api.registerTool(toolDef), the registry automatically associates the tool with the correct plugin.
The final OpenClawPluginApi object includes the PluginLogger for diagnostic output, the config snapshot for reading settings, and 20+ registration methods:
// Registration methods on the composed API object
registerTool, registerHook, registerHttpRoute, registerChannel,
registerGatewayMethod, registerCli, registerService, registerCliBackend,
registerProvider, registerSpeechProvider, registerMediaUnderstandingProvider,
registerImageGenerationProvider, registerWebFetchProvider,
registerWebSearchProvider, registerInteractiveHandler, registerCommand,
registerContextEngine, registerMemoryPromptSection, registerMemoryFlushPlan,
registerMemoryRuntime, registerMemoryEmbeddingProvider, on
sequenceDiagram
participant Loader as Plugin Loader
participant Builder as api-builder.ts
participant Plugin as Plugin Module
participant Reg as Plugin Registry
Loader->>Builder: buildPluginApi(params)
Builder->>Builder: Check handler overrides
Builder->>Builder: Fill missing with noop
Builder->>Builder: Bind plugin identity
Builder-->>Loader: OpenClawPluginApi
Loader->>Plugin: plugin.register(api)
Plugin->>Reg: api.registerTool(toolDef)
Plugin->>Reg: api.registerHook('before_agent_start', handler)
Plugin->>Reg: api.registerProvider(providerPlugin)
Note over Plugin,Reg: Each call scoped to plugin id
The loader creates the API via the builder, then passes it to the plugin’s register() function. Every registration call the plugin makes flows through the handler (real or noop) with the plugin’s identity automatically attached.
Walk through the key patterns inside api-builder.ts:
Each registration method has a typed noop fallback. These are pure functions that accept the same parameters as real handlers but do nothing. This keeps the API surface uniform regardless of loading mode.
// Typed noop fallbacks for each registration method
const noopRegisterTool = (_toolDef: PluginToolDefinition): void => {};
const noopRegisterHook = (_name: PluginHookName, _handler: HookHandler): void => {};
const noopRegisterChannel = (_channel: ChannelPlugin): void => {};
const noopRegisterProvider = (_provider: ProviderPlugin): void => {};
The builder merges provided handlers with noop defaults using nullish coalescing. If params.handlers is provided, each method is checked individually:
// Handler override merging — real handlers fill in, noops cover the rest
const handlers = params.handlers ?? {};
const registerTool = handlers.registerTool ?? noopRegisterTool;
const registerHook = handlers.registerHook ?? noopRegisterHook;
const registerChannel = handlers.registerChannel ?? noopRegisterChannel;
const registerProvider = handlers.registerProvider ?? noopRegisterProvider;
The returned API object carries the plugin’s identity so every downstream registration is automatically scoped:
// Identity binding — plugin id/name on returned API object
const api: OpenClawPluginApi = {
id: params.id,
name: params.name,
config: params.config,
logger: params.logger,
runtime: params.runtime,
registerTool: (toolDef) => registerTool({ ...toolDef, pluginId: params.id }),
registerHook: (name, handler) => registerHook(name, handler, params.id),
// ... 20+ more methods, each scoped to params.id
};
The PluginLogger is attached to the API for plugin diagnostics. Plugins use api.logger.info(), api.logger.warn(), etc., and all output is tagged with the plugin id:
// Logger injection — tagged with plugin identity
const logger: PluginLogger = createPluginLogger(params.id, params.name);
// Plugin usage: api.logger.info("Tool registered", { tool: toolDef.name });
// Output: [my-plugin] Tool registered { tool: "my_tool" }
| Scenario | Behavior | Impact |
|---|---|---|
| All handlers are noop (test mode) | API is fully constructed but registration calls are silently discarded | Useful for unit testing plugin logic without a live registry |
| Config snapshot changes after API construction | Plugin holds a stale config reference; changes are not reflected | Plugins must not cache config values long-term |
Plugin calls registerTool multiple times | Each call is forwarded independently; conflict detection is in the registry, not the builder | Duplicate tool names are caught downstream |
| Handler throws during registration | Exception propagates to the loader; partial registrations may exist | Loader catches and marks the plugin as failed |
OpenClawPluginDefinition interface that calls the built API📁 src/plugin-sdk/
├── api-builder.ts # buildPluginApi() — main factory
├── api-builder-types.ts # BuildPluginApiParams, handler override types
├── noop-handlers.ts # noopRegisterTool, noopRegisterHook, etc.
├── plugin-api-types.ts # OpenClawPluginApi interface definition
├── plugin-logger.ts # PluginLogger creation and tagging
└── plugin-identity.ts # Plugin id/name/version binding utilities