message adapter from
openclaw/plugin-sdk/channel-message. The adapter describes the native message
lifecycle that the platform supports:
message tool. The plugin owns native send/edit/delete calls, target
normalization, platform threading, selected quotes, notification flags, account
state, and platform-specific side effects.
Use this page together with Building channel plugins.
The channel-message subpath is intentionally cheap enough for hot plugin
bootstrap files such as channel.ts: it exposes adapter contracts, capability
proofs, receipts, and compatibility facades without loading outbound delivery.
Runtime delivery helpers are available from
openclaw/plugin-sdk/channel-message-runtime for monitor/send code paths that
are already doing asynchronous message I/O.
Minimal adapter
Most new channel plugins can start with a small adapter:Outbound bridge
If the channel already has a compatibleoutbound adapter, prefer deriving the
message adapter instead of duplicating send code:
MessageReceipt values. New
code should pass receipts end to end and only derive legacy ids at compatibility
edges with listMessageReceiptPlatformIds(...) or
resolveMessageReceiptPrimaryId(...).
If no receive policy is supplied, createChannelMessageAdapterFromOutbound(...)
uses manual receive acknowledgement policy. That makes plugin-owned platform
acknowledgement explicit without changing channels that acknowledge webhooks,
sockets, or polling offsets outside generic receive context.
Message tool sends
The sharedmessage(action="send") path should use the same core delivery
lifecycle as final replies. If a channel needs provider-specific shaping for the
tool send, implement actions.prepareSendPayload(...) instead of sending from
actions.handleAction(...).
prepareSendPayload(...) receives the normalized core ReplyPayload plus the
full action context. Return a payload with channel-specific data in
payload.channelData.<channel> and let core call sendMessage(...),
deliverOutboundPayloads(...), the write-ahead queue, message-sending hooks,
retry, recovery, and ack cleanup.
Return null only when the send cannot be represented as a durable payload, for
example because it contains a non-serializable component factory. Core will keep
the legacy plugin action fallback for compatibility, but new channel send
features should be expressible as durable payload data.
payload.channelData.demo inside sendPayload.
This keeps platform-specific rendering in the plugin while core still owns
persist, retry, recover, hooks, and ack.
Prepared message(action="send") payloads and generic final-reply delivery use
core delivery with best-effort queueing by default. Required durable queueing is
only valid after core verifies the channel can reconcile a send whose outcome is
unknown after a crash. If the adapter cannot implement reconcileUnknownSend,
keep the prepared send path best-effort; core will still try the write-ahead
queue, but queue persistence or uncertain crash recovery is not part of the
required delivery contract.
Durable final capabilities
Durable final delivery is opt in per side effect. Core will only use generic durable delivery when the adapter declares every capability needed by the payload and delivery options.| Capability | Declare when |
|---|---|
text | The adapter can send text and return a receipt. |
media | Media sends return receipts for every visible platform message. |
payload | The adapter preserves rich reply payload semantics, not only text and one media URL. |
replyTo | Native reply targets reach the platform. |
thread | Native thread, topic, or channel thread targets reach the platform. |
silent | Notification suppression reaches the platform. |
nativeQuote | Selected quote metadata reaches the platform. |
messageSendingHooks | Core message-sending hooks can cancel or rewrite content before platform I/O. |
batch | Multi-part rendered batches are replayable as one durable plan. |
reconcileUnknownSend | The adapter can resolve unknown_after_send recovery without blind replay. |
afterSendSuccess | Channel-local after-send side effects run once. |
afterCommit | Channel-local after-commit side effects run once. |
reconcileUnknownSend; it uses the
shared lifecycle when the adapter preserves the payload’s visible semantics, and
falls back to direct platform I/O if queue persistence is unavailable. Required
durable final delivery must explicitly require reconcileUnknownSend. If the
adapter cannot determine whether a started/unknown send reached the platform,
do not declare that capability; core will reject required durable delivery
before queueing.
When a caller needs durable delivery, derive requirements instead of building
maps by hand:
messageSendingHooks is required by default. Set messageSendingHooks: false
only for a path that intentionally cannot run global message-sending hooks.
Durable send contract
A durable final send has stricter semantics than legacy channel-owned delivery:- Create the durable intent before platform I/O.
- If durable delivery returns a handled result, do not fall back to legacy send.
- Treat hook cancellation and no-send results as terminal.
- Treat
unsupportedas a pre-intent result only. - For required durability, fail before platform I/O if the queue cannot record that platform send has started.
- For required final delivery and required prepared message-tool sends,
preflight
reconcileUnknownSend; recovery must be able to ack an already-sent message or replay only after the adapter proves the original send did not happen. - For
best_effort, queue write failures may fall back to direct platform I/O. - Forward abort signals to media loading and platform sends.
- Run after-commit hooks after queue ack; direct best-effort fallback runs them after successful platform I/O because there is no durable queue commit.
- Return receipts for every visible platform message id.
- Use
reconcileUnknownSendwhen a platform can check whether an uncertain send already reached the user.
Receipts
MessageReceipt is the new internal record of what the platform accepted:
createMessageReceiptFromOutboundResults(...) when adapting an existing
send result. Use createPreviewMessageReceipt(...) when a live preview message
becomes the final receipt. Avoid adding new owner-local messageIds fields.
Legacy ChannelDeliveryResult.messageIds is still produced at compatibility
edges.
Live preview
Channels that stream draft previews or progress updates should declare live capabilities:defineFinalizableLivePreviewAdapter(...) and
deliverWithFinalizableLivePreviewAdapter(...) for runtime finalization. The
finalizer decides whether the final reply edits the preview in place, sends a
normal fallback, discards pending preview state, keeps an ambiguous failed edit
without duplicating the message, and returns the final receipt.
Receive ack policy
Inbound receivers that control platform acknowledgement timing should declare receive policy:| Policy | Use when |
|---|---|
after_receive_record | The platform can be acknowledged after the inbound event is parsed and recorded. |
after_agent_dispatch | The platform should wait until the agent dispatch has been accepted. |
after_durable_send | The platform should wait until final delivery has a durable decision. |
manual | The plugin owns acknowledgement because platform semantics do not match a generic stage. |
createMessageReceiveContext(...) in receivers that defer ack state, and
shouldAckMessageAfterStage(...) when the receiver needs to test whether a
stage has satisfied the configured policy.
Contract tests
Capability declarations are part of the plugin contract. Back them with tests:Deprecated compatibility APIs
These APIs remain importable for third-party compatibility. Do not use them for new channel code.| Deprecated API | Replacement |
|---|---|
openclaw/plugin-sdk/channel-reply-pipeline | openclaw/plugin-sdk/channel-message |
createChannelTurnReplyPipeline(...) | createChannelMessageReplyPipeline(...) for compatibility dispatchers, or a message adapter for new channel code |
deliverDurableInboundReplyPayload(...) | deliverInboundReplyWithMessageSendContext(...) from openclaw/plugin-sdk/channel-message-runtime |
dispatchInboundReplyWithBase(...) | dispatchChannelMessageReplyWithBase(...) only for compatibility dispatchers |
recordInboundSessionAndDispatchReply(...) | recordChannelMessageReplyDispatch(...) only for compatibility dispatchers |
resolveChannelSourceReplyDeliveryMode(...) | resolveChannelMessageSourceReplyDeliveryMode(...) |
deliverFinalizableDraftPreview(...) | defineFinalizableLivePreviewAdapter(...) plus deliverWithFinalizableLivePreviewAdapter(...) |
DraftPreviewFinalizerDraft | LivePreviewFinalizerDraft |
DraftPreviewFinalizerResult | LivePreviewFinalizerResult |
createReplyPrefixContext(...),
createReplyPrefixOptions(...), and createTypingCallbacks(...) through the
message facade. New lifecycle code should avoid the old
channel-reply-pipeline subpath.
Migration checklist
- Add
message: defineChannelMessageAdapter(...)ormessage: createChannelMessageAdapterFromOutbound(...)to the channel plugin. - Return
MessageReceiptfrom text, media, and payload sends. - Declare only capabilities backed by native behavior and tests.
- Replace hand-written durable requirement maps with
deriveDurableFinalDeliveryRequirements(...). - Move preview finalization through the live preview helpers when the channel edits draft messages in place.
- Declare receive ack policy only when the receiver can really defer platform acknowledgement.
- Keep legacy reply dispatch helpers only at compatibility edges.