Building channel plugins
기준일: 2026-07-26
공식 기준: Building channel plugins
Building channel plugins 문서는 OpenClaw 공식 문서(plugins/sdk-channel-plugins)를 한국어로 정리한 가이드입니다. Step-by-step guide to building a messaging channel plugin for OpenClaw 명령·설정 키·코드 예시는 공식 문서를 그대로 보존하며, 해석과 절차 안내는 한국어로 제공합니다. 최종 동작은 설치된 CLI 버전과 공식 원문을 확인하세요.
핵심 요약
Step-by-step guide to building a messaging channel plugin for OpenClaw
한국어 가이드 범위: plugins/sdk-channel-plugins 경로의 설정·명령·제약·예시를 학습용으로 재구성합니다.
문서 구성
공식 문서의 주요 섹션은 다음과 같습니다.
- What your plugin owns
- Message adapter
- Inbound ingress (experimental)
- Durable ingress and replay dedupe
- Typing indicators
- Media source params
- Native payload shaping
- Session conversation grammar
- Account-scoped conversation binding support
- Approvals and channel capabilities
- Approval auth
- Payload lifecycle and setup guidance
- Native approval delivery
- Narrower approval runtime subpaths
- Setup subpaths
- Other narrow channel subpaths
- Inbound mention policy
- Walkthrough
- File structure
- Advanced topics
- 다음 단계
- 관련 문서
상세 내용
본문
This guide builds a channel plugin that connects OpenClaw to a messaging platform: DM security, pairing, reply threading, and outbound messaging.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
What your plugin owns
Channel plugins do not implement send/edit/react tools; core provides one shared message tool. Your plugin owns:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- 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
- Outbound - sending text, media, and polls to the platform
- Threading - how replies are threaded
- Heartbeat typing - optional typing/busy signals for heartbeat delivery
Message adapter
Expose a message adapter with defineChannelMessageAdapter from openclaw/plugin-sdk/channel-outbound. Declare only the durable final-send capabilities your native transport actually supports, backed by a contract test that proves the native side effect and returned receipt. Point text/media sends at the same transport functions the legacy outbound adapter uses. For the full API contract, capability matrix, receipt rules, live preview finalization, receive ack policy, tests, and migration table, see Channel outbound API.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
| Surface | Values |
|---|---|
message.live.capabilities |
draftPreview, previewFinalization, progressUpdates, nativeStreaming, quietFinalization |
message.live.finalizer.capabilities |
finalEdit, normalFallback, discardPending, previewReceipt, retainOnAmbiguousFailure |
Inbound ingress (experimental)
Channels migrating inbound authorization can use the experimental openclaw/plugin-sdk/channel-ingress-runtime subpath from runtime receive paths. It accepts platform facts, raw allowlists, route descriptors, command facts, and access group config, then returns sender/route/command/activation projections plus the ordered ingress graph, while platform lookup and side effects stay in the plugin. Keep plugin identity normalization in the descriptor you pass to the resolver; do not serialize raw match values from the resolved state or decision. See Channel ingress API for the API design, ownership boundary, and test expectations.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Durable ingress and replay dedupe
Channels adopting durable ingress should use createChannelIngressMonitor from openclaw/plugin-sdk/channel-outbound unless they need a materially different admission or pump contract. Enqueue the raw transport envelope at a single receive chokepoint (no normalization at receive time), gate the transport ack on the durable append for webhook transports, derive one serialized lane per conversation, and mark the event complete at dispatch adoption. The queue's primary key is (queue_name, event_id) and completion tombstones the row instead of deleting it, so a late platform redelivery of the same event_id is rejected durably for the tombstone retention window. See Channel outbound API for the monitor API and shutdown contract.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- Ack-gated webhook or event delivery: acknowledge or return success only
- Awaited polling or stream delivery: advance the remote cursor or send the
- Non-replay sockets: IRC, Mattermost, Twitch, and Zalo Personal cannot ask
Typing indicators
If your channel supports typing indicators outside inbound replies, expose heartbeat.sendTyping(...) on the channel plugin. Core calls it with the resolved heartbeat delivery target before the heartbeat model run starts and uses the shared typing keepalive/cleanup lifecycle. Add heartbeat.clearTyping(...) when the platform needs an explicit stop signal.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Media source params
If your channel adds message-tool params that carry media sources, expose those param names through plugin.actions.describeMessageTool(...).mediaSourceParams. Core uses that explicit list for sandbox path normalization and outbound media-access policy, so plugins do not need shared-core special cases for provider-specific avatar, attachment, or cover-image params.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Native payload shaping
If your channel needs provider-specific shaping for message(action="send"), prefer actions.prepareSendPayload(...). Put native cards, blocks, embeds, or other durable data under payload.channelData. and let core send through the outbound/message adapter. Use actions.handleAction(...) for send only as a compatibility fallback for payloads that cannot be serialized and retried.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Session conversation grammar
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, order them from the narrowest parent to the broadest/base conversation.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Account-scoped conversation binding support
Set conversationBindings.supportsCurrentConversationBinding when the channel supports generic current-conversation bindings. createChatChannelPlugin(...) sets this static capability to true by default.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
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. ChannelPlugin.approvals was removed; put approval delivery/native/render/auth facts on one approvalCapability object instead. plugin.auth is login/logout only - core no longer reads approval auth hooks from that object.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Approval auth
approvalCapability.getActionAvailabilityState are the canonical approval-auth seam.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
approvalCapability.authorizeActorActionand- Use
getActionAvailabilityStatefor same-chat approval auth availability. - If your channel exposes native exec approvals, use
- If a channel can infer stable owner-like DM identities from existing config,
- If custom approval auth intentionally allows only same-chat fallback, return
- If a channel-owned native callback resolves approvals directly, use
Payload lifecycle and setup guidance
outbound.beforeDeliverPayload for channel-specific payload lifecycle behavior such as hiding duplicate local approval prompts or sending typing indicators before delivery.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- Use
outbound.shouldSuppressLocalPayloadPromptor - Use
approvalCapability.describeExecApprovalSetupwhen the channel wants - Use
approvalCapability.describePluginApprovalSetupwhen plugin approval
Native approval delivery
If a channel needs native approval delivery, keep channel code focused on target normalization plus transport/presentation facts. Use createChannelExecApprovalProfile, createChannelNativeOriginTargetResolver, createChannelApproverDmTargetResolver, and createApproverRestrictedNativeApprovalCapability from openclaw/plugin-sdk/approval-runtime. Put the channel-specific facts behind approvalCapability.nativeRuntime, ideally via createChannelApprovalNativeRuntimeAdapter(...) or createLazyChannelApprovalNativeRuntimeAdapter(...), so core can assemble the handler and own request filtering, routing, dedupe, expiry, gateway subscription, and routed-elsewhere notices.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
availability- whether the account is configured and whether a requestpresentation- map the shared approval view model intotransport- prepare targets plus send/update/delete native approvalinteractions- optional bind/unbind/clear-action hooks for native buttonsobserve- optional delivery diagnostics hooks- Use
createNativeApprovalChannelRouteGatesfrom createChannelNativeOriginTargetResolveruses the shared channel-route- If the channel needs runtime-owned objects such as a client, token, Bolt
- Reach for the lower-level
createChannelApprovalHandleror - Native approval channels must route both
accountIdandapprovalKind - Core owns approval reroute notices too. Channel plugins should not send
- Preserve the delivered approval id kind end-to-end. Native clients should
- Pass that explicit
approvalKindtoresolveApprovalOverGateway. This uses - Different approval kinds can intentionally expose different native
createApproverRestrictedNativeApprovalAdapterstill exists as a
Narrower approval runtime subpaths
For hot channel entrypoints, prefer these narrower subpaths over the broader approval-runtime barrel when you only need one part of that family:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
openclaw/plugin-sdk/approval-auth-runtimeopenclaw/plugin-sdk/approval-client-runtimeopenclaw/plugin-sdk/approval-delivery-runtimeopenclaw/plugin-sdk/approval-gateway-runtimeopenclaw/plugin-sdk/approval-reference-runtimeopenclaw/plugin-sdk/approval-handler-adapter-runtimeopenclaw/plugin-sdk/approval-handler-runtimeopenclaw/plugin-sdk/approval-native-runtimeopenclaw/plugin-sdk/approval-reply-runtimeopenclaw/plugin-sdk/channel-runtime-context
Setup subpaths
createSetupTranslator, import-safe setup patch adapters (createPatchedAccountSetupAdapter, createEnvPatchedAccountSetupAdapter, createSetupInputPresenceValidator), lookup-note output, promptResolvedAllowFrom, splitSetupEntries, and the delegated setup-proxy builders.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
openclaw/plugin-sdk/setup-runtimecovers the runtime-safe setup helpers:openclaw/plugin-sdk/channel-setupcovers the optional-install setup- Use the broader
openclaw/plugin-sdk/setupseam only when you also need
Other narrow channel subpaths
For other hot channel paths, prefer the narrow helpers over broader legacy surfaces:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
openclaw/plugin-sdk/account-core,openclaw/plugin-sdk/account-id,openclaw/plugin-sdk/inbound-envelopeandopenclaw/plugin-sdk/channel-targetsfor target parsing helpersopenclaw/plugin-sdk/channel-outboundfor outbound identity/send delegatesbuildThreadAwareOutboundSessionRoute(...)fromopenclaw/plugin-sdk/thread-bindings-runtimefor thread-binding lifecycle
Inbound mention policy
Keep inbound mention handling split in two layers:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- plugin-owned evidence gathering
- shared policy evaluation
- reply-to-bot detection
- quoted-bot detection
- thread-participation checks
- service/system-message exclusions
- platform-native caches needed to prove bot participation
requireMention- explicit mention result
- implicit mention allowlist
- command bypass
- final skip decision
implicitMentionKindWhen,
matchesMentionWithExplicit,
resolveInboundMentionDecision,
} from "openclaw/plugin-sdk/channel-inbound";
const wasMentioned = matchesMentionWithExplicit({
text,
mentionRegexes,
explicit: {
hasAnyMention,
isExplicitlyMentioned,
canResolveExplicit,
},
});
const facts = {
canDetectMention: true,
wasMentioned,
hasAnyMention,
implicitMentionKinds: [
...implicitMentionKindWhen("reply_to_bot", isReplyToBot),
...implicitMentionKindWhen("quoted_bot", isQuoteOfBot),
],
};
const implicitMentions = resolveChannelImplicitMentions({
cfg,
channel: channelId,
accountId,
});
const decision = resolveInboundMentionDecision({
facts,
policy: {
isGroup,
requireMention,
implicitMentions,
allowTextCommands,
hasControlCommand,
commandAuthorized,
},
});
if (decision.shouldSkip) return;
Walkthrough
Create the standard plugin files. The channels field in openclaw.plugin.json (not a kind field) is what marks a manifest as owning a channel. For the full package-metadata surface, see Plugin Setup and Config:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
`configSchema` validates `plugins.entries.acme-chat.config`. Use it for
plugin-owned settings that are not the channel account config.
`channelConfigs.acme-chat.schema` validates `channels.acme-chat` and is the
cold-path source used by config schema, setup, and UI surfaces before the
plugin runtime loads. See [Plugin manifest](/plugins/manifest) for the full
top-level field reference.
The `ChannelPlugin` interface has many optional adapter surfaces. Start with
the minimum - `id`, `config`, and `setup` - and add adapters as you need
them.
Create `src/channel.ts`:
For channels that accept both canonical top-level DM keys and legacy nested keys, use the helpers from `plugin-sdk/channel-config-helpers`: `resolveChannelDmAccess`, `resolveChannelDmPolicy`, `resolveChannelDmAllowFrom`, and `normalizeChannelDmPolicy` keep account-local values ahead of inherited root values. Pair the same resolver with doctor repair through `normalizeLegacyDmAliases` so runtime and migration read the same contract.
Instead of implementing low-level adapter interfaces manually, you pass
declarative options and the builder composes them:
| 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); requires a sibling `channel` id so core can stamp the returned delivery result |
You can also pass raw adapter objects instead of the declarative options
if you need full control.
Raw outbound adapters may define a `chunker(text, limit, ctx)` function.
The optional `ctx.formatting` carries delivery-time formatting decisions
such as `maxLinesPerMessage`; apply it before sending so reply threading
and chunk boundaries are resolved once by shared outbound delivery.
Send contexts also include `replyToIdSource` (`implicit` or `explicit`)
when a native reply target was resolved, so payload helpers can preserve
explicit reply tags without consuming an implicit single-use reply slot.
### Group tool-policy adapters
A channel that implements `group.resolveToolPolicy` and supports
`toolsBySender` must forward the complete `ChannelGroupContext` to its
shared policy resolver. In particular, honor `senderPolicyMode: "never"`
by skipping sender-specific overlays at both the matched-group and wildcard
scopes while still applying the base `tools` policy.
OpenClaw sets this mode only for trusted non-ingress execution whose sender
authority was already captured in a server-owned envelope, such as an
explicitly capped scheduled run. Plugins must not derive the mode from
inbound metadata, persist it as channel state, or expose it as config. Add
an adapter test that proves the mode skips a wildcard `toolsBySender` entry
without dropping the matching base `tools` restriction.
Create `index.ts`:
Put channel-owned CLI descriptors in `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.
`defineChannelPluginEntry` handles the registration-mode split automatically.
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`. See
[Entry Points](/plugins/sdk-entrypoints#definechannelpluginentry) for all
options.
Create `setup-entry.ts` for lightweight loading during onboarding:
File structure
<bundled-plugin-root>/acme-chat/
├── package.json # openclaw.channel metadata
├── openclaw.plugin.json # Manifest with config schema
├── index.ts # defineChannelPluginEntry
├── setup-entry.ts # defineSetupPluginEntry
├── api.ts # Public exports (optional)
├── runtime-api.ts # Internal runtime exports (optional)
└── src/
├── channel.ts # ChannelPlugin via createChatChannelPlugin
├── channel.test.ts # Tests
├── client.ts # Platform API client
└── runtime.ts # Runtime store (if needed)
Advanced topics
Fixed, account-scoped, or custom reply modes
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
다음 단계
주요 항목:
- 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
관련 문서
주요 항목:
- Plugin SDK setup
- Building plugins
- Agent harness plugins
실습 체크리스트
- 공식 문서와 로컬 버전을 대조합니다:
https://docs.openclaw.ai/plugins/sdk-channel-plugins - 관련 CLI는
openclaw --help및 하위 명령--help로 옵션을 확인합니다. - 설정 변경 시
openclaw config/openclaw doctor로 유효성을 검사합니다. - Gateway·채널·플러그인 변경 후에는 필요 시 Gateway를 재시작합니다.
자주 쓰는 명령·설정 예시
implicitMentionKindWhen,
matchesMentionWithExplicit,
resolveInboundMentionDecision,
} from "openclaw/plugin-sdk/channel-inbound";
const wasMentioned = matchesMentionWithExplicit({
text,
mentionRegexes,
explicit: {
hasAnyMention,
isExplicitlyMentioned,
canResolveExplicit,
},
});
const facts = {
canDetectMention: true,
wasMentioned,
hasAnyMention,
implicitMentionKinds: [
...implicitMentionKindWhen("reply_to_bot", isReplyToBot),
...implicitMentionKindWhen("quoted_bot", isQuoteOfBot),
],
};
const implicitMentions = resolveChannelImplicitMentions({
cfg,
channel: channelId,
accountId,
});
const decision = resolveInboundMentionDecision({
facts,
policy: {
isGroup,
requireMention,
implicitMentions,
allowTextCommands,
hasControlCommand,
commandAuthorized,
},
});
if (decision.shouldSkip) return;
`configSchema` validates `plugins.entries.acme-chat.config`. Use it for
plugin-owned settings that are not the channel account config.
`channelConfigs.acme-chat.schema` validates `channels.acme-chat` and is the
cold-path source used by config schema, setup, and UI surfaces before the
plugin runtime loads. See [Plugin manifest](/plugins/manifest) for the full
top-level field reference.
The `ChannelPlugin` interface has many optional adapter surfaces. Start with
the minimum - `id`, `config`, and `setup` - and add adapters as you need
them.
Create `src/channel.ts`:
For channels that accept both canonical top-level DM keys and legacy nested keys, use the helpers from `plugin-sdk/channel-config-helpers`: `resolveChannelDmAccess`, `resolveChannelDmPolicy`, `resolveChannelDmAllowFrom`, and `normalizeChannelDmPolicy` keep account-local values ahead of inherited root values. Pair the same resolver with doctor repair through `normalizeLegacyDmAliases` so runtime and migration read the same contract.
Instead of implementing low-level adapter interfaces manually, you pass
declarative options and the builder composes them:
| 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); requires a sibling `channel` id so core can stamp the returned delivery result |
You can also pass raw adapter objects instead of the declarative options
if you need full control.
Raw outbound adapters may define a `chunker(text, limit, ctx)` function.
The optional `ctx.formatting` carries delivery-time formatting decisions
such as `maxLinesPerMessage`; apply it before sending so reply threading
and chunk boundaries are resolved once by shared outbound delivery.
Send contexts also include `replyToIdSource` (`implicit` or `explicit`)
when a native reply target was resolved, so payload helpers can preserve
explicit reply tags without consuming an implicit single-use reply slot.
### Group tool-policy adapters
A channel that implements `group.resolveToolPolicy` and supports
`toolsBySender` must forward the complete `ChannelGroupContext` to its
shared policy resolver. In particular, honor `senderPolicyMode: "never"`
by skipping sender-specific overlays at both the matched-group and wildcard
scopes while still applying the base `tools` policy.
OpenClaw sets this mode only for trusted non-ingress execution whose sender
authority was already captured in a server-owned envelope, such as an
explicitly capped scheduled run. Plugins must not derive the mode from
inbound metadata, persist it as channel state, or expose it as config. Add
an adapter test that proves the mode skips a wildcard `toolsBySender` entry
without dropping the matching base `tools` restriction.
Create `index.ts`:
Put channel-owned CLI descriptors in `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.
`defineChannelPluginEntry` handles the registration-mode split automatically.
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`. See
[Entry Points](/plugins/sdk-entrypoints#definechannelpluginentry) for all
options.
Create `setup-entry.ts` for lightweight loading during onboarding:
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](/plugins/sdk-setup#setup-entry) for details.
Bundled workspace channels that split setup-safe exports into sidecar
modules can use `defineBundledChannelSetupEntry(...)` from
`openclaw/plugin-sdk/channel-entry-contract` when they also need an
explicit setup-time runtime setter.
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:
관련 링크
이 가이드는 공식 문서를 한국어 학습용으로 재구성한 것입니다. 옵션 기본값·플래그 이름은 설치 버전에 따라 달라질 수 있습니다.