Message presentation
기준일: 2026-07-26
공식 기준: Message presentation
Message presentation 문서는 OpenClaw 공식 문서(plugins/message-presentation)를 한국어로 정리한 가이드입니다. Semantic message cards, charts, tables, controls, fallback text, and delivery hints for channel plugins 명령·설정 키·코드 예시는 공식 문서를 그대로 보존하며, 해석과 절차 안내는 한국어로 제공합니다. 최종 동작은 설치된 CLI 버전과 공식 원문을 확인하세요.
핵심 요약
Semantic message cards, charts, tables, controls, fallback text, and delivery hints for channel plugins
한국어 가이드 범위: plugins/message-presentation 경로의 설정·명령·제약·예시를 학습용으로 재구성합니다.
문서 구성
공식 문서의 주요 섹션은 다음과 같습니다.
- Contract
- Producer examples
- Renderer contract
- Core render flow
- Degradation rules
- Button value fallback visibility
- Provider mapping
- Presentation vs InteractiveReply
- Delivery pin
- Plugin author checklist
- Related docs
상세 내용
본문
Message presentation is OpenClaw's shared contract for rich outbound chat UI. It lets agents, CLI commands, approval flows, and plugins describe the message intent once, while each channel plugin renders the best native shape it can.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Contract
Plugin authors import the public contract from:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
action.type: "command"runs a native slash command through core's commandaction.type: "callback"carries opaque plugin data through the channel'saction.type: "approval"identifies one durable operator approval, itsaction.type: "question"identifies one choice for a live, runtime-authoredaction.type: "url"opens a normal link.action.type: "web-app"launches a channel-native web app. Seturlfor avalueis the legacy opaque callback value. New controls should useactionurl,webApp, andweb_appremain accepted as deprecated boundary inputs.labelis required and is also used in text fallback.styleis advisory. Renderers should map unsupported styles to a safepriorityis optional. When a channel advertises action limits and controlsdisabledis optional. Channels must opt in withsupportsDisabled; otherwisereusableis optional. Channels that support reusable native callbacks mayoptions[].actionaccepts onlycommandorcallback; approval and link actions are button-only.options[].valueis the legacy selected application value.placeholderis advisory and may be ignored by channels without native- If a channel does not support selects, fallback text lists the labels.
pierequires positive segment values.bar,area, andlineuse one orderedcategoriesarray. Every series- Category labels and series names must be unique. Invalid or incomplete chart
- Native chart rendering is opt-in through
presentationCapabilities.charts. captionis a required short heading.headersmust contain at least onerowsmust contain at least one row. Every row must have exactly one cell perrowHeaderColumnIndexis an optional zero-based index identifying the column- Table normalization is atomic. An invalid caption, header, row width, cell,
MessagePresentation,
ReplyPayloadDelivery,
} from "openclaw/plugin-sdk/interactive-runtime";
type MessagePresentation = {
title?: string;
tone?: "neutral" | "info" | "success" | "warning" | "danger";
blocks: MessagePresentationBlock[];
};
type MessagePresentationBlock =
| { type: "text"; text: string }
| { type: "context"; text: string }
| { type: "divider" }
| { type: "buttons"; buttons: MessagePresentationButton[] }
| { type: "select"; placeholder?: string; options: MessagePresentationOption[] }
| {
type: "chart";
chartType: "pie";
title: string;
segments: Array<{ label: string; value: number }>;
}
| {
type: "chart";
chartType: "bar" | "area" | "line";
title: string;
categories: string[];
series: Array<{ name: string; values: number[] }>;
xLabel?: string;
yLabel?: string;
}
| {
type: "table";
caption: string;
headers: string[];
rows: Array<Array<string | number>>;
rowHeaderColumnIndex?: number;
};
type MessagePresentationAction =
| { type: "command"; command: string }
| { type: "callback"; value: string }
| {
type: "approval";
approvalId: string;
approvalKind: "exec" | "plugin";
decision: "allow-once" | "allow-always" | "deny";
}
| {
type: "question";
questionId: string;
optionValue: string;
}
| { type: "url"; url: string }
| {
type: "web-app";
url: string;
widgetId?: string;
}
| {
type: "web-app";
url?: string;
widgetId: string;
};
type MessagePresentationButton = {
label: string;
action?: MessagePresentationAction;
/** Legacy callback value. Prefer action for new controls. */
value?: string;
/** @deprecated Use an action with type "url". */
url?: string;
/** @deprecated Use an action with type "web-app". */
webApp?: { url: string };
/** @deprecated Use an action with type "web-app". */
web_app?: { url: string };
priority?: number;
disabled?: boolean;
reusable?: boolean;
style?: "primary" | "secondary" | "success" | "danger";
};
type MessagePresentationOption = {
label: string;
action?: Extract<MessagePresentationAction, { type: "command" | "callback" }>;
/** Legacy callback value. Prefer action for new controls. */
value?: string;
};
type ReplyPayloadDelivery = {
pin?:
| boolean
| {
enabled: boolean;
notify?: boolean;
required?: boolean;
};
};
Open pipeline (table)
- Account: Acme; Stage: Won; ARR: 125000
- Account: Globex; Stage: Review; ARR: 82000
Producer examples
{
"title": "Deploy approval",
"tone": "warning",
"blocks": [
{ "type": "text", "text": "Canary is ready to promote." },
{ "type": "context", "text": "Build 1234, staging passed." },
{
"type": "buttons",
"buttons": [
{
"label": "Approve",
"action": { "type": "callback", "value": "deploy:approve" },
"style": "success"
},
{
"label": "Decline",
"action": { "type": "callback", "value": "deploy:decline" },
"style": "danger"
}
]
}
]
}
{
"blocks": [
{ "type": "text", "text": "Release notes are ready." },
{
"type": "buttons",
"buttons": [
{
"label": "Open notes",
"action": { "type": "url", "url": "https://example.com/release" }
}
]
}
]
}
{
"blocks": [
{
"type": "buttons",
"buttons": [
{
"label": "Launch",
"action": { "type": "web-app", "url": "https://example.com/app" }
}
]
}
]
}
{
"title": "Choose environment",
"blocks": [
{
"type": "select",
"placeholder": "Environment",
"options": [
{ "label": "Canary", "value": "env:canary" },
{ "label": "Production", "value": "env:prod" }
]
}
]
}
Renderer contract
Channel plugins declare render support on their outbound adapter:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
const adapter: ChannelOutboundAdapter = {
deliveryMode: "direct",
presentationCapabilities: {
supported: true,
buttons: true,
selects: true,
context: true,
divider: true,
charts: false,
tables: false,
limits: {
actions: {
maxActions: 25,
maxActionsPerRow: 5,
maxRows: 5,
maxLabelLength: 80,
maxValueBytes: 100,
supportsStyles: true,
supportsDisabled: false,
},
selects: {
maxOptions: 25,
maxLabelLength: 100,
maxValueBytes: 100,
},
text: {
maxLength: 2000,
encoding: "characters",
markdownDialect: "discord-markdown",
},
},
},
deliveryCapabilities: {
pin: true,
},
renderPresentation({ payload, presentation, ctx }) {
return renderNativePayload(payload, presentation, ctx);
},
async pinDeliveredMessage({ target, messageId, pin }) {
await pinNativeMessage(target, messageId, { notify: pin.notify === true });
},
};
type ChannelPresentationCapabilities = {
supported?: boolean;
buttons?: boolean;
selects?: boolean;
context?: boolean;
divider?: boolean;
charts?: boolean;
tables?: boolean;
limits?: {
actions?: {
maxActions?: number;
maxActionsPerRow?: number;
maxRows?: number;
maxLabelLength?: number;
maxValueBytes?: number;
supportsStyles?: boolean;
supportsDisabled?: boolean;
supportsLayoutHints?: boolean;
};
selects?: {
maxOptions?: number;
maxLabelLength?: number;
maxValueBytes?: number;
};
text?: {
maxLength?: number;
encoding?: "characters" | "utf8-bytes" | "utf16-units";
markdownDialect?: "plain" | "markdown" | "html" | "slack-mrkdwn" | "discord-markdown";
supportsEdit?: boolean;
};
};
};
Core render flow
On the canonical outbound path used by CLI and standard message actions, core:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
Degradation rules
Presentation must be safe to send on limited channels.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
titleas the first linetextblocks as normal paragraphscontextblocks as compact context linesdividerblocks as a visual separator- button labels, including URLs for link buttons
- select option labels
- chart title, type, axes, categories, series, and values
- table caption, headers, and every row value
Button value fallback visibility
When a channel cannot render interactive controls, button and select values fall back to plain text. The fallback behavior preserves usability while keeping opaque callback data private:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
command-typed actions render aslabel: `command`so users cancallback-typed actions and legacyvaluefields render asapproval-typed actions render label-only. Approval IDs and decisions areurlactions, URL-backedweb-appactions, and deprecated **url/- Select options render as label-only. The underlying option value is not
- Telegram with inline buttons disabled sends text fallback.
- A channel without select support lists select options as text.
- A channel without native chart support lists the chart data as text.
- A channel without native table support lists every table row as text.
- A URL-only button becomes either a native link button or a fallback URL line.
- Optional pin failures do not fail the delivered message.
Provider mapping
Provider-native payload compatibility is a transition affordance for existing reply producers. It is not a reason to add new shared native fields.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
| Channel | Native render target | Notes |
|---|---|---|
| Discord | Components and component containers | Preserves legacy channelData.discord.components for existing provider-native payload producers, but new shared sends should use presentation. |
| Feishu | Interactive cards | Card header can use title; body avoids duplicating that title. |
| Matrix | Text fallback plus structured event field | Buttons/selects advertise as supported, but every block currently renders as renderMessagePresentationFallbackText output carried in a com.openclaw.presentation event field, not native interactive widgets. |
| Mattermost | Text plus interactive props | Selects and dividers are not supported; those blocks degrade to text. |
| Microsoft Teams | Adaptive Cards | Plain message text is included with the card when both are provided. Selects, styles, and disabled state are not supported. |
| Slack | Block Kit | Renders chart as native data_visualization and table as native data_table; preserves legacy channelData.slack.blocks, but new shared sends should use presentation. |
| Telegram | Text plus inline keyboards | Buttons/selects require inline button capability for the target surface; otherwise text fallback is used. |
| Plain channels | Text fallback | Channels without a renderer still get readable output. |
Presentation vs InteractiveReply
InteractiveReply is the older internal subset used by approval and interaction helpers. It supports:
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- text
- buttons
- selects
- title
- tone
- context
- divider
- chart
- table
- URL-only buttons
- generic delivery metadata through
ReplyPayload.delivery normalizeMessagePresentation(raw)/hasMessagePresentationBlocks(value)isMessagePresentationInteractiveBlock(block)narrows a block to theresolveMessagePresentationButtonAction(button)andresolveMessagePresentationActionValue(action)/renderMessagePresentationChartFallbackText(block)/InteractiveReply,InteractiveReplyBlock,InteractiveReplyButton, andnormalizeInteractiveReply(...)hasInteractiveReplyBlocks(...)interactiveReplyToPresentation(...)presentationToInteractiveReply(...)presentationToInteractiveControlsReply(...)resolveInteractiveTextFallback(...)reduceInteractiveReply(...)- use
buildApprovalPresentation(...)instead of
adaptMessagePresentationForChannel,
applyPresentationActionLimits,
hasMessagePresentationBlocks,
interactiveReplyToPresentation,
isMessagePresentationInteractiveBlock,
normalizeMessagePresentation,
presentationPageSize,
presentationToInteractiveControlsReply,
presentationToInteractiveReply,
renderMessagePresentationChartFallbackText,
renderMessagePresentationFallbackText,
renderMessagePresentationTableFallbackText,
resolveMessagePresentationActionValue,
resolveMessagePresentationButtonAction,
resolveMessagePresentationControlValue,
resolveMessagePresentationOptionAction,
} from "openclaw/plugin-sdk/interactive-runtime";
Delivery pin
Pinning is delivery behavior, not presentation. Use delivery.pin instead of provider-native fields such as channelData.telegram.pin.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
pin: truepins the first successfully delivered message.pin.notifydefaults tofalse.pin.requireddefaults tofalse.- Optional pin failures degrade and leave the sent message intact.
- Required pin failures fail delivery.
- Chunked messages pin the first delivered chunk, not the tail chunk.
Plugin author checklist
render or safely degrade semantic presentation.
위 내용은 공식 문서의 해당 섹션 요지입니다. 세부 플래그·기본값은 원문과
--help를 확인하세요.
주요 항목:
- Declare
presentationfromdescribeMessageTool(...)when the channel can - Add
presentationCapabilitiesto the runtime outbound adapter. - Implement
renderPresentationin runtime code, not control-plane plugin - Keep native UI libraries out of hot setup/catalog paths.
- Declare generic capability limits on
presentationCapabilities.limitswhen - Preserve final platform limits in the renderer and tests.
- Add fallback tests for unsupported charts, tables, buttons, selects, URL
- Add delivery pin support through
deliveryCapabilities.pinand - Do not expose new provider-native card/block/component/button fields through
Related docs
주요 항목:
- Message CLI
- Plugin SDK Overview
- Plugin Architecture
- Channel Presentation Refactor Plan
실습 체크리스트
- 공식 문서와 로컬 버전을 대조합니다:
https://docs.openclaw.ai/plugins/message-presentation - 관련 CLI는
openclaw --help및 하위 명령--help로 옵션을 확인합니다. - 설정 변경 시
openclaw config/openclaw doctor로 유효성을 검사합니다. - Gateway·채널·플러그인 변경 후에는 필요 시 Gateway를 재시작합니다.
자주 쓰는 명령·설정 예시
MessagePresentation,
ReplyPayloadDelivery,
} from "openclaw/plugin-sdk/interactive-runtime";
type MessagePresentation = {
title?: string;
tone?: "neutral" | "info" | "success" | "warning" | "danger";
blocks: MessagePresentationBlock[];
};
type MessagePresentationBlock =
| { type: "text"; text: string }
| { type: "context"; text: string }
| { type: "divider" }
| { type: "buttons"; buttons: MessagePresentationButton[] }
| { type: "select"; placeholder?: string; options: MessagePresentationOption[] }
| {
type: "chart";
chartType: "pie";
title: string;
segments: Array<{ label: string; value: number }>;
}
| {
type: "chart";
chartType: "bar" | "area" | "line";
title: string;
categories: string[];
series: Array<{ name: string; values: number[] }>;
xLabel?: string;
yLabel?: string;
}
| {
type: "table";
caption: string;
headers: string[];
rows: Array<Array<string | number>>;
rowHeaderColumnIndex?: number;
};
type MessagePresentationAction =
| { type: "command"; command: string }
| { type: "callback"; value: string }
| {
type: "approval";
approvalId: string;
approvalKind: "exec" | "plugin";
decision: "allow-once" | "allow-always" | "deny";
}
| {
type: "question";
questionId: string;
optionValue: string;
}
| { type: "url"; url: string }
| {
type: "web-app";
url: string;
widgetId?: string;
}
| {
type: "web-app";
url?: string;
widgetId: string;
};
type MessagePresentationButton = {
label: string;
action?: MessagePresentationAction;
/** Legacy callback value. Prefer action for new controls. */
value?: string;
/** @deprecated Use an action with type "url". */
url?: string;
/** @deprecated Use an action with type "web-app". */
webApp?: { url: string };
/** @deprecated Use an action with type "web-app". */
web_app?: { url: string };
priority?: number;
disabled?: boolean;
reusable?: boolean;
style?: "primary" | "secondary" | "success" | "danger";
};
type MessagePresentationOption = {
label: string;
action?: Extract<MessagePresentationAction, { type: "command" | "callback" }>;
/** Legacy callback value. Prefer action for new controls. */
value?: string;
};
type ReplyPayloadDelivery = {
pin?:
| boolean
| {
enabled: boolean;
notify?: boolean;
required?: boolean;
};
};
Open pipeline (table)
- Account: Acme; Stage: Won; ARR: 125000
- Account: Globex; Stage: Review; ARR: 82000
{
"title": "Deploy approval",
"tone": "warning",
"blocks": [
{ "type": "text", "text": "Canary is ready to promote." },
{ "type": "context", "text": "Build 1234, staging passed." },
{
"type": "buttons",
"buttons": [
{
"label": "Approve",
"action": { "type": "callback", "value": "deploy:approve" },
"style": "success"
},
{
"label": "Decline",
"action": { "type": "callback", "value": "deploy:decline" },
"style": "danger"
}
]
}
]
}
{
"blocks": [
{ "type": "text", "text": "Release notes are ready." },
{
"type": "buttons",
"buttons": [
{
"label": "Open notes",
"action": { "type": "url", "url": "https://example.com/release" }
}
]
}
]
}
{
"blocks": [
{
"type": "buttons",
"buttons": [
{
"label": "Launch",
"action": { "type": "web-app", "url": "https://example.com/app" }
}
]
}
]
}
관련 링크
이 가이드는 공식 문서를 한국어 학습용으로 재구성한 것입니다. 옵션 기본값·플래그 이름은 설치 버전에 따라 달라질 수 있습니다.