Skip to main content

Channel adapters

A channel adapter connects WednesdayAI to a messaging platform. A channel is a plugin that exposes a ChannelPlugin object — a composition of small adapter interfaces, each owning one concern (auth, messaging, outbound delivery, status, setup, onboarding, and more). You implement only the adapters your platform needs; the rest are optional.
ChannelPlugin is not a flat object with login / logout / send / handleEvent methods. Those names are not part of the contract. The real model is a set of named adapter slots, each typed by its own interface and re-exported from openclaw/plugin-sdk.

The ChannelPlugin shape

import type { ChannelPlugin } from "openclaw/plugin-sdk";

const myChannel: ChannelPlugin<MyResolvedAccount> = {
  id: "my-channel", // ChannelId — must match the plugin id and directory
  meta: {
    /* display name, labels, channel metadata */
  },
  capabilities: {
    /* what the channel supports: media, threads, reactions, etc. */
  },
  config: myConfigAdapter, // required: resolves accounts from config
  // Optional adapter slots — implement what your platform needs:
  auth: myAuthAdapter,
  setup: mySetupAdapter,
  onboarding: myOnboardingAdapter,
  messaging: myMessagingAdapter,
  outbound: myOutboundAdapter,
  status: myStatusAdapter,
  security: mySecurityAdapter,
  groups: myGroupAdapter,
  mentions: myMentionAdapter,
  // ...and more (see the adapter slots table)
};

export default myChannel;
ChannelPlugin<ResolvedAccount, Probe, Audit> is generic. ResolvedAccount is the per-account type your config adapter resolves; Probe and Audit are channel-specific status types.

Adapter slots

SlotAdapter typeOwns
config (required)ChannelConfigAdapterResolving accounts from config
meta (required)ChannelMetaDisplay name and channel metadata
capabilities (required)ChannelCapabilitiesDeclared feature support
authChannelAuthAdapterLogin/logout, credential handling
setupChannelSetupAdapterFirst-run setup
onboardingChannelOnboardingAdapterCLI onboarding wizard steps
messagingChannelMessagingAdapterInbound message handling
outboundChannelOutboundAdapterDelivering outbound replies
statusChannelStatusAdapterConnectivity/health status
securityChannelSecurityAdapterDM policy, allowlist enforcement
pairingChannelPairingAdapterPairing flow
groupsChannelGroupAdapterGroup/channel handling
mentionsChannelMentionAdapterMention gating
threadingChannelThreadingAdapterThreads/topics
streamingChannelStreamingAdapterStreaming/partial replies
commandsChannelCommandAdapterChannel-specific commands
directoryChannelDirectoryAdapterPeer/group directory listing
resolverChannelResolverAdapterTarget resolution
actionsChannelMessageActionAdapterMessage actions
heartbeatChannelHeartbeatAdapterHeartbeat delivery
gatewayChannelGatewayAdapterGateway-side methods
agentToolsChannelAgentToolFactory or ChannelAgentTool[]Channel-owned agent tools
Each adapter type is imported from openclaw/plugin-sdk. Implement an adapter in its own file (src/auth.ts, src/messaging.ts, etc.) and compose them in the plugin object.

Registering the channel

A channel plugin is registered from the plugin’s register function via api.registerChannel(...):
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { myChannel } from "./channel.js";

export default function register(api: OpenClawPluginApi): void {
  api.registerChannel(myChannel);
  // Or, with an optional dock:
  // api.registerChannel({ plugin: myChannel, dock: myDock });
}

Repo layout

Channel plugins live under extensions/ as pnpm workspace packages:
extensions/my-channel/
├── package.json
├── openclaw.plugin.json   # manifest (id + configSchema)
├── src/
│   ├── index.ts           # register() — calls api.registerChannel()
│   ├── channel.ts         # the ChannelPlugin object
│   ├── auth.ts            # ChannelAuthAdapter
│   ├── messaging.ts       # ChannelMessagingAdapter
│   └── outbound.ts        # ChannelOutboundAdapter
└── vitest.config.ts

package.json requirements

{
  "name": "@wednesdayai/my-channel",
  "version": "1.0.0",
  "type": "module",
  "openclaw": {
    "id": "my-channel"
  },
  "peerDependencies": {
    "openclaw": "2026.3.2"
  },
  "devDependencies": {
    "openclaw": "2026.3.2"
  }
}
Rules:
  • Plugin id, directory name, and npm package name must match exactly.
  • Pin openclaw to exactly 2026.3.2 (the fork base) in both peerDependencies and devDependencies. Do not use a range like >=2026.3.2 — plugins built against a newer openclaw may import APIs that do not exist in this fork.
  • openclaw goes in devDependencies / peerDependencies, never dependencies.
  • No workspace:* in dependencies.
The package.json "openclaw" key carries the id used for workspace discovery. A separate openclaw.plugin.json manifest (with id and configSchema) is still required at the plugin root for config validation. See the manifest reference.

Account model

The ResolvedAccount type parameter is the per-account record your config adapter resolves from configuration. For channels that store credentials, keep them under ~/.openclaw/credentials/<channel>/ and use the SDK credential and config helpers rather than writing files directly.

Testing a channel adapter

Test each adapter in isolation. Mock the platform client; do not make live API calls in unit tests.
pnpm --filter @wednesdayai/my-channel test
Live integration tests (require credentials) run under a dedicated live script:
MY_CHANNEL_TOKEN=... pnpm --filter @wednesdayai/my-channel test:live

Docs and PR checklist for a new channel

When adding a channel, update every surface that lists channels (onboarding/overview docs, control UI, mobile/macOS app where applicable) and add matching status and configuration forms.
  • ChannelPlugin composes the adapters the platform needs (config, meta, capabilities at minimum)
  • Registered via api.registerChannel(...)
  • Package name @wednesdayai/<channel>; id matches directory name
  • openclaw pinned to exactly 2026.3.2; no workspace:* in dependencies
  • openclaw.plugin.json present with id + configSchema
  • Unit tests pass: pnpm test:fast
  • Channel doc added under docs/channels/ and indexed
  • CHANGELOG.md updated; dev log at docs/logs/YYYY-MM-DD-<name>-channel.md

What’s next