> ## Documentation Index
> Fetch the complete documentation index at: https://docs2.openclaw.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 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](/plugins/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:

```ts theme={"theme":{"light":"min-light","dark":"min-dark"}}
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 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](/plugins/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.
