Building Stable Messaging Connectors with the New Plugin SDK
I have spent too much time fixing broken connectors after a core update. It usually happens because a plugin was reaching into internal folders it shouldn’t have touched. When internal structures change, everything breaks, and you are left chasing imports.
To fix this, we are moving toward a model where every messaging connector is a plugin that uses one stable API. No more direct imports from src/**. Instead, all dependencies go through a dedicated SDK or the runtime.
What You’ll Need
Section titled “What You’ll Need”openclaw/plugin-sdkpackageOpenClawPluginApiaccess- Core runtime compatibility (
openclawRuntime: ">=2026.2.0") - Configuration keys for your specific channel
Quick Start
Section titled “Quick Start”You can get your connector running on the new architecture by following these steps:
- Install the SDK: Add
openclaw/plugin-sdkto your project to get access to stable types and config utilities. - Update Imports: Remove any direct imports from
src/**. Use the SDK for types likeChannelPluginorChannelMeta. - Access the Runtime: Use
OpenClawPluginApi.runtimefor core behaviors like text chunking, media fetching, or routing. - Set Versioning: Declare your required runtime range in your plugin configuration to ensure compatibility.
The Two-Layer Architecture
Section titled “The Two-Layer Architecture”I recommend thinking about your plugin in two parts: the SDK and the Runtime.
1. Plugin SDK
Section titled “1. Plugin SDK”This is a compile-time layer. It contains types and config helpers but has no side effects. You use this for:
- Types:
ChannelPlugin, adapters,ChannelMeta, andChannelCapabilities. - Config helpers:
buildChannelConfigSchemaanddeleteAccountFromConfigSection. - Pairing helpers:
PAIRING_APPROVED_MESSAGEandformatPairingApproveHint. - Tool param helpers:
createActionGateandreadStringParam.
2. Plugin Runtime
Section titled “2. Plugin Runtime”The runtime is the execution surface. It is injected into your plugin, so you never have to import core logic directly. Here is what the PluginRuntime surface looks like:
export type PluginRuntime = { channel: { text: { chunkMarkdownText(text: string, limit: number): string[]; resolveTextChunkLimit(cfg: OpenClawConfig, channel: string, accountId?: string): number; hasControlCommand(text: string, cfg: OpenClawConfig): boolean; }; reply: { dispatchReplyWithBufferedBlockDispatcher(params: { ctx: unknown; cfg: unknown; dispatcherOptions: { deliver: (payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string; }) => void | Promise<void>; onError?: (err: unknown, info: { kind: string }) => void; }; }): Promise<void>; createReplyDispatcherWithTyping?: unknown; }; routing: { resolveAgentRoute(params: { cfg: unknown; channel: string; accountId: string; peer: { kind: RoutePeerKind; id: string }; }): { sessionKey: string; accountId: string }; }; pairing: { buildPairingReply(params: { channel: string; idLine: string; code: string }): string; readAllowFromStore(channel: string): Promise<string[]>; upsertPairingRequest(params: { channel: string; id: string; meta?: { name?: string }; }): Promise<{ code: string; created: boolean }>; }; media: { fetchRemoteMedia(params: { url: string }): Promise<{ buffer: Buffer; contentType?: string }>; saveMediaBuffer( buffer: Uint8Array, contentType: string | undefined, direction: "inbound" | "outbound", maxBytes: number, ): Promise<{ path: string; contentType?: string }>; }; mentions: { buildMentionRegexes(cfg: OpenClawConfig, agentId?: string): RegExp[]; matchesMentionPatterns(text: string, regexes: RegExp[]): boolean; }; groups: { resolveGroupPolicy( cfg: OpenClawConfig, channel: string, accountId: string, groupId: string, ): { allowlistEnabled: boolean; allowed: boolean; groupConfig?: unknown; defaultConfig?: unknown; }; resolveRequireMention( cfg: OpenClawConfig, channel: string, accountId: string, groupId: string, override?: boolean, ): boolean; }; debounce: { createInboundDebouncer<T>(opts: { debounceMs: number; buildKey: (v: T) => string | null; shouldDebounce: (v: T) => boolean; onFlush: (entries: T[]) => Promise<void>; onError?: (err: unknown) => void; }): { push: (v: T) => void; flush: () => Promise<void> }; resolveInboundDebounceMs(cfg: OpenClawConfig, channel: string): number; }; commands: { resolveCommandAuthorizedFromAuthorizers(params: { useAccessGroups: boolean; authorizers: Array<{ configured: boolean; allowed: boolean }>; }): boolean; }; }; logging: { shouldLogVerbose(): boolean; getChildLogger(name: string): PluginLogger; }; state: { resolveStateDir(cfg: OpenClawConfig): string; };};Troubleshooting
Section titled “Troubleshooting”- Direct Core Import Errors: If your build fails because of
src/**imports, you need to migrate those calls toapi.runtime. This ensures your plugin doesn’t break when core internals change. - Behavior Drift: If routing or pairing logic feels off after an update, check your adapter-level unit tests. I suggest using golden tests for each plugin to verify that mention gating and allowlists still work as expected.
If you hit a wall while setting up your first plugin with the new SDK, check out the AI Setup Assistant.
What’s Next
Section titled “What’s Next”OpenClaw Expert
Still stuck?
If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.