chore(sync): mirror docs from openclaw/openclaw@674feda214

This commit is contained in:
openclaw-docs-sync[bot] 2026-04-21 20:30:41 +00:00
parent ca718133fb
commit 72ae355e0e
11 changed files with 1388 additions and 45 deletions

View File

@ -1,5 +1,5 @@
{
"repository": "openclaw/openclaw",
"sha": "78030d0d52bbe9aa191fa3ad422a44531e585f2f",
"syncedAt": "2026-04-21T20:27:23.543Z"
"sha": "674feda214f8151f7fd5a2aff1d10162993cf6c1",
"syncedAt": "2026-04-21T20:30:40.370Z"
}

View File

@ -1,2 +1,2 @@
f135ddc1802b7f8b2d29bf495fd0ac1f497a89bab8164ca8c7c8f18efc010e6e plugin-sdk-api-baseline.json
a47d06095ec5c3701a94888a11e89700d8a8511db46fa3122fb9407e160707b6 plugin-sdk-api-baseline.jsonl
cfeee4630cb43ffc4d702f207d28d35962c6458aa8fd2b1671c35e0be158bb35 plugin-sdk-api-baseline.json
af4fbf19861c6ec000b41ac5a3ded597700e45bb15f8b1d74bb2d1f550bd09b6 plugin-sdk-api-baseline.jsonl

View File

@ -611,7 +611,7 @@ Teams markdown is more limited than Slack or Discord:
- Basic formatting works: **bold**, _italic_, `code`, links
- Complex markdown (tables, nested lists) may not render correctly
- Adaptive Cards are supported for polls and arbitrary card sends (see below)
- Adaptive Cards are supported for polls and semantic presentation sends (see below)
## Configuration
@ -783,11 +783,11 @@ OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API)
- The gateway must stay online to record votes.
- Polls do not auto-post result summaries yet (inspect the store file if needed).
## Adaptive Cards (arbitrary)
## Presentation Cards
Send any Adaptive Card JSON to Teams users or conversations using the `message` tool or CLI.
Send semantic presentation payloads to Teams users or conversations using the `message` tool or CLI. OpenClaw renders them as Teams Adaptive Cards from the generic presentation contract.
The `card` parameter accepts an Adaptive Card JSON object. When `card` is provided, the message text is optional.
The `presentation` parameter accepts semantic blocks. When `presentation` is provided, the message text is optional.
**Agent tool:**
@ -796,10 +796,9 @@ The `card` parameter accepts an Adaptive Card JSON object. When `card` is provid
action: "send",
channel: "msteams",
target: "user:<id>",
card: {
type: "AdaptiveCard",
version: "1.5",
body: [{ type: "TextBlock", text: "Hello!" }],
presentation: {
title: "Hello",
blocks: [{ type: "text", text: "Hello!" }],
},
}
```
@ -809,10 +808,10 @@ The `card` parameter accepts an Adaptive Card JSON object. When `card` is provid
```bash
openclaw message send --channel msteams \
--target "conversation:19:abc...@thread.tacv2" \
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}'
--presentation '{"title":"Hello","blocks":[{"type":"text","text":"Hello!"}]}'
```
See [Adaptive Cards documentation](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below.
For target format details, see [Target formats](#target-formats) below.
## Target formats
@ -837,9 +836,9 @@ openclaw message send --channel msteams --target "user:John Smith" --message "He
# Send to a group chat or channel
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello"
# Send an Adaptive Card to a conversation
# Send a presentation card to a conversation
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}'
--presentation '{"title":"Hello","blocks":[{"type":"text","text":"Hello"}]}'
```
**Agent tool examples:**
@ -858,10 +857,9 @@ openclaw message send --channel msteams --target "conversation:19:abc...@thread.
action: "send",
channel: "msteams",
target: "conversation:19:abc...@thread.tacv2",
card: {
type: "AdaptiveCard",
version: "1.5",
body: [{ type: "TextBlock", text: "Hello" }],
presentation: {
title: "Hello",
blocks: [{ type: "text", text: "Hello" }],
},
}
```

View File

@ -803,7 +803,8 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \
Telegram send also supports:
- `--buttons` for inline keyboards when `channels.telegram.capabilities.inlineButtons` allows it
- `--presentation` with `buttons` blocks for inline keyboards when `channels.telegram.capabilities.inlineButtons` allows it
- `--pin` or `--delivery '{"pin":true}'` to request pinned delivery when the bot can pin in that chat
- `--force-document` to send outbound images and GIFs as documents instead of compressed photo or animated-media uploads
Action gating:

View File

@ -67,15 +67,13 @@ Name lookup:
- `send`
- Channels: WhatsApp/Telegram/Discord/Google Chat/Slack/Mattermost (plugin)/Signal/iMessage/Matrix/Microsoft Teams
- Required: `--target`, plus `--message` or `--media`
- Optional: `--media`, `--interactive`, `--buttons`, `--components`, `--card`, `--reply-to`, `--thread-id`, `--gif-playback`, `--force-document`, `--silent`
- Shared interactive payloads: `--interactive` sends a channel-native interactive JSON payload when supported
- Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it)
- Required: `--target`, plus `--message`, `--media`, or `--presentation`
- Optional: `--media`, `--presentation`, `--delivery`, `--pin`, `--reply-to`, `--thread-id`, `--gif-playback`, `--force-document`, `--silent`
- Shared presentation payloads: `--presentation` sends semantic blocks (`text`, `context`, `divider`, `buttons`, `select`) that core renders through the selected channel's declared capabilities. See [Message Presentation](/plugins/message-presentation).
- Generic delivery preferences: `--delivery` accepts delivery hints such as `{ "pin": true }`; `--pin` is shorthand for pinned delivery when the channel supports it.
- Telegram only: `--force-document` (send images and GIFs as documents to avoid Telegram compression)
- Telegram only: `--thread-id` (forum topic id)
- Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field)
- Discord only: `--components` JSON payload
- Adaptive-card channels: `--card` JSON payload when supported
- Telegram + Discord: `--silent`
- WhatsApp only: `--gif-playback`
@ -208,22 +206,22 @@ openclaw message send --channel discord \
--target channel:123 --message "hi" --reply-to 456
```
Send a Discord message with components:
Send a message with semantic buttons:
```
openclaw message send --channel discord \
--target channel:123 --message "Choose:" \
--components '{"text":"Choose a path","blocks":[{"type":"actions","buttons":[{"label":"Approve","style":"success"},{"label":"Decline","style":"danger"}]}]}'
--presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Approve","value":"approve","style":"success"},{"label":"Decline","value":"decline","style":"danger"}]}]}'
```
See [Discord components](/channels/discord#interactive-components) for the full schema.
Core renders the same `presentation` payload into Discord components, Slack blocks, Telegram inline buttons, Mattermost props, or Teams/Feishu cards depending on channel capability. See [Message Presentation](/plugins/message-presentation) for the full contract and fallback rules.
Send a shared interactive payload:
Send a richer presentation payload:
```bash
openclaw message send --channel googlechat --target spaces/AAA... \
--message "Choose:" \
--interactive '{"text":"Choose a path","blocks":[{"type":"actions","buttons":[{"label":"Approve"},{"label":"Decline"}]}]}'
--presentation '{"title":"Deploy approval","tone":"warning","blocks":[{"type":"text","text":"Choose a path"},{"type":"buttons","buttons":[{"label":"Approve","value":"approve"},{"label":"Decline","value":"decline"}]}]}'
```
Create a Discord poll:
@ -277,19 +275,19 @@ openclaw message react --channel signal \
--emoji "✅" --target-author-uuid 123e4567-e89b-12d3-a456-426614174000
```
Send Telegram inline buttons:
Send Telegram inline buttons through generic presentation:
```
openclaw message send --channel telegram --target @mychat --message "Choose:" \
--buttons '[ [{"text":"Yes","callback_data":"cmd:yes"}], [{"text":"No","callback_data":"cmd:no"}] ]'
--presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Yes","value":"cmd:yes"},{"label":"No","value":"cmd:no"}]}]}'
```
Send a Teams Adaptive Card:
Send a Teams card through generic presentation:
```bash
openclaw message send --channel msteams \
--target conversation:19:abc@thread.tacv2 \
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Status update"}]}'
--presentation '{"title":"Status update","blocks":[{"type":"text","text":"Build completed"}]}'
```
Send a Telegram image as a document to avoid compression:

254
docs/plan/ui-channels.md Normal file
View File

@ -0,0 +1,254 @@
---
title: Channel Presentation Refactor Plan
summary: Decouple semantic message presentation from channel native UI renderers.
read_when:
- Refactoring channel message UI, interactive payloads, or native channel renderers
- Changing message tool capabilities, delivery hints, or cross-context markers
- Debugging Discord Carbon import fanout or channel plugin runtime laziness
---
# Channel Presentation Refactor Plan
## Status
Implemented for the shared agent, CLI, plugin capability, and outbound delivery surfaces:
- `ReplyPayload.presentation` carries semantic message UI.
- `ReplyPayload.delivery.pin` carries sent-message pin requests.
- Shared message actions expose `presentation`, `delivery`, and `pin` instead of provider-native `components`, `blocks`, `buttons`, or `card`.
- Core renders or auto-degrades presentation through plugin-declared outbound capabilities.
- Discord, Slack, Telegram, Mattermost, MS Teams, and Feishu renderers consume the generic contract.
- Discord channel control-plane code no longer imports Carbon-backed UI containers.
Canonical docs now live in [Message Presentation](/plugins/message-presentation).
Keep this plan as historical implementation context; update the canonical guide
for contract, renderer, or fallback behavior changes.
## Problem
Channel UI is currently split across several incompatible surfaces:
- Core owns a Discord-shaped cross-context renderer hook through `buildCrossContextComponents`.
- Discord `channel.ts` can import native Carbon UI through `DiscordUiContainer`, which pulls runtime UI dependencies into the channel plugin control plane.
- The agent and CLI expose native payload escape hatches such as Discord `components`, Slack `blocks`, Telegram or Mattermost `buttons`, and Teams or Feishu `card`.
- `ReplyPayload.channelData` carries both transport hints and native UI envelopes.
- The generic `interactive` model exists, but it is narrower than the richer layouts already used by Discord, Slack, Teams, Feishu, LINE, Telegram, and Mattermost.
This makes core aware of native UI shapes, weakens plugin runtime laziness, and gives agents too many provider-specific ways to express the same message intent.
## Goals
- Core decides the best semantic presentation for a message from declared capabilities.
- Extensions declare capabilities and render semantic presentation into native transport payloads.
- Web Control UI remains separate from chat native UI.
- Native channel payloads are not exposed through the shared agent or CLI message surface.
- Unsupported presentation features auto-degrade to the best text representation.
- Delivery behavior such as pinning a sent message is generic delivery metadata, not presentation.
## Non Goals
- No backwards compatibility shim for `buildCrossContextComponents`.
- No public native escape hatches for `components`, `blocks`, `buttons`, or `card`.
- No core imports of channel-native UI libraries.
- No provider-specific SDK seams for bundled channels.
## Target Model
Add a core-owned `presentation` field to `ReplyPayload`.
```ts
type MessagePresentationTone = "neutral" | "info" | "success" | "warning" | "danger";
type MessagePresentation = {
tone?: MessagePresentationTone;
title?: string;
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 MessagePresentationButton = {
label: string;
value?: string;
url?: string;
style?: "primary" | "secondary" | "success" | "danger";
};
type MessagePresentationOption = {
label: string;
value: string;
};
```
`interactive` becomes a subset of `presentation` during migration:
- `interactive` text block maps to `presentation.blocks[].type = "text"`.
- `interactive` buttons block maps to `presentation.blocks[].type = "buttons"`.
- `interactive` select block maps to `presentation.blocks[].type = "select"`.
The external agent and CLI schemas now use `presentation`; `interactive` remains an internal legacy parser/rendering helper for existing reply producers.
## Delivery Metadata
Add a core-owned `delivery` field for send behavior that is not UI.
```ts
type ReplyPayloadDelivery = {
pin?:
| boolean
| {
enabled: boolean;
notify?: boolean;
required?: boolean;
};
};
```
Semantics:
- `delivery.pin = true` means pin the first successfully delivered message.
- `notify` defaults to `false`.
- `required` defaults to `false`; unsupported channels or failed pinning auto-degrade by continuing delivery.
- Manual `pin`, `unpin`, and `list-pins` message actions remain for existing messages.
Current Telegram ACP topic binding should move from `channelData.telegram.pin = true` to `delivery.pin = true`.
## Runtime Capability Contract
Add presentation and delivery render hooks to the runtime outbound adapter, not the control-plane channel plugin.
```ts
type ChannelPresentationCapabilities = {
supported: boolean;
buttons?: boolean;
selects?: boolean;
context?: boolean;
divider?: boolean;
tones?: MessagePresentationTone[];
};
type ChannelDeliveryCapabilities = {
pinSentMessage?: boolean;
};
type ChannelOutboundAdapter = {
presentationCapabilities?: ChannelPresentationCapabilities;
renderPresentation?: (params: {
payload: ReplyPayload;
presentation: MessagePresentation;
ctx: ChannelOutboundSendContext;
}) => ReplyPayload | null;
deliveryCapabilities?: ChannelDeliveryCapabilities;
pinDeliveredMessage?: (params: {
cfg: OpenClawConfig;
accountId?: string | null;
to: string;
threadId?: string | number | null;
messageId: string;
notify: boolean;
}) => Promise<void>;
};
```
Core behavior:
- Resolve target channel and runtime adapter.
- Ask for presentation capabilities.
- Degrade unsupported blocks before rendering.
- Call `renderPresentation`.
- If no renderer exists, convert presentation to text fallback.
- After successful send, call `pinDeliveredMessage` when `delivery.pin` is requested and supported.
## Channel Mapping
Discord:
- Render `presentation` to components v2 and Carbon containers in runtime-only modules.
- Keep accent color helpers in light modules.
- Remove `DiscordUiContainer` imports from channel plugin control-plane code.
Slack:
- Render `presentation` to Block Kit.
- Remove agent and CLI `blocks` input.
Telegram:
- Render text, context, and dividers as text.
- Render actions and select as inline keyboards when configured and allowed for the target surface.
- Use text fallback when inline buttons are disabled.
- Move ACP topic pinning to `delivery.pin`.
Mattermost:
- Render actions as interactive buttons where configured.
- Render other blocks as text fallback.
MS Teams:
- Render `presentation` to Adaptive Cards.
- Keep manual pin/unpin/list-pins actions.
- Optionally implement `pinDeliveredMessage` if Graph support is reliable for the target conversation.
Feishu:
- Render `presentation` to interactive cards.
- Keep manual pin/unpin/list-pins actions.
- Optionally implement `pinDeliveredMessage` for sent-message pinning if API behavior is reliable.
LINE:
- Render `presentation` to Flex or template messages where possible.
- Fall back to text for unsupported blocks.
- Remove LINE UI payloads from `channelData`.
Plain or limited channels:
- Convert presentation to text with conservative formatting.
## Refactor Steps
1. Reapply the Discord release fix that splits `ui-colors.ts` from Carbon-backed UI and removes `DiscordUiContainer` from `extensions/discord/src/channel.ts`.
2. Add `presentation` and `delivery` to `ReplyPayload`, outbound payload normalization, delivery summaries, and hook payloads.
3. Add `MessagePresentation` schema and parser helpers in a narrow SDK/runtime subpath.
4. Replace message capabilities `buttons`, `cards`, `components`, and `blocks` with semantic presentation capabilities.
5. Add runtime outbound adapter hooks for presentation render and delivery pinning.
6. Replace cross-context component construction with `buildCrossContextPresentation`.
7. Delete `src/infra/outbound/channel-adapters.ts` and remove `buildCrossContextComponents` from channel plugin types.
8. Change `maybeApplyCrossContextMarker` to attach `presentation` instead of native params.
9. Update plugin-dispatch send paths to consume only semantic presentation and delivery metadata.
10. Remove agent and CLI native payload params: `components`, `blocks`, `buttons`, and `card`.
11. Remove SDK helpers that create native message-tool schemas, replacing them with presentation schema helpers.
12. Remove UI/native envelopes from `channelData`; keep only transport metadata until each remaining field is reviewed.
13. Migrate Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, and LINE renderers.
14. Update docs for message CLI, channel pages, plugin SDK, and capability cookbook.
15. Run import fanout profiling for Discord and affected channel entrypoints.
Steps 1-11 and 13-14 are implemented in this refactor for the shared agent, CLI, plugin capability, and outbound adapter contracts. Step 12 remains a deeper internal cleanup pass for provider-private `channelData` transport envelopes. Step 15 remains follow-up validation if we want quantified import-fanout numbers beyond the type/test gate.
## Tests
Add or update:
- Presentation normalization tests.
- Presentation auto-degrade tests for unsupported blocks.
- Cross-context marker tests for plugin dispatch and core delivery paths.
- Channel render matrix tests for Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, LINE, and text fallback.
- Message tool schema tests proving native fields are gone.
- CLI tests proving native flags are gone.
- Discord entrypoint import-laziness regression covering Carbon.
- Delivery pin tests covering Telegram and generic fallback.
## Open Questions
- Should `delivery.pin` be implemented for Discord, Slack, MS Teams, and Feishu in the first pass, or only Telegram first?
- Should `delivery` eventually absorb existing fields such as `replyToId`, `replyToCurrent`, `silent`, and `audioAsVoice`, or stay focused on post-send behaviors?
- Should presentation support images or file references directly, or should media remain separate from UI layout for now?

View File

@ -1251,16 +1251,21 @@ Compatibility note:
## Message tool schemas
Plugins should own channel-specific `describeMessageTool(...)` schema
contributions. Keep provider-specific fields in the plugin, not in shared core.
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.
For shared portable schema fragments, reuse the generic helpers exported through
`openclaw/plugin-sdk/channel-actions`:
Send-capable plugins declare what they can render through message capabilities:
- `createMessageToolButtonsSchema()` for button-grid style payloads
- `createMessageToolCardSchema()` for structured card payloads
- `presentation` for semantic presentation blocks (`text`, `context`, `divider`, `buttons`, `select`)
- `delivery-pin` for pinned-delivery requests
If a schema shape only makes sense for one provider, define it in that plugin's
own source instead of promoting it into the shared SDK.
Core decides whether to render the presentation natively or degrade it to text.
Do not expose 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

View File

@ -0,0 +1,338 @@
---
title: "Message Presentation"
summary: "Semantic message cards, buttons, selects, fallback text, and delivery hints for channel plugins"
read_when:
- Adding or modifying message card, button, or select rendering
- Building a channel plugin that supports rich outbound messages
- Changing message tool presentation or delivery capabilities
- Debugging provider-specific card/block/component rendering regressions
---
# Message Presentation
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.
Use presentation for portable message UI:
- text sections
- small context/footer text
- dividers
- buttons
- select menus
- card title and tone
Do not add new provider-native fields such as Discord `components`, Slack
`blocks`, Telegram `buttons`, Teams `card`, or Feishu `card` to the shared
message tool. Those are renderer outputs owned by the channel plugin.
## Contract
Plugin authors import the public contract from:
```ts
import type {
MessagePresentation,
ReplyPayloadDelivery,
} from "openclaw/plugin-sdk/interactive-runtime";
```
Shape:
```ts
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 MessagePresentationButton = {
label: string;
value?: string;
url?: string;
style?: "primary" | "secondary" | "success" | "danger";
};
type MessagePresentationOption = {
label: string;
value: string;
};
type ReplyPayloadDelivery = {
pin?:
| boolean
| {
enabled: boolean;
notify?: boolean;
required?: boolean;
};
};
```
Button semantics:
- `value` is an application action value routed back through the channel's
existing interaction path when the channel supports clickable controls.
- `url` is a link button. It can exist without `value`.
- `label` is required and is also used in text fallback.
- `style` is advisory. Renderers should map unsupported styles to a safe
default, not fail the send.
Select semantics:
- `options[].value` is the selected application value.
- `placeholder` is advisory and may be ignored by channels without native
select support.
- If a channel does not support selects, fallback text lists the labels.
## Producer Examples
Simple card:
```json
{
"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", "value": "deploy:approve", "style": "success" },
{ "label": "Decline", "value": "deploy:decline", "style": "danger" }
]
}
]
}
```
URL-only link button:
```json
{
"blocks": [
{ "type": "text", "text": "Release notes are ready." },
{
"type": "buttons",
"buttons": [{ "label": "Open notes", "url": "https://example.com/release" }]
}
]
}
```
Select menu:
```json
{
"title": "Choose environment",
"blocks": [
{
"type": "select",
"placeholder": "Environment",
"options": [
{ "label": "Canary", "value": "env:canary" },
{ "label": "Production", "value": "env:prod" }
]
}
]
}
```
CLI send:
```bash
openclaw message send --channel slack \
--target channel:C123 \
--message "Deploy approval" \
--presentation '{"title":"Deploy approval","tone":"warning","blocks":[{"type":"text","text":"Canary is ready."},{"type":"buttons","buttons":[{"label":"Approve","value":"deploy:approve","style":"success"},{"label":"Decline","value":"deploy:decline","style":"danger"}]}]}'
```
Pinned delivery:
```bash
openclaw message send --channel telegram \
--target -1001234567890 \
--message "Topic opened" \
--pin
```
Pinned delivery with explicit JSON:
```json
{
"pin": {
"enabled": true,
"notify": true,
"required": false
}
}
```
## Renderer Contract
Channel plugins declare render support on their outbound adapter:
```ts
const adapter: ChannelOutboundAdapter = {
deliveryMode: "direct",
presentationCapabilities: {
supported: true,
buttons: true,
selects: true,
context: true,
divider: true,
},
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 });
},
};
```
Capability fields are intentionally simple booleans. They describe what the
renderer can make interactive, not every native platform limit. Renderers still
own platform-specific limits such as maximum button count, block count, and
card size.
## Core Render Flow
When a `ReplyPayload` or message action includes `presentation`, core:
1. Normalizes the presentation payload.
2. Resolves the target channel's outbound adapter.
3. Reads `presentationCapabilities`.
4. Calls `renderPresentation` when the adapter can render the payload.
5. Falls back to conservative text when the adapter is absent or cannot render.
6. Sends the resulting payload through the normal channel delivery path.
7. Applies delivery metadata such as `delivery.pin` after the first successful
sent message.
Core owns fallback behavior so producers can stay channel-agnostic. Channel
plugins own native rendering and interaction handling.
## Degradation Rules
Presentation must be safe to send on limited channels.
Fallback text includes:
- `title` as the first line
- `text` blocks as normal paragraphs
- `context` blocks as compact context lines
- `divider` blocks as a visual separator
- button labels, including URLs for link buttons
- select option labels
Unsupported native controls should degrade rather than fail the whole send.
Examples:
- Telegram with inline buttons disabled sends text fallback.
- A channel without select support lists select options 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.
The main exception is `delivery.pin.required: true`; if pinning is requested as
required and the channel cannot pin the sent message, delivery reports failure.
## Provider Mapping
Current bundled renderers:
| 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`. |
| Slack | Block Kit | Preserves legacy `channelData.slack.blocks` for existing provider-native payload producers, 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. |
| Mattermost | Text plus interactive props | Other blocks degrade to text. |
| Microsoft Teams | Adaptive Cards | Plain `message` text is included with the card when both are provided. |
| Feishu | Interactive cards | Card header can use `title`; body avoids duplicating that title. |
| Plain channels | Text fallback | Channels without a renderer still get readable output. |
Provider-native payload compatibility is a transition affordance for existing
reply producers. It is not a reason to add new shared native fields.
## Presentation vs InteractiveReply
`InteractiveReply` is the older internal subset used by approval and interaction
helpers. It supports:
- text
- buttons
- selects
`MessagePresentation` is the canonical shared send contract. It adds:
- title
- tone
- context
- divider
- URL-only buttons
- generic delivery metadata through `ReplyPayload.delivery`
Use helpers from `openclaw/plugin-sdk/interactive-runtime` when bridging older
code:
```ts
import {
interactiveReplyToPresentation,
normalizeMessagePresentation,
presentationToInteractiveReply,
renderMessagePresentationFallbackText,
} from "openclaw/plugin-sdk/interactive-runtime";
```
New code should accept or produce `MessagePresentation` directly.
## Delivery Pin
Pinning is delivery behavior, not presentation. Use `delivery.pin` instead of
provider-native fields such as `channelData.telegram.pin`.
Semantics:
- `pin: true` pins the first successfully delivered message.
- `pin.notify` defaults to `false`.
- `pin.required` defaults to `false`.
- 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.
Manual `pin`, `unpin`, and `pins` message actions still exist for existing
messages where the provider supports those operations.
## Plugin Author Checklist
- Declare `presentation` from `describeMessageTool(...)` when the channel can
render or safely degrade semantic presentation.
- Add `presentationCapabilities` to the runtime outbound adapter.
- Implement `renderPresentation` in runtime code, not control-plane plugin
setup code.
- Keep native UI libraries out of hot setup/catalog paths.
- Preserve platform limits in the renderer and tests.
- Add fallback tests for unsupported buttons, selects, URL buttons, title/text
duplication, and mixed `message` plus `presentation` sends.
- Add delivery pin support through `deliveryCapabilities.pin` and
`pinDeliveredMessage` only when the provider can pin the sent message id.
- Do not expose new provider-native card/block/component/button fields through
the shared message action schema.
## Related Docs
- [Message CLI](/cli/message)
- [Plugin SDK Overview](/plugins/sdk-overview)
- [Plugin Architecture](/plugins/architecture#message-tool-schemas)
- [Channel Presentation Refactor Plan](/plan/ui-channels)

View File

@ -109,13 +109,13 @@ explicitly promotes one as public.
| `plugin-sdk/allowlist-config-edit` | Allowlist config edit/read helpers |
| `plugin-sdk/group-access` | Shared group-access decision helpers |
| `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers |
| `plugin-sdk/interactive-runtime` | Interactive reply payload normalization/reduction helpers |
| `plugin-sdk/interactive-runtime` | Semantic message presentation, delivery, and legacy interactive reply helpers. See [Message Presentation](/plugins/message-presentation) |
| `plugin-sdk/channel-inbound` | Compatibility barrel for inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
| `plugin-sdk/channel-mention-gating` | Narrow mention-policy helpers without the broader inbound runtime surface |
| `plugin-sdk/channel-location` | Channel location context and formatting helpers |
| `plugin-sdk/channel-logging` | Channel logging helpers for inbound drops and typing/ack failures |
| `plugin-sdk/channel-send-result` | Reply result types |
| `plugin-sdk/channel-actions` | `createMessageToolButtonsSchema`, `createMessageToolCardSchema` |
| `plugin-sdk/channel-actions` | Channel message-action helpers, plus deprecated native schema helpers kept for plugin compatibility |
| `plugin-sdk/channel-targets` | Target parsing/matching helpers |
| `plugin-sdk/channel-contract` | Channel contract types |
| `plugin-sdk/channel-feedback` | Feedback/reaction wiring |

View File

@ -0,0 +1,731 @@
---
title: "Skill Workshop Plugin"
summary: "Experimental capture of reusable procedures as workspace skills with review, approval, quarantine, and hot skill refresh"
read_when:
- You want agents to turn corrections or reusable procedures into workspace skills
- You are configuring procedural skill memory
- You are debugging skill_workshop tool behavior
- You are deciding whether to enable automatic skill creation
---
# Skill Workshop Plugin
Skill Workshop is **experimental**. It is disabled by default, its capture
heuristics and reviewer prompts may change between releases, and automatic
writes should be used only in trusted workspaces after reviewing pending-mode
output first.
Skill Workshop is procedural memory for workspace skills. It lets an agent turn
reusable workflows, user corrections, hard-won fixes, and recurring pitfalls
into `SKILL.md` files under:
```text
<workspace>/skills/<skill-name>/SKILL.md
```
This is different from long-term memory:
- **Memory** stores facts, preferences, entities, and past context.
- **Skills** store reusable procedures the agent should follow on future tasks.
- **Skill Workshop** is the bridge from a useful turn to a durable workspace
skill, with safety checks and optional approval.
Skill Workshop is useful when the agent learns a procedure such as:
- how to validate externally sourced animated GIF assets
- how to replace screenshot assets and verify dimensions
- how to run a repo-specific QA scenario
- how to debug a recurring provider failure
- how to repair a stale local workflow note
It is not intended for:
- facts like “the user likes blue”
- broad autobiographical memory
- raw transcript archiving
- secrets, credentials, or hidden prompt text
- one-off instructions that will not repeat
## Default State
The bundled plugin is **experimental** and **disabled by default** unless it is
explicitly enabled in `plugins.entries.skill-workshop`.
The plugin manifest does not set `enabledByDefault: true`. The `enabled: true`
default inside the plugin config schema applies only after the plugin entry has
already been selected and loaded.
Experimental means:
- the plugin is supported enough for opt-in testing and dogfooding
- proposal storage, reviewer thresholds, and capture heuristics can evolve
- pending approval is the recommended starting mode
- auto apply is for trusted personal/workspace setups, not shared or hostile
input-heavy environments
## Enable
Minimal safe config:
```json5
{
plugins: {
entries: {
"skill-workshop": {
enabled: true,
config: {
autoCapture: true,
approvalPolicy: "pending",
reviewMode: "hybrid",
},
},
},
},
}
```
With this config:
- the `skill_workshop` tool is available
- explicit reusable corrections are queued as pending proposals
- threshold-based reviewer passes can propose skill updates
- no skill file is written until a pending proposal is applied
Use automatic writes only in trusted workspaces:
```json5
{
plugins: {
entries: {
"skill-workshop": {
enabled: true,
config: {
autoCapture: true,
approvalPolicy: "auto",
reviewMode: "hybrid",
},
},
},
},
}
```
`approvalPolicy: "auto"` still uses the same scanner and quarantine path. It
does not apply proposals with critical findings.
## Configuration
| Key | Default | Range / values | Meaning |
| -------------------- | ----------- | ------------------------------------------- | -------------------------------------------------------------------- |
| `enabled` | `true` | boolean | Enables the plugin after the plugin entry is loaded. |
| `autoCapture` | `true` | boolean | Enables post-turn capture/review on successful agent turns. |
| `approvalPolicy` | `"pending"` | `"pending"`, `"auto"` | Queue proposals or write safe proposals automatically. |
| `reviewMode` | `"hybrid"` | `"off"`, `"heuristic"`, `"llm"`, `"hybrid"` | Chooses explicit correction capture, LLM reviewer, both, or neither. |
| `reviewInterval` | `15` | `1..200` | Run reviewer after this many successful turns. |
| `reviewMinToolCalls` | `8` | `1..500` | Run reviewer after this many observed tool calls. |
| `reviewTimeoutMs` | `45000` | `5000..180000` | Timeout for the embedded reviewer run. |
| `maxPending` | `50` | `1..200` | Max pending/quarantined proposals kept per workspace. |
| `maxSkillBytes` | `40000` | `1024..200000` | Max generated skill/support file size. |
Recommended profiles:
```json5
// Conservative: explicit tool use only, no automatic capture.
{
autoCapture: false,
approvalPolicy: "pending",
reviewMode: "off",
}
```
```json5
// Review-first: capture automatically, but require approval.
{
autoCapture: true,
approvalPolicy: "pending",
reviewMode: "hybrid",
}
```
```json5
// Trusted automation: write safe proposals immediately.
{
autoCapture: true,
approvalPolicy: "auto",
reviewMode: "hybrid",
}
```
```json5
// Low-cost: no reviewer LLM call, only explicit correction phrases.
{
autoCapture: true,
approvalPolicy: "pending",
reviewMode: "heuristic",
}
```
## Capture Paths
Skill Workshop has three capture paths.
### Tool Suggestions
The model can call `skill_workshop` directly when it sees a reusable procedure
or when the user asks it to save/update a skill.
This is the most explicit path and works even with `autoCapture: false`.
### Heuristic Capture
When `autoCapture` is enabled and `reviewMode` is `heuristic` or `hybrid`, the
plugin scans successful turns for explicit user correction phrases:
- `next time`
- `from now on`
- `remember to`
- `make sure to`
- `always ... use/check/verify/record/save/prefer`
- `prefer ... when/for/instead/use`
- `when asked`
The heuristic creates a proposal from the latest matching user instruction. It
uses topic hints to choose skill names for common workflows:
- animated GIF tasks -> `animated-gif-workflow`
- screenshot or asset tasks -> `screenshot-asset-workflow`
- QA or scenario tasks -> `qa-scenario-workflow`
- GitHub PR tasks -> `github-pr-workflow`
- fallback -> `learned-workflows`
Heuristic capture is intentionally narrow. It is for clear corrections and
repeatable process notes, not for general transcript summarization.
### LLM Reviewer
When `autoCapture` is enabled and `reviewMode` is `llm` or `hybrid`, the plugin
runs a compact embedded reviewer after thresholds are reached.
The reviewer receives:
- the recent transcript text, capped to the last 12,000 characters
- up to 12 existing workspace skills
- up to 2,000 characters from each existing skill
- JSON-only instructions
The reviewer has no tools:
- `disableTools: true`
- `toolsAllow: []`
- `disableMessageTool: true`
It can return:
```json
{ "action": "none" }
```
or one skill proposal:
```json
{
"action": "create",
"skillName": "media-asset-qa",
"title": "Media Asset QA",
"reason": "Reusable animated media acceptance workflow",
"description": "Validate externally sourced animated media before product use.",
"body": "## Workflow\n\n- Verify true animation.\n- Record attribution.\n- Store a local approved copy.\n- Verify in product UI before final reply."
}
```
It can also append to an existing skill:
```json
{
"action": "append",
"skillName": "qa-scenario-workflow",
"title": "QA Scenario Workflow",
"reason": "Animated media QA needs reusable checks",
"description": "QA scenario workflow.",
"section": "Workflow",
"body": "- For animated GIF tasks, verify frame count and attribution before passing."
}
```
Or replace exact text in an existing skill:
```json
{
"action": "replace",
"skillName": "screenshot-asset-workflow",
"title": "Screenshot Asset Workflow",
"reason": "Old validation missed image optimization",
"oldText": "- Replace the screenshot asset.",
"newText": "- Replace the screenshot asset, preserve dimensions, optimize the PNG, and run the relevant validation gate."
}
```
Prefer `append` or `replace` when a relevant skill already exists. Use `create`
only when no existing skill fits.
## Proposal Lifecycle
Every generated update becomes a proposal with:
- `id`
- `createdAt`
- `updatedAt`
- `workspaceDir`
- optional `agentId`
- optional `sessionId`
- `skillName`
- `title`
- `reason`
- `source`: `tool`, `agent_end`, or `reviewer`
- `status`
- `change`
- optional `scanFindings`
- optional `quarantineReason`
Proposal statuses:
- `pending` - waiting for approval
- `applied` - written to `<workspace>/skills`
- `rejected` - rejected by operator/model
- `quarantined` - blocked by critical scanner findings
State is stored per workspace under the Gateway state directory:
```text
<stateDir>/skill-workshop/<workspace-hash>.json
```
Pending and quarantined proposals are deduplicated by skill name and change
payload. The store keeps the newest pending/quarantined proposals up to
`maxPending`.
## Tool Reference
The plugin registers one agent tool:
```text
skill_workshop
```
### `status`
Count proposals by state for the active workspace.
```json
{ "action": "status" }
```
Result shape:
```json
{
"workspaceDir": "/path/to/workspace",
"pending": 1,
"quarantined": 0,
"applied": 3,
"rejected": 0
}
```
### `list_pending`
List pending proposals.
```json
{ "action": "list_pending" }
```
To list another status:
```json
{ "action": "list_pending", "status": "applied" }
```
Valid `status` values:
- `pending`
- `applied`
- `rejected`
- `quarantined`
### `list_quarantine`
List quarantined proposals.
```json
{ "action": "list_quarantine" }
```
Use this when automatic capture appears to do nothing and the logs mention
`skill-workshop: quarantined <skill>`.
### `inspect`
Fetch a proposal by id.
```json
{
"action": "inspect",
"id": "proposal-id"
}
```
### `suggest`
Create a proposal. With `approvalPolicy: "pending"`, this queues by default.
```json
{
"action": "suggest",
"skillName": "animated-gif-workflow",
"title": "Animated GIF Workflow",
"reason": "User established reusable GIF validation rules.",
"description": "Validate animated GIF assets before using them.",
"body": "## Workflow\n\n- Verify the URL resolves to image/gif.\n- Confirm it has multiple frames.\n- Record attribution and license.\n- Avoid hotlinking when a local asset is needed."
}
```
Force a safe write:
```json
{
"action": "suggest",
"apply": true,
"skillName": "animated-gif-workflow",
"description": "Validate animated GIF assets before using them.",
"body": "## Workflow\n\n- Verify true animation.\n- Record attribution."
}
```
Force pending even in `approvalPolicy: "auto"`:
```json
{
"action": "suggest",
"apply": false,
"skillName": "screenshot-asset-workflow",
"description": "Screenshot replacement workflow.",
"body": "## Workflow\n\n- Verify dimensions.\n- Optimize the PNG.\n- Run the relevant gate."
}
```
Append to a section:
```json
{
"action": "suggest",
"skillName": "qa-scenario-workflow",
"section": "Workflow",
"description": "QA scenario workflow.",
"body": "- For media QA, verify generated assets render and pass final assertions."
}
```
Replace exact text:
```json
{
"action": "suggest",
"skillName": "github-pr-workflow",
"oldText": "- Check the PR.",
"newText": "- Check unresolved review threads, CI status, linked issues, and changed files before deciding."
}
```
### `apply`
Apply a pending proposal.
```json
{
"action": "apply",
"id": "proposal-id"
}
```
`apply` refuses quarantined proposals:
```text
quarantined proposal cannot be applied
```
### `reject`
Mark a proposal rejected.
```json
{
"action": "reject",
"id": "proposal-id"
}
```
### `write_support_file`
Write a supporting file inside an existing or proposed skill directory.
Allowed top-level support directories:
- `references/`
- `templates/`
- `scripts/`
- `assets/`
Example:
```json
{
"action": "write_support_file",
"skillName": "release-workflow",
"relativePath": "references/checklist.md",
"body": "# Release Checklist\n\n- Run release docs.\n- Verify changelog.\n"
}
```
Support files are workspace-scoped, path-checked, byte-limited by
`maxSkillBytes`, scanned, and written atomically.
## Skill Writes
Skill Workshop writes only under:
```text
<workspace>/skills/<normalized-skill-name>/
```
Skill names are normalized:
- lowercased
- non `[a-z0-9_-]` runs become `-`
- leading/trailing non-alphanumerics are removed
- max length is 80 characters
- final name must match `[a-z0-9][a-z0-9_-]{1,79}`
For `create`:
- if the skill does not exist, Skill Workshop writes a new `SKILL.md`
- if it already exists, Skill Workshop appends the body to `## Workflow`
For `append`:
- if the skill exists, Skill Workshop appends to the requested section
- if it does not exist, Skill Workshop creates a minimal skill then appends
For `replace`:
- the skill must already exist
- `oldText` must be present exactly
- only the first exact match is replaced
All writes are atomic and refresh the in-memory skills snapshot immediately, so
the new or updated skill can become visible without a Gateway restart.
## Safety Model
Skill Workshop has a safety scanner on generated `SKILL.md` content and support
files.
Critical findings quarantine proposals:
| Rule id | Blocks content that... |
| -------------------------------------- | --------------------------------------------------------------------- |
| `prompt-injection-ignore-instructions` | tells the agent to ignore prior/higher instructions |
| `prompt-injection-system` | references system prompts, developer messages, or hidden instructions |
| `prompt-injection-tool` | encourages bypassing tool permission/approval |
| `shell-pipe-to-shell` | includes `curl`/`wget` piped into `sh`, `bash`, or `zsh` |
| `secret-exfiltration` | appears to send env/process env data over the network |
Warn findings are retained but do not block by themselves:
| Rule id | Warns on... |
| -------------------- | -------------------------------- |
| `destructive-delete` | broad `rm -rf` style commands |
| `unsafe-permissions` | `chmod 777` style permission use |
Quarantined proposals:
- keep `scanFindings`
- keep `quarantineReason`
- appear in `list_quarantine`
- cannot be applied through `apply`
To recover from a quarantined proposal, create a new safe proposal with the
unsafe content removed. Do not edit the store JSON by hand.
## Prompt Guidance
When enabled, Skill Workshop injects a short prompt section that tells the agent
to use `skill_workshop` for durable procedural memory.
The guidance emphasizes:
- procedures, not facts/preferences
- user corrections
- non-obvious successful procedures
- recurring pitfalls
- stale/thin/wrong skill repair through append/replace
- saving reusable procedure after long tool loops or hard fixes
- short imperative skill text
- no transcript dumps
The write mode text changes with `approvalPolicy`:
- pending mode: queue suggestions; apply only after explicit approval
- auto mode: apply safe workspace-skill updates when clearly reusable
## Costs and Runtime Behavior
Heuristic capture does not call a model.
LLM review uses an embedded run on the active/default agent model. It is
threshold-based so it does not run on every turn by default.
The reviewer:
- uses the same configured provider/model context when available
- falls back to runtime agent defaults
- has `reviewTimeoutMs`
- uses lightweight bootstrap context
- has no tools
- writes nothing directly
- can only emit a proposal that goes through the normal scanner and
approval/quarantine path
If the reviewer fails, times out, or returns invalid JSON, the plugin logs a
warning/debug message and skips that review pass.
## Operating Patterns
Use Skill Workshop when the user says:
- “next time, do X”
- “from now on, prefer Y”
- “make sure to verify Z”
- “save this as a workflow”
- “this took a while; remember the process”
- “update the local skill for this”
Good skill text:
```markdown
## Workflow
- Verify the GIF URL resolves to `image/gif`.
- Confirm the file has multiple frames.
- Record source URL, license, and attribution.
- Store a local copy when the asset will ship with the product.
- Verify the local asset renders in the target UI before final reply.
```
Poor skill text:
```markdown
The user asked about a GIF and I searched two websites. Then one was blocked by
Cloudflare. The final answer said to check attribution.
```
Reasons the poor version should not be saved:
- transcript-shaped
- not imperative
- includes noisy one-off details
- does not tell the next agent what to do
## Debugging
Check whether the plugin is loaded:
```bash
openclaw plugins list --enabled
```
Check proposal counts from an agent/tool context:
```json
{ "action": "status" }
```
Inspect pending proposals:
```json
{ "action": "list_pending" }
```
Inspect quarantined proposals:
```json
{ "action": "list_quarantine" }
```
Common symptoms:
| Symptom | Likely cause | Check |
| ------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Tool is unavailable | Plugin entry is not enabled | `plugins.entries.skill-workshop.enabled` and `openclaw plugins list` |
| No automatic proposal appears | `autoCapture: false`, `reviewMode: "off"`, or thresholds not met | Config, proposal status, Gateway logs |
| Heuristic did not capture | User wording did not match correction patterns | Use explicit `skill_workshop.suggest` or enable LLM reviewer |
| Reviewer did not create a proposal | Reviewer returned `none`, invalid JSON, or timed out | Gateway logs, `reviewTimeoutMs`, thresholds |
| Proposal is not applied | `approvalPolicy: "pending"` | `list_pending`, then `apply` |
| Proposal disappeared from pending | Duplicate proposal reused, max pending pruning, or was applied/rejected/quarantined | `status`, `list_pending` with status filters, `list_quarantine` |
| Skill file exists but model misses it | Skill snapshot not refreshed or skill gating excludes it | `openclaw skills` status and workspace skill eligibility |
Relevant logs:
- `skill-workshop: queued <skill>`
- `skill-workshop: applied <skill>`
- `skill-workshop: quarantined <skill>`
- `skill-workshop: heuristic capture skipped: ...`
- `skill-workshop: reviewer skipped: ...`
- `skill-workshop: reviewer found no update`
## QA Scenarios
Repo-backed QA scenarios:
- `qa/scenarios/plugins/skill-workshop-animated-gif-autocreate.md`
- `qa/scenarios/plugins/skill-workshop-pending-approval.md`
- `qa/scenarios/plugins/skill-workshop-reviewer-autonomous.md`
Run the deterministic coverage:
```bash
pnpm openclaw qa suite \
--scenario skill-workshop-animated-gif-autocreate \
--scenario skill-workshop-pending-approval \
--concurrency 1
```
Run reviewer coverage:
```bash
pnpm openclaw qa suite \
--scenario skill-workshop-reviewer-autonomous \
--concurrency 1
```
The reviewer scenario is intentionally separate because it enables
`reviewMode: "llm"` and exercises the embedded reviewer pass.
## When Not To Enable Auto Apply
Avoid `approvalPolicy: "auto"` when:
- the workspace contains sensitive procedures
- the agent is working on untrusted input
- skills are shared across a broad team
- you are still tuning prompts or scanner rules
- the model frequently handles hostile web/email content
Use pending mode first. Switch to auto mode only after reviewing the kind of
skills the agent proposes in that workspace.
## Related Docs
- [Skills](/tools/skills)
- [Plugins](/tools/plugin)
- [Testing](/reference/test)

View File

@ -90,6 +90,24 @@ You can gate them via `metadata.openclaw.requires.config` on the plugins conf
entry. See [Plugins](/tools/plugin) for discovery/config and [Tools](/tools) for the
tool surface those skills teach.
## Skill Workshop
The optional, experimental Skill Workshop plugin can create or update workspace
skills from reusable procedures observed during agent work. It is disabled by
default and must be explicitly enabled through
`plugins.entries.skill-workshop`.
Skill Workshop writes only to `<workspace>/skills`, scans generated content,
supports pending approval or automatic safe writes, quarantines unsafe
proposals, and refreshes the skill snapshot after successful writes so new
skills can become available without a Gateway restart.
Use it when you want corrections such as “next time, verify GIF attribution” or
hard-won workflows such as media QA checklists to become durable procedural
instructions. Start with pending approval; use automatic writes only in trusted
workspaces after reviewing its proposals. Full guide:
[Skill Workshop Plugin](/plugins/skill-workshop).
## ClawHub (install + sync)
ClawHub is the public skills registry for OpenClaw. Browse at