API Builder

Deep Dive

How buildPluginApi() constructs the plugin registration facade

Facade Pattern 20+ Methods Noop Fallbacks TypeScript

📚 What is this & why does it matter?

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.

💡 Real-world analogy

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.

Contents

Overview

Primary Role

Construct an OpenClawPluginApi instance that plugins receive in their register() function.

Main Inputs

BuildPluginApiParams — plugin id, name, config, runtime, logger, resolution path, handler overrides.

Key Decisions

Which registration handlers to use (real vs noop), how to bind plugin identity to each call.

Outputs

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.

How It Works

Accepting Build Parameters

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.

Handler Resolution

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.

Identity Binding

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.

API Composition

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

Behavior Sequence

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.

Annotated Code Walkthrough

Walk through the key patterns inside api-builder.ts:

The Noop Pattern

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 => {};

Handler Override Merging

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;

Identity Binding

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
};

Logger Injection

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" }

Edge Cases & Error Handling

ScenarioBehaviorImpact
All handlers are noop (test mode)API is fully constructed but registration calls are silently discardedUseful for unit testing plugin logic without a live registry
Config snapshot changes after API constructionPlugin holds a stale config reference; changes are not reflectedPlugins must not cache config values long-term
Plugin calls registerTool multiple timesEach call is forwarded independently; conflict detection is in the registry, not the builderDuplicate tool names are caught downstream
Handler throws during registrationException propagates to the loader; partial registrations may existLoader catches and marks the plugin as failed

Directory Map

Source files for API Builder
📁 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