Building Channel Plugins
This guide walks through building a channel plugin that connects OpenClaw to a messaging platform. By the end you will have a working channel with DM security, pairing, reply threading, and outbound messaging.If you have not built any OpenClaw plugin before, read
Getting Started first for the basic package
structure and manifest setup.
How channel plugins work
Channel plugins do not need their own send/edit/react tools. OpenClaw keeps one sharedmessage tool in core. Your plugin owns:
- Config — account resolution and setup wizard
- Security — DM policy and allowlists
- Pairing — DM approval flow
- Session grammar — how provider-specific conversation ids map to base chats, thread ids, and parent fallbacks
- Outbound — sending text, media, and polls to the platform
- Threading — how replies are threaded
:thread: bookkeeping, and dispatch.
If your platform stores extra scope inside conversation ids, keep that parsing
in the plugin with messaging.resolveSessionConversation(...). That is the
canonical hook for mapping rawId to the base conversation id, optional thread
id, explicit baseConversationId, and any parentConversationCandidates.
When you return parentConversationCandidates, keep them ordered from the
narrowest parent to the broadest/base conversation.
Bundled plugins that need the same parsing before the channel registry boots
can also expose a top-level session-key-api.ts file with a matching
resolveSessionConversation(...) export. Core uses that bootstrap-safe surface
only when the runtime plugin registry is not available yet.
messaging.resolveParentConversationCandidates(...) remains available as a
legacy compatibility fallback when a plugin only needs parent fallbacks on top
of the generic/raw id. If both hooks exist, core uses
resolveSessionConversation(...).parentConversationCandidates first and only
falls back to resolveParentConversationCandidates(...) when the canonical hook
omits them.
Approvals and channel capabilities
Most channel plugins do not need approval-specific code.- Core owns same-chat
/approve, shared approval button payloads, and generic fallback delivery. - Prefer one
approvalCapabilityobject on the channel plugin when the channel needs approval-specific behavior. approvalCapability.authorizeActorActionandapprovalCapability.getActionAvailabilityStateare the canonical approval-auth seam.- Use
outbound.shouldSuppressLocalPayloadPromptoroutbound.beforeDeliverPayloadfor channel-specific payload lifecycle behavior such as hiding duplicate local approval prompts or sending typing indicators before delivery. - Use
approvalCapability.deliveryonly for native approval routing or fallback suppression. - Use
approvalCapability.renderonly when a channel truly needs custom approval payloads instead of the shared renderer. - If a channel can infer stable owner-like DM identities from existing config, use
createResolvedApproverActionAuthAdapterfromopenclaw/plugin-sdk/approval-runtimeto restrict same-chat/approvewithout adding approval-specific core logic. - If a channel needs native approval delivery, keep channel code focused on target normalization and transport hooks. Use
createChannelExecApprovalProfile,createChannelNativeOriginTargetResolver,createChannelApproverDmTargetResolver,createApproverRestrictedNativeApprovalCapability, andcreateChannelNativeApprovalRuntimefromopenclaw/plugin-sdk/approval-runtimeso core owns request filtering, routing, dedupe, expiry, and gateway subscription. - Native approval channels must route both
accountIdandapprovalKindthrough those helpers.accountIdkeeps multi-account approval policy scoped to the right bot account, andapprovalKindkeeps exec vs plugin approval behavior available to the channel without hardcoded branches in core. - Preserve the delivered approval id kind end-to-end. Native clients should not guess or rewrite exec vs plugin approval routing from channel-local state.
- Different approval kinds can intentionally expose different native surfaces.
Current bundled examples:
- Slack keeps native approval routing available for both exec and plugin ids.
- Matrix keeps native DM/channel routing for exec approvals only and leaves
plugin approvals on the shared same-chat
/approvepath.
createApproverRestrictedNativeApprovalAdapterstill exists as a compatibility wrapper, but new code should prefer the capability builder and exposeapprovalCapabilityon the plugin.
openclaw/plugin-sdk/approval-auth-runtimeopenclaw/plugin-sdk/approval-client-runtimeopenclaw/plugin-sdk/approval-delivery-runtimeopenclaw/plugin-sdk/approval-native-runtimeopenclaw/plugin-sdk/approval-reply-runtime
openclaw/plugin-sdk/setup-runtime,
openclaw/plugin-sdk/setup-adapter-runtime,
openclaw/plugin-sdk/reply-runtime,
openclaw/plugin-sdk/reply-dispatch-runtime,
openclaw/plugin-sdk/reply-reference, and
openclaw/plugin-sdk/reply-chunking when you do not need the broader umbrella
surface.
For setup specifically:
openclaw/plugin-sdk/setup-runtimecovers the runtime-safe setup helpers: import-safe setup patch adapters (createPatchedAccountSetupAdapter,createEnvPatchedAccountSetupAdapter,createSetupInputPresenceValidator), lookup-note output,promptResolvedAllowFrom,splitSetupEntries, and the delegated setup-proxy buildersopenclaw/plugin-sdk/setup-adapter-runtimeis the narrow env-aware adapter seam forcreateEnvPatchedAccountSetupAdapteropenclaw/plugin-sdk/channel-setupcovers the optional-install setup builders plus a few setup-safe primitives:createOptionalChannelSetupSurface,createOptionalChannelSetupAdapter,createOptionalChannelSetupWizard,DEFAULT_ACCOUNT_ID,createTopLevelChannelDmPolicy,setSetupChannelEnabled, andsplitSetupEntries- use the broader
openclaw/plugin-sdk/setupseam only when you also need the heavier shared setup/config helpers such asmoveSingleAccountChannelSectionToDefaultAccount(...)
createOptionalChannelSetupSurface(...). The generated
adapter/wizard fail closed on config writes and finalization, and they reuse
the same install-required message across validation, finalize, and docs-link
copy.
For other hot channel paths, prefer the narrow helpers over broader legacy
surfaces:
openclaw/plugin-sdk/account-core,openclaw/plugin-sdk/account-id,openclaw/plugin-sdk/account-resolution, andopenclaw/plugin-sdk/account-helpersfor multi-account config and default-account fallbackopenclaw/plugin-sdk/inbound-envelopeandopenclaw/plugin-sdk/inbound-reply-dispatchfor inbound route/envelope and record-and-dispatch wiringopenclaw/plugin-sdk/messaging-targetsfor target parsing/matchingopenclaw/plugin-sdk/outbound-mediaandopenclaw/plugin-sdk/outbound-runtimefor media loading plus outbound identity/send delegatesopenclaw/plugin-sdk/thread-bindings-runtimefor thread-binding lifecycle and adapter registrationopenclaw/plugin-sdk/agent-media-payloadonly when a legacy agent/media payload field layout is still requiredopenclaw/plugin-sdk/telegram-command-configfor Telegram custom-command normalization, duplicate/conflict validation, and a fallback-stable command config contract
Walkthrough
Package and manifest
Create the standard plugin files. The
channel field in package.json is
what makes this a channel plugin. For the full package-metadata surface,
see Plugin Setup and Config:Build the channel plugin object
The
ChannelPlugin interface has many optional adapter surfaces. Start with
the minimum — id and setup — and add adapters as you need them.Create src/channel.ts:src/channel.ts
What createChatChannelPlugin does for you
What createChatChannelPlugin does for you
Instead of implementing low-level adapter interfaces manually, you pass
declarative options and the builder composes them:
You can also pass raw adapter objects instead of the declarative options
if you need full control.
| Option | What it wires |
|---|---|
security.dm | Scoped DM security resolver from config fields |
pairing.text | Text-based DM pairing flow with code exchange |
threading | Reply-to-mode resolver (fixed, account-scoped, or custom) |
outbound.attachedResults | Send functions that return result metadata (message IDs) |
Wire the entry point
Create Put channel-owned CLI descriptors in
index.ts:index.ts
registerCliMetadata(...) so OpenClaw
can show them in root help without activating the full channel runtime,
while normal full loads still pick up the same descriptors for real command
registration. Keep registerFull(...) for runtime-only work.
If registerFull(...) registers gateway RPC methods, use a
plugin-specific prefix. Core admin namespaces (config.*,
exec.approvals.*, wizard.*, update.*) stay reserved and always
resolve to operator.admin.
defineChannelPluginEntry handles the registration-mode split automatically. See
Entry Points for all
options.Add a setup entry
Create OpenClaw loads this instead of the full entry when the channel is disabled
or unconfigured. It avoids pulling in heavy runtime code during setup flows.
See Setup and Config for details.
setup-entry.ts for lightweight loading during onboarding:setup-entry.ts
Handle inbound messages
Your plugin needs to receive messages from the platform and forward them to
OpenClaw. The typical pattern is a webhook that verifies the request and
dispatches it through your channel’s inbound handler:
Inbound message handling is channel-specific. Each channel plugin owns
its own inbound pipeline. Look at bundled channel plugins
(for example the Microsoft Teams or Google Chat plugin package) for real patterns.
Test
Write colocated tests in For shared test helpers, see Testing.
src/channel.test.ts:src/channel.test.ts
File structure
Advanced topics
Threading options
Fixed, account-scoped, or custom reply modes
Message tool integration
describeMessageTool and action discovery
Target resolution
inferTargetChatType, looksLikeId, resolveTarget
Runtime helpers
TTS, STT, media, subagent via api.runtime
Next steps
- Provider Plugins — if your plugin also provides models
- SDK Overview — full subpath import reference
- SDK Testing — test utilities and contract tests
- Plugin Manifest — full manifest schema