Skip to main content
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:
Callback payload fields:
  • status: "approved" or "denied"
  • decision: "allow-once", "allow-always", or "deny"
  • binding: the resolved binding for approved requests
  • request: 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:
  • presentation for semantic presentation blocks (text, context, divider, chart, table, buttons, select)
  • delivery-pin for 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 as direct, group, or channel before directory lookup. Implicit owner heartbeat delivery requires this direct classification; without it, Gateway status reports waiting 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.reservedLiterals lists 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 inferTargetChatType for category decisions that should happen before searching peers/groups.
  • Use looksLikeId for “treat this as an explicit/native target id” checks.
  • Use resolveTarget for provider-specific normalization fallback, not for broad directory search.
  • Keep provider-native ids like chat ids, thread ids, JIDs, handles, and room ids inside target values 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 enabled and configured.
  • Include credential source/status fields when relevant, such as:
    • tokenSource, tokenStatus
    • botTokenSource, botTokenStatus
    • appTokenSource, appTokenStatus
    • signingSecretSource, 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_unavailable when 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.