Skip to content

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.

  • openclaw/plugin-sdk package
  • OpenClawPluginApi access
  • Core runtime compatibility (openclawRuntime: ">=2026.2.0")
  • Configuration keys for your specific channel

You can get your connector running on the new architecture by following these steps:

  1. Install the SDK: Add openclaw/plugin-sdk to your project to get access to stable types and config utilities.
  2. Update Imports: Remove any direct imports from src/**. Use the SDK for types like ChannelPlugin or ChannelMeta.
  3. Access the Runtime: Use OpenClawPluginApi.runtime for core behaviors like text chunking, media fetching, or routing.
  4. Set Versioning: Declare your required runtime range in your plugin configuration to ensure compatibility.

I recommend thinking about your plugin in two parts: the SDK and the Runtime.

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, and ChannelCapabilities.
  • Config helpers: buildChannelConfigSchema and deleteAccountFromConfigSection.
  • Pairing helpers: PAIRING_APPROVED_MESSAGE and formatPairingApproveHint.
  • Tool param helpers: createActionGate and readStringParam.

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;
};
};
  • Direct Core Import Errors: If your build fails because of src/** imports, you need to migrate those calls to api.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.

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.