Plugin maintainer reference
Channel surfaces
The surfaces a channel plugin owns: conversation binding callbacks, message tool schemas, target resolution, config-backed directories, and read-only account inspection. Part of the Plugin architecture internals guide.
Conversation binding callbacks
Plugins that bind a conversation can react when an approval is resolved.
Use api.onConversationBindingResolved(...) to receive a callback after a bind
request is approved or denied:
export default { id: "my-plugin", register(api) { api.onConversationBindingResolved(async (event) => { if (event.status === "approved") { // A binding now exists for this plugin + conversation. console.log(event.binding?.conversationId); return; } // The request was denied; clear any local pending state. console.log(event.request.conversation.conversationId); }); },};Callback payload fields:
status:"approved"or"denied"decision:"allow-once","allow-always", or"deny"binding: the resolved binding for approved requestsrequest: the original request summary, detach hint, sender id, and conversation metadata
This callback is notification-only. It does not change who is allowed to bind a conversation, and it runs after core approval handling finishes.
Message tool schemas
Plugins should own channel-specific describeMessageTool(...) schema
contributions for non-message primitives such as reactions, reads, and polls.
Shared send presentation should use the generic MessagePresentation contract
instead of provider-native button, component, block, or card fields.
See Message Presentation for the contract,
fallback rules, provider mapping, and plugin author checklist.
Provider-native schema extensions require explicit maintainer approval,
channel-owned parsing, documented cross-channel behavior, and capabilities that
MessagePresentation cannot express. Discord components is the approved
built-in exception for its advanced Components V2 layouts.
Send-capable plugins declare what they can render through message capabilities:
presentationfor semantic presentation blocks (text,context,divider,chart,table,buttons,select)delivery-pinfor pinned-delivery requests
Core decides whether to render the presentation natively or degrade it to text. Do not expose unapproved provider-native UI escape hatches from the generic message tool. Deprecated SDK helpers for legacy native schemas remain exported for existing third-party plugins, but new plugins should not use them.
Channel target resolution
Channel plugins should own channel-specific target semantics. Keep the shared outbound host generic and use the messaging adapter surface for provider rules:
messaging.inferTargetChatType({ to })decides whether a normalized target should be treated asdirect,group, orchannelbefore directory lookup. Implicit owner heartbeat delivery requires this direct classification; without it, Gateway status reportswaiting for route.messaging.targetResolver.looksLikeId(raw, normalized)tells core whether an input should skip straight to id-like resolution instead of directory search.messaging.targetResolver.reservedLiteralslists bare words that are channel/session references for that provider. Resolution preserves configured directory entries before rejecting reserved literals, then fails closed on a directory miss.messaging.targetResolver.resolveTarget(...)is the plugin fallback when core needs a final provider-owned resolution after normalization or after a directory miss.messaging.resolveOutboundSessionRoute(...)owns provider-specific session route construction once a target is resolved.
Recommended split:
- Use
inferTargetChatTypefor category decisions that should happen before searching peers/groups. - Use
looksLikeIdfor "treat this as an explicit/native target id" checks. - Use
resolveTargetfor provider-specific normalization fallback, not for broad directory search. - Keep provider-native ids like chat ids, thread ids, JIDs, handles, and room
ids inside
targetvalues or provider-specific params, not in generic SDK fields.
Config-backed directories
Plugins that derive directory entries from config should keep that logic in the
plugin and reuse the shared helpers from
openclaw/plugin-sdk/directory-runtime.
Use this when a channel needs config-backed peers/groups such as:
- allowlist-driven DM peers
- configured channel/group maps
- account-scoped static directory fallbacks
The shared helpers in directory-runtime only handle generic operations:
- query filtering
- limit application
- deduping/normalization helpers
- building
ChannelDirectoryEntry[]
Channel-specific account inspection and id normalization should stay in the plugin implementation.
Read-only channel inspection
If your plugin registers a channel, prefer implementing
plugin.config.inspectAccount(cfg, accountId) alongside resolveAccount(...).
Why:
resolveAccount(...)is the runtime path. It is allowed to assume credentials are fully materialized and can fail fast when required secrets are missing.- Read-only command paths such as
openclaw status,openclaw status --all,openclaw channels status,openclaw channels resolve, and doctor/config repair flows should not need to materialize runtime credentials just to describe configuration.
Recommended inspectAccount(...) behavior:
- Return descriptive account state only.
- Preserve
enabledandconfigured. - Include credential source/status fields when relevant, such as:
tokenSource,tokenStatusbotTokenSource,botTokenStatusappTokenSource,appTokenStatussigningSecretSource,signingSecretStatus
- You do not need to return raw token values just to report read-only
availability. Returning
tokenStatus: "available"(and the matching source field) is enough for status-style commands. - Use
configured_unavailablewhen a credential is configured via SecretRef but unavailable in the current command path.
This lets read-only commands report "configured but unavailable in this command path" instead of crashing or misreporting the account as not configured.