{{ page.summary }}
+ {% endif %} + + {% if page.read_when %} + + {% endif %} + +Click the toolbar button on a tab to attach / detach.
+
+ If you see a red ! badge on the extension icon, the relay server is not reachable.
+ Start OpenClaw’s browser relay on this machine (Gateway or node host), then click the toolbar button again.
+
+ Full guide (install, remote Gateway, security): docs.openclaw.ai/tools/chrome-extension +
+18792. Extension connects to: http://127.0.0.1:<port>/.
+ Only change this if your OpenClaw profile uses a different cdpUrl port.
+ {{ page.summary }}
+ {% endif %} + + {% if page.read_when %} + + {% endif %} + +`
+- Pairing is the default token exchange. Details: [Pairing](/start/pairing)
+
+Groups:
+
+- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`).
+- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
+
+### Mention gating (groups)
+
+BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior:
+
+- Uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) to detect mentions.
+- When `requireMention` is enabled for a group, the agent only responds when mentioned.
+- Control commands from authorized senders bypass mention gating.
+
+Per-group configuration:
+
+```json5
+{
+ channels: {
+ bluebubbles: {
+ groupPolicy: "allowlist",
+ groupAllowFrom: ["+15555550123"],
+ groups: {
+ "*": { requireMention: true }, // default for all groups
+ "iMessage;-;chat123": { requireMention: false }, // override for specific group
+ },
+ },
+ },
+}
+```
+
+### Command gating
+
+- Control commands (e.g., `/config`, `/model`) require authorization.
+- Uses `allowFrom` and `groupAllowFrom` to determine command authorization.
+- Authorized senders can run control commands even without mentioning in groups.
+
+## Typing + read receipts
+
+- **Typing indicators**: Sent automatically before and during response generation.
+- **Read receipts**: Controlled by `channels.bluebubbles.sendReadReceipts` (default: `true`).
+- **Typing indicators**: OpenClaw sends typing start events; BlueBubbles clears typing automatically on send or timeout (manual stop via DELETE is unreliable).
+
+```json5
+{
+ channels: {
+ bluebubbles: {
+ sendReadReceipts: false, // disable read receipts
+ },
+ },
+}
+```
+
+## Advanced actions
+
+BlueBubbles supports advanced message actions when enabled in config:
+
+```json5
+{
+ channels: {
+ bluebubbles: {
+ actions: {
+ reactions: true, // tapbacks (default: true)
+ edit: true, // edit sent messages (macOS 13+, broken on macOS 26 Tahoe)
+ unsend: true, // unsend messages (macOS 13+)
+ reply: true, // reply threading by message GUID
+ sendWithEffect: true, // message effects (slam, loud, etc.)
+ renameGroup: true, // rename group chats
+ setGroupIcon: true, // set group chat icon/photo (flaky on macOS 26 Tahoe)
+ addParticipant: true, // add participants to groups
+ removeParticipant: true, // remove participants from groups
+ leaveGroup: true, // leave group chats
+ sendAttachment: true, // send attachments/media
+ },
+ },
+ },
+}
+```
+
+Available actions:
+
+- **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`)
+- **edit**: Edit a sent message (`messageId`, `text`)
+- **unsend**: Unsend a message (`messageId`)
+- **reply**: Reply to a specific message (`messageId`, `text`, `to`)
+- **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`)
+- **renameGroup**: Rename a group chat (`chatGuid`, `displayName`)
+- **setGroupIcon**: Set a group chat's icon/photo (`chatGuid`, `media`) — flaky on macOS 26 Tahoe (API may return success but the icon does not sync).
+- **addParticipant**: Add someone to a group (`chatGuid`, `address`)
+- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`)
+- **leaveGroup**: Leave a group chat (`chatGuid`)
+- **sendAttachment**: Send media/files (`to`, `buffer`, `filename`, `asVoice`)
+ - Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
+
+### Message IDs (short vs full)
+
+OpenClaw may surface _short_ message IDs (e.g., `1`, `2`) to save tokens.
+
+- `MessageSid` / `ReplyToId` can be short IDs.
+- `MessageSidFull` / `ReplyToIdFull` contain the provider full IDs.
+- Short IDs are in-memory; they can expire on restart or cache eviction.
+- Actions accept short or full `messageId`, but short IDs will error if no longer available.
+
+Use full IDs for durable automations and storage:
+
+- Templates: `{{MessageSidFull}}`, `{{ReplyToIdFull}}`
+- Context: `MessageSidFull` / `ReplyToIdFull` in inbound payloads
+
+See [Configuration](/gateway/configuration) for template variables.
+
+## Block streaming
+
+Control whether responses are sent as a single message or streamed in blocks:
+
+```json5
+{
+ channels: {
+ bluebubbles: {
+ blockStreaming: true, // enable block streaming (default behavior)
+ },
+ },
+}
+```
+
+## Media + limits
+
+- Inbound attachments are downloaded and stored in the media cache.
+- Media cap via `channels.bluebubbles.mediaMaxMb` (default: 8 MB).
+- Outbound text is chunked to `channels.bluebubbles.textChunkLimit` (default: 4000 chars).
+
+## Configuration reference
+
+Full configuration: [Configuration](/gateway/configuration)
+
+Provider options:
+
+- `channels.bluebubbles.enabled`: Enable/disable the channel.
+- `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL.
+- `channels.bluebubbles.password`: API password.
+- `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`).
+- `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`).
+- `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`).
+- `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`).
+- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist.
+- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.).
+- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
+- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `true`).
+- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
+- `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
+- `channels.bluebubbles.mediaMaxMb`: Inbound media cap in MB (default: 8).
+- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables).
+- `channels.bluebubbles.dmHistoryLimit`: DM history limit.
+- `channels.bluebubbles.actions`: Enable/disable specific actions.
+- `channels.bluebubbles.accounts`: Multi-account configuration.
+
+Related global options:
+
+- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).
+- `messages.responsePrefix`.
+
+## Addressing / delivery targets
+
+Prefer `chat_guid` for stable routing:
+
+- `chat_guid:iMessage;-;+15555550123` (preferred for groups)
+- `chat_id:123`
+- `chat_identifier:...`
+- Direct handles: `+15555550123`, `user@example.com`
+ - If a direct handle does not have an existing DM chat, OpenClaw will create one via `POST /api/v1/chat/new`. This requires the BlueBubbles Private API to be enabled.
+
+## Security
+
+- Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted.
+- Keep the API password and webhook endpoint secret (treat them like credentials).
+- Localhost trust means a same-host reverse proxy can unintentionally bypass the password. If you proxy the gateway, require auth at the proxy and configure `gateway.trustedProxies`. See [Gateway security](/gateway/security#reverse-proxy-configuration).
+- Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN.
+
+## Troubleshooting
+
+- If typing/read events stop working, check the BlueBubbles webhook logs and verify the gateway path matches `channels.bluebubbles.webhookPath`.
+- Pairing codes expire after one hour; use `openclaw pairing list bluebubbles` and `openclaw pairing approve bluebubbles `.
+- Reactions require the BlueBubbles private API (`POST /api/v1/message/react`); ensure the server version exposes it.
+- Edit/unsend require macOS 13+ and a compatible BlueBubbles server version. On macOS 26 (Tahoe), edit is currently broken due to private API changes.
+- Group icon updates can be flaky on macOS 26 (Tahoe): the API may return success but the new icon does not sync.
+- OpenClaw auto-hides known-broken actions based on the BlueBubbles server's macOS version. If edit still appears on macOS 26 (Tahoe), disable it manually with `channels.bluebubbles.actions.edit=false`.
+- For status/health info: `openclaw status --all` or `openclaw status --deep`.
+
+For general channel workflow reference, see [Channels](/channels) and the [Plugins](/plugins) guide.
diff --git a/docs/channels/discord.md b/docs/channels/discord.md
new file mode 100644
index 0000000000000000000000000000000000000000..d2198d2d557a93b24245707c625ad9e567121c50
--- /dev/null
+++ b/docs/channels/discord.md
@@ -0,0 +1,471 @@
+---
+summary: "Discord bot support status, capabilities, and configuration"
+read_when:
+ - Working on Discord channel features
+title: "Discord"
+---
+
+# Discord (Bot API)
+
+Status: ready for DM and guild text channels via the official Discord bot gateway.
+
+## Quick setup (beginner)
+
+1. Create a Discord bot and copy the bot token.
+2. In the Discord app settings, enable **Message Content Intent** (and **Server Members Intent** if you plan to use allowlists or name lookups).
+3. Set the token for OpenClaw:
+ - Env: `DISCORD_BOT_TOKEN=...`
+ - Or config: `channels.discord.token: "..."`.
+ - If both are set, config takes precedence (env fallback is default-account only).
+4. Invite the bot to your server with message permissions (create a private server if you just want DMs).
+5. Start the gateway.
+6. DM access is pairing by default; approve the pairing code on first contact.
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ discord: {
+ enabled: true,
+ token: "YOUR_BOT_TOKEN",
+ },
+ },
+}
+```
+
+## Goals
+
+- Talk to OpenClaw via Discord DMs or guild channels.
+- Direct chats collapse into the agent's main session (default `agent:main:main`); guild channels stay isolated as `agent::discord:channel:` (display names use `discord:#`).
+- Group DMs are ignored by default; enable via `channels.discord.dm.groupEnabled` and optionally restrict by `channels.discord.dm.groupChannels`.
+- Keep routing deterministic: replies always go back to the channel they arrived on.
+
+## How it works
+
+1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token.
+2. Invite the bot to your server with the permissions required to read/send messages where you want to use it.
+3. Configure OpenClaw with `channels.discord.token` (or `DISCORD_BOT_TOKEN` as a fallback).
+4. Run the gateway; it auto-starts the Discord channel when a token is available (config first, env fallback) and `channels.discord.enabled` is not `false`.
+ - If you prefer env vars, set `DISCORD_BOT_TOKEN` (a config block is optional).
+5. Direct chats: use `user:` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected.
+6. Guild channels: use `channel:` for delivery. Mentions are required by default and can be set per guild or per channel.
+7. Direct chats: secure by default via `channels.discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `openclaw pairing approve discord `.
+ - To keep old “open to anyone” behavior: set `channels.discord.dm.policy="open"` and `channels.discord.dm.allowFrom=["*"]`.
+ - To hard-allowlist: set `channels.discord.dm.policy="allowlist"` and list senders in `channels.discord.dm.allowFrom`.
+ - To ignore all DMs: set `channels.discord.dm.enabled=false` or `channels.discord.dm.policy="disabled"`.
+8. Group DMs are ignored by default; enable via `channels.discord.dm.groupEnabled` and optionally restrict by `channels.discord.dm.groupChannels`.
+9. Optional guild rules: set `channels.discord.guilds` keyed by guild id (preferred) or slug, with per-channel rules.
+10. Optional native commands: `commands.native` defaults to `"auto"` (on for Discord/Telegram, off for Slack). Override with `channels.discord.commands.native: true|false|"auto"`; `false` clears previously registered commands. Text commands are controlled by `commands.text` and must be sent as standalone `/...` messages. Use `commands.useAccessGroups: false` to bypass access-group checks for commands.
+ - Full command list + config: [Slash commands](/tools/slash-commands)
+11. Optional guild context history: set `channels.discord.historyLimit` (default 20, falls back to `messages.groupChat.historyLimit`) to include the last N guild messages as context when replying to a mention. Set `0` to disable.
+12. Reactions: the agent can trigger reactions via the `discord` tool (gated by `channels.discord.actions.*`).
+ - Reaction removal semantics: see [/tools/reactions](/tools/reactions).
+ - The `discord` tool is only exposed when the current channel is Discord.
+13. Native commands use isolated session keys (`agent::discord:slash:`) rather than the shared `main` session.
+
+Note: Name → id resolution uses guild member search and requires Server Members Intent; if the bot can’t search members, use ids or `<@id>` mentions.
+Note: Slugs are lowercase with spaces replaced by `-`. Channel names are slugged without the leading `#`.
+Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-ready replies easy.
+
+## Config writes
+
+By default, Discord is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
+
+Disable with:
+
+```json5
+{
+ channels: { discord: { configWrites: false } },
+}
+```
+
+## How to create your own bot
+
+This is the “Discord Developer Portal” setup for running OpenClaw in a server (guild) channel like `#help`.
+
+### 1) Create the Discord app + bot user
+
+1. Discord Developer Portal → **Applications** → **New Application**
+2. In your app:
+ - **Bot** → **Add Bot**
+ - Copy the **Bot Token** (this is what you put in `DISCORD_BOT_TOKEN`)
+
+### 2) Enable the gateway intents OpenClaw needs
+
+Discord blocks “privileged intents” unless you explicitly enable them.
+
+In **Bot** → **Privileged Gateway Intents**, enable:
+
+- **Message Content Intent** (required to read message text in most guilds; without it you’ll see “Used disallowed intents” or the bot will connect but not react to messages)
+- **Server Members Intent** (recommended; required for some member/user lookups and allowlist matching in guilds)
+
+You usually do **not** need **Presence Intent**.
+
+### 3) Generate an invite URL (OAuth2 URL Generator)
+
+In your app: **OAuth2** → **URL Generator**
+
+**Scopes**
+
+- ✅ `bot`
+- ✅ `applications.commands` (required for native commands)
+
+**Bot Permissions** (minimal baseline)
+
+- ✅ View Channels
+- ✅ Send Messages
+- ✅ Read Message History
+- ✅ Embed Links
+- ✅ Attach Files
+- ✅ Add Reactions (optional but recommended)
+- ✅ Use External Emojis / Stickers (optional; only if you want them)
+
+Avoid **Administrator** unless you’re debugging and fully trust the bot.
+
+Copy the generated URL, open it, pick your server, and install the bot.
+
+### 4) Get the ids (guild/user/channel)
+
+Discord uses numeric ids everywhere; OpenClaw config prefers ids.
+
+1. Discord (desktop/web) → **User Settings** → **Advanced** → enable **Developer Mode**
+2. Right-click:
+ - Server name → **Copy Server ID** (guild id)
+ - Channel (e.g. `#help`) → **Copy Channel ID**
+ - Your user → **Copy User ID**
+
+### 5) Configure OpenClaw
+
+#### Token
+
+Set the bot token via env var (recommended on servers):
+
+- `DISCORD_BOT_TOKEN=...`
+
+Or via config:
+
+```json5
+{
+ channels: {
+ discord: {
+ enabled: true,
+ token: "YOUR_BOT_TOKEN",
+ },
+ },
+}
+```
+
+Multi-account support: use `channels.discord.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
+
+#### Allowlist + channel routing
+
+Example “single server, only allow me, only allow #help”:
+
+```json5
+{
+ channels: {
+ discord: {
+ enabled: true,
+ dm: { enabled: false },
+ guilds: {
+ YOUR_GUILD_ID: {
+ users: ["YOUR_USER_ID"],
+ requireMention: true,
+ channels: {
+ help: { allow: true, requireMention: true },
+ },
+ },
+ },
+ retry: {
+ attempts: 3,
+ minDelayMs: 500,
+ maxDelayMs: 30000,
+ jitter: 0.1,
+ },
+ },
+ },
+}
+```
+
+Notes:
+
+- `requireMention: true` means the bot only replies when mentioned (recommended for shared channels).
+- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
+- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
+- If `channels` is present, any channel not listed is denied by default.
+- Use a `"*"` channel entry to apply defaults across all channels; explicit channel entries override the wildcard.
+- Threads inherit parent channel config (allowlist, `requireMention`, skills, prompts, etc.) unless you add the thread channel id explicitly.
+- Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered).
+- Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels..users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
+
+### 6) Verify it works
+
+1. Start the gateway.
+2. In your server channel, send: `@Krill hello` (or whatever your bot name is).
+3. If nothing happens: check **Troubleshooting** below.
+
+### Troubleshooting
+
+- First: run `openclaw doctor` and `openclaw channels status --probe` (actionable warnings + quick audits).
+- **“Used disallowed intents”**: enable **Message Content Intent** (and likely **Server Members Intent**) in the Developer Portal, then restart the gateway.
+- **Bot connects but never replies in a guild channel**:
+ - Missing **Message Content Intent**, or
+ - The bot lacks channel permissions (View/Send/Read History), or
+ - Your config requires mentions and you didn’t mention it, or
+ - Your guild/channel allowlist denies the channel/user.
+- **`requireMention: false` but still no replies**:
+- `channels.discord.groupPolicy` defaults to **allowlist**; set it to `"open"` or add a guild entry under `channels.discord.guilds` (optionally list channels under `channels.discord.guilds..channels` to restrict).
+ - If you only set `DISCORD_BOT_TOKEN` and never create a `channels.discord` section, the runtime
+ defaults `groupPolicy` to `open`. Add `channels.discord.groupPolicy`,
+ `channels.defaults.groupPolicy`, or a guild/channel allowlist to lock it down.
+- `requireMention` must live under `channels.discord.guilds` (or a specific channel). `channels.discord.requireMention` at the top level is ignored.
+- **Permission audits** (`channels status --probe`) only check numeric channel IDs. If you use slugs/names as `channels.discord.guilds.*.channels` keys, the audit can’t verify permissions.
+- **DMs don’t work**: `channels.discord.dm.enabled=false`, `channels.discord.dm.policy="disabled"`, or you haven’t been approved yet (`channels.discord.dm.policy="pairing"`).
+- **Exec approvals in Discord**: Discord supports a **button UI** for exec approvals in DMs (Allow once / Always allow / Deny). `/approve ...` is only for forwarded approvals and won’t resolve Discord’s button prompts. If you see `❌ Failed to submit approval: Error: unknown approval id` or the UI never shows up, check:
+ - `channels.discord.execApprovals.enabled: true` in your config.
+ - Your Discord user ID is listed in `channels.discord.execApprovals.approvers` (the UI is only sent to approvers).
+ - Use the buttons in the DM prompt (**Allow once**, **Always allow**, **Deny**).
+ - See [Exec approvals](/tools/exec-approvals) and [Slash commands](/tools/slash-commands) for the broader approvals and command flow.
+
+## Capabilities & limits
+
+- DMs and guild text channels (threads are treated as separate channels; voice not supported).
+- Typing indicators sent best-effort; message chunking uses `channels.discord.textChunkLimit` (default 2000) and splits tall replies by line count (`channels.discord.maxLinesPerMessage`, default 17).
+- Optional newline chunking: set `channels.discord.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
+- File uploads supported up to the configured `channels.discord.mediaMaxMb` (default 8 MB).
+- Mention-gated guild replies by default to avoid noisy bots.
+- Reply context is injected when a message references another message (quoted content + ids).
+- Native reply threading is **off by default**; enable with `channels.discord.replyToMode` and reply tags.
+
+## Retry policy
+
+Outbound Discord API calls retry on rate limits (429) using Discord `retry_after` when available, with exponential backoff and jitter. Configure via `channels.discord.retry`. See [Retry policy](/concepts/retry).
+
+## Config
+
+```json5
+{
+ channels: {
+ discord: {
+ enabled: true,
+ token: "abc.123",
+ groupPolicy: "allowlist",
+ guilds: {
+ "*": {
+ channels: {
+ general: { allow: true },
+ },
+ },
+ },
+ mediaMaxMb: 8,
+ actions: {
+ reactions: true,
+ stickers: true,
+ emojiUploads: true,
+ stickerUploads: true,
+ polls: true,
+ permissions: true,
+ messages: true,
+ threads: true,
+ pins: true,
+ search: true,
+ memberInfo: true,
+ roleInfo: true,
+ roles: false,
+ channelInfo: true,
+ channels: true,
+ voiceStatus: true,
+ events: true,
+ moderation: false,
+ },
+ replyToMode: "off",
+ dm: {
+ enabled: true,
+ policy: "pairing", // pairing | allowlist | open | disabled
+ allowFrom: ["123456789012345678", "steipete"],
+ groupEnabled: false,
+ groupChannels: ["openclaw-dm"],
+ },
+ guilds: {
+ "*": { requireMention: true },
+ "123456789012345678": {
+ slug: "friends-of-openclaw",
+ requireMention: false,
+ reactionNotifications: "own",
+ users: ["987654321098765432", "steipete"],
+ channels: {
+ general: { allow: true },
+ help: {
+ allow: true,
+ requireMention: true,
+ users: ["987654321098765432"],
+ skills: ["search", "docs"],
+ systemPrompt: "Keep answers short.",
+ },
+ },
+ },
+ },
+ },
+ },
+}
+```
+
+Ack reactions are controlled globally via `messages.ackReaction` +
+`messages.ackReactionScope`. Use `messages.removeAckAfterReply` to clear the
+ack reaction after the bot replies.
+
+- `dm.enabled`: set `false` to ignore all DMs (default `true`).
+- `dm.policy`: DM access control (`pairing` recommended). `"open"` requires `dm.allowFrom=["*"]`.
+- `dm.allowFrom`: DM allowlist (user ids or names). Used by `dm.policy="allowlist"` and for `dm.policy="open"` validation. The wizard accepts usernames and resolves them to ids when the bot can search members.
+- `dm.groupEnabled`: enable group DMs (default `false`).
+- `dm.groupChannels`: optional allowlist for group DM channel ids or slugs.
+- `groupPolicy`: controls guild channel handling (`open|disabled|allowlist`); `allowlist` requires channel allowlists.
+- `guilds`: per-guild rules keyed by guild id (preferred) or slug.
+- `guilds."*"`: default per-guild settings applied when no explicit entry exists.
+- `guilds..slug`: optional friendly slug used for display names.
+- `guilds..users`: optional per-guild user allowlist (ids or names).
+- `guilds..tools`: optional per-guild tool policy overrides (`allow`/`deny`/`alsoAllow`) used when the channel override is missing.
+- `guilds..toolsBySender`: optional per-sender tool policy overrides at the guild level (applies when the channel override is missing; `"*"` wildcard supported).
+- `guilds..channels..allow`: allow/deny the channel when `groupPolicy="allowlist"`.
+- `guilds..channels..requireMention`: mention gating for the channel.
+- `guilds..channels..tools`: optional per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
+- `guilds..channels..toolsBySender`: optional per-sender tool policy overrides within the channel (`"*"` wildcard supported).
+- `guilds..channels..users`: optional per-channel user allowlist.
+- `guilds..channels..skills`: skill filter (omit = all skills, empty = none).
+- `guilds..channels..systemPrompt`: extra system prompt for the channel (combined with channel topic).
+- `guilds..channels..enabled`: set `false` to disable the channel.
+- `guilds..channels`: channel rules (keys are channel slugs or ids).
+- `guilds..requireMention`: per-guild mention requirement (overridable per channel).
+- `guilds..reactionNotifications`: reaction system event mode (`off`, `own`, `all`, `allowlist`).
+- `textChunkLimit`: outbound text chunk size (chars). Default: 2000.
+- `chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
+- `maxLinesPerMessage`: soft max line count per message. Default: 17.
+- `mediaMaxMb`: clamp inbound media saved to disk.
+- `historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20; falls back to `messages.groupChat.historyLimit`; `0` disables).
+- `dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `dms[""].historyLimit`.
+- `retry`: retry policy for outbound Discord API calls (attempts, minDelayMs, maxDelayMs, jitter).
+- `pluralkit`: resolve PluralKit proxied messages so system members appear as distinct senders.
+- `actions`: per-action tool gates; omit to allow all (set `false` to disable).
+ - `reactions` (covers react + read reactions)
+ - `stickers`, `emojiUploads`, `stickerUploads`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search`
+ - `memberInfo`, `roleInfo`, `channelInfo`, `voiceStatus`, `events`
+ - `channels` (create/edit/delete channels + categories + permissions)
+ - `roles` (role add/remove, default `false`)
+ - `moderation` (timeout/kick/ban, default `false`)
+- `execApprovals`: Discord-only exec approval DMs (button UI). Supports `enabled`, `approvers`, `agentFilter`, `sessionFilter`.
+
+Reaction notifications use `guilds..reactionNotifications`:
+
+- `off`: no reaction events.
+- `own`: reactions on the bot's own messages (default).
+- `all`: all reactions on all messages.
+- `allowlist`: reactions from `guilds..users` on all messages (empty list disables).
+
+### PluralKit (PK) support
+
+Enable PK lookups so proxied messages resolve to the underlying system + member.
+When enabled, OpenClaw uses the member identity for allowlists and labels the
+sender as `Member (PK:System)` to avoid accidental Discord pings.
+
+```json5
+{
+ channels: {
+ discord: {
+ pluralkit: {
+ enabled: true,
+ token: "pk_live_...", // optional; required for private systems
+ },
+ },
+ },
+}
+```
+
+Allowlist notes (PK-enabled):
+
+- Use `pk:` in `dm.allowFrom`, `guilds..users`, or per-channel `users`.
+- Member display names are also matched by name/slug.
+- Lookups use the **original** Discord message ID (the pre-proxy message), so
+ the PK API only resolves it within its 30-minute window.
+- If PK lookups fail (e.g., private system without a token), proxied messages
+ are treated as bot messages and are dropped unless `channels.discord.allowBots=true`.
+
+### Tool action defaults
+
+| Action group | Default | Notes |
+| -------------- | -------- | ---------------------------------- |
+| reactions | enabled | React + list reactions + emojiList |
+| stickers | enabled | Send stickers |
+| emojiUploads | enabled | Upload emojis |
+| stickerUploads | enabled | Upload stickers |
+| polls | enabled | Create polls |
+| permissions | enabled | Channel permission snapshot |
+| messages | enabled | Read/send/edit/delete |
+| threads | enabled | Create/list/reply |
+| pins | enabled | Pin/unpin/list |
+| search | enabled | Message search (preview feature) |
+| memberInfo | enabled | Member info |
+| roleInfo | enabled | Role list |
+| channelInfo | enabled | Channel info + list |
+| channels | enabled | Channel/category management |
+| voiceStatus | enabled | Voice state lookup |
+| events | enabled | List/create scheduled events |
+| roles | disabled | Role add/remove |
+| moderation | disabled | Timeout/kick/ban |
+
+- `replyToMode`: `off` (default), `first`, or `all`. Applies only when the model includes a reply tag.
+
+## Reply tags
+
+To request a threaded reply, the model can include one tag in its output:
+
+- `[[reply_to_current]]` — reply to the triggering Discord message.
+- `[[reply_to:]]` — reply to a specific message id from context/history.
+ Current message ids are appended to prompts as `[message_id: …]`; history entries already include ids.
+
+Behavior is controlled by `channels.discord.replyToMode`:
+
+- `off`: ignore tags.
+- `first`: only the first outbound chunk/attachment is a reply.
+- `all`: every outbound chunk/attachment is a reply.
+
+Allowlist matching notes:
+
+- `allowFrom`/`users`/`groupChannels` accept ids, names, tags, or mentions like `<@id>`.
+- Prefixes like `discord:`/`user:` (users) and `channel:` (group DMs) are supported.
+- Use `*` to allow any sender/channel.
+- When `guilds..channels` is present, channels not listed are denied by default.
+- When `guilds..channels` is omitted, all channels in the allowlisted guild are allowed.
+- To allow **no channels**, set `channels.discord.groupPolicy: "disabled"` (or keep an empty allowlist).
+- The configure wizard accepts `Guild/Channel` names (public + private) and resolves them to IDs when possible.
+- On startup, OpenClaw resolves channel/user names in allowlists to IDs (when the bot can search members)
+ and logs the mapping; unresolved entries are kept as typed.
+
+Native command notes:
+
+- The registered commands mirror OpenClaw’s chat commands.
+- Native commands honor the same allowlists as DMs/guild messages (`channels.discord.dm.allowFrom`, `channels.discord.guilds`, per-channel rules).
+- Slash commands may still be visible in Discord UI to users who aren’t allowlisted; OpenClaw enforces allowlists on execution and replies “not authorized”.
+
+## Tool actions
+
+The agent can call `discord` with actions like:
+
+- `react` / `reactions` (add or list reactions)
+- `sticker`, `poll`, `permissions`
+- `readMessages`, `sendMessage`, `editMessage`, `deleteMessage`
+- Read/search/pin tool payloads include normalized `timestampMs` (UTC epoch ms) and `timestampUtc` alongside raw Discord `timestamp`.
+- `threadCreate`, `threadList`, `threadReply`
+- `pinMessage`, `unpinMessage`, `listPins`
+- `searchMessages`, `memberInfo`, `roleInfo`, `roleAdd`, `roleRemove`, `emojiList`
+- `channelInfo`, `channelList`, `voiceStatus`, `eventList`, `eventCreate`
+- `timeout`, `kick`, `ban`
+
+Discord message ids are surfaced in the injected context (`[discord message id: …]` and history lines) so the agent can target them.
+Emoji can be unicode (e.g., `✅`) or custom emoji syntax like `<:party_blob:1234567890>`.
+
+## Safety & ops
+
+- Treat the bot token like a password; prefer the `DISCORD_BOT_TOKEN` env var on supervised hosts or lock down the config file permissions.
+- Only grant the bot permissions it needs (typically Read/Send Messages).
+- If the bot is stuck or rate limited, restart the gateway (`openclaw gateway --force`) after confirming no other processes own the Discord session.
diff --git a/docs/channels/googlechat.md b/docs/channels/googlechat.md
new file mode 100644
index 0000000000000000000000000000000000000000..07c7dd7dc6964ce0141f1f3eeab932a1032c4ff1
--- /dev/null
+++ b/docs/channels/googlechat.md
@@ -0,0 +1,250 @@
+---
+summary: "Google Chat app support status, capabilities, and configuration"
+read_when:
+ - Working on Google Chat channel features
+title: "Google Chat"
+---
+
+# Google Chat (Chat API)
+
+Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only).
+
+## Quick setup (beginner)
+
+1. Create a Google Cloud project and enable the **Google Chat API**.
+ - Go to: [Google Chat API Credentials](https://console.cloud.google.com/apis/api/chat.googleapis.com/credentials)
+ - Enable the API if it is not already enabled.
+2. Create a **Service Account**:
+ - Press **Create Credentials** > **Service Account**.
+ - Name it whatever you want (e.g., `openclaw-chat`).
+ - Leave permissions blank (press **Continue**).
+ - Leave principals with access blank (press **Done**).
+3. Create and download the **JSON Key**:
+ - In the list of service accounts, click on the one you just created.
+ - Go to the **Keys** tab.
+ - Click **Add Key** > **Create new key**.
+ - Select **JSON** and press **Create**.
+4. Store the downloaded JSON file on your gateway host (e.g., `~/.openclaw/googlechat-service-account.json`).
+5. Create a Google Chat app in the [Google Cloud Console Chat Configuration](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat):
+ - Fill in the **Application info**:
+ - **App name**: (e.g. `OpenClaw`)
+ - **Avatar URL**: (e.g. `https://openclaw.ai/logo.png`)
+ - **Description**: (e.g. `Personal AI Assistant`)
+ - Enable **Interactive features**.
+ - Under **Functionality**, check **Join spaces and group conversations**.
+ - Under **Connection settings**, select **HTTP endpoint URL**.
+ - Under **Triggers**, select **Use a common HTTP endpoint URL for all triggers** and set it to your gateway's public URL followed by `/googlechat`.
+ - _Tip: Run `openclaw status` to find your gateway's public URL._
+ - Under **Visibility**, check **Make this Chat app available to specific people and groups in <Your Domain>**.
+ - Enter your email address (e.g. `user@example.com`) in the text box.
+ - Click **Save** at the bottom.
+6. **Enable the app status**:
+ - After saving, **refresh the page**.
+ - Look for the **App status** section (usually near the top or bottom after saving).
+ - Change the status to **Live - available to users**.
+ - Click **Save** again.
+7. Configure OpenClaw with the service account path + webhook audience:
+ - Env: `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE=/path/to/service-account.json`
+ - Or config: `channels.googlechat.serviceAccountFile: "/path/to/service-account.json"`.
+8. Set the webhook audience type + value (matches your Chat app config).
+9. Start the gateway. Google Chat will POST to your webhook path.
+
+## Add to Google Chat
+
+Once the gateway is running and your email is added to the visibility list:
+
+1. Go to [Google Chat](https://chat.google.com/).
+2. Click the **+** (plus) icon next to **Direct Messages**.
+3. In the search bar (where you usually add people), type the **App name** you configured in the Google Cloud Console.
+ - **Note**: The bot will _not_ appear in the "Marketplace" browse list because it is a private app. You must search for it by name.
+4. Select your bot from the results.
+5. Click **Add** or **Chat** to start a 1:1 conversation.
+6. Send "Hello" to trigger the assistant!
+
+## Public URL (Webhook-only)
+
+Google Chat webhooks require a public HTTPS endpoint. For security, **only expose the `/googlechat` path** to the internet. Keep the OpenClaw dashboard and other sensitive endpoints on your private network.
+
+### Option A: Tailscale Funnel (Recommended)
+
+Use Tailscale Serve for the private dashboard and Funnel for the public webhook path. This keeps `/` private while exposing only `/googlechat`.
+
+1. **Check what address your gateway is bound to:**
+
+ ```bash
+ ss -tlnp | grep 18789
+ ```
+
+ Note the IP address (e.g., `127.0.0.1`, `0.0.0.0`, or your Tailscale IP like `100.x.x.x`).
+
+2. **Expose the dashboard to the tailnet only (port 8443):**
+
+ ```bash
+ # If bound to localhost (127.0.0.1 or 0.0.0.0):
+ tailscale serve --bg --https 8443 http://127.0.0.1:18789
+
+ # If bound to Tailscale IP only (e.g., 100.106.161.80):
+ tailscale serve --bg --https 8443 http://100.106.161.80:18789
+ ```
+
+3. **Expose only the webhook path publicly:**
+
+ ```bash
+ # If bound to localhost (127.0.0.1 or 0.0.0.0):
+ tailscale funnel --bg --set-path /googlechat http://127.0.0.1:18789/googlechat
+
+ # If bound to Tailscale IP only (e.g., 100.106.161.80):
+ tailscale funnel --bg --set-path /googlechat http://100.106.161.80:18789/googlechat
+ ```
+
+4. **Authorize the node for Funnel access:**
+ If prompted, visit the authorization URL shown in the output to enable Funnel for this node in your tailnet policy.
+
+5. **Verify the configuration:**
+ ```bash
+ tailscale serve status
+ tailscale funnel status
+ ```
+
+Your public webhook URL will be:
+`https://..ts.net/googlechat`
+
+Your private dashboard stays tailnet-only:
+`https://..ts.net:8443/`
+
+Use the public URL (without `:8443`) in the Google Chat app config.
+
+> Note: This configuration persists across reboots. To remove it later, run `tailscale funnel reset` and `tailscale serve reset`.
+
+### Option B: Reverse Proxy (Caddy)
+
+If you use a reverse proxy like Caddy, only proxy the specific path:
+
+```caddy
+your-domain.com {
+ reverse_proxy /googlechat* localhost:18789
+}
+```
+
+With this config, any request to `your-domain.com/` will be ignored or returned as 404, while `your-domain.com/googlechat` is safely routed to OpenClaw.
+
+### Option C: Cloudflare Tunnel
+
+Configure your tunnel's ingress rules to only route the webhook path:
+
+- **Path**: `/googlechat` -> `http://localhost:18789/googlechat`
+- **Default Rule**: HTTP 404 (Not Found)
+
+## How it works
+
+1. Google Chat sends webhook POSTs to the gateway. Each request includes an `Authorization: Bearer ` header.
+2. OpenClaw verifies the token against the configured `audienceType` + `audience`:
+ - `audienceType: "app-url"` → audience is your HTTPS webhook URL.
+ - `audienceType: "project-number"` → audience is the Cloud project number.
+3. Messages are routed by space:
+ - DMs use session key `agent::googlechat:dm:`.
+ - Spaces use session key `agent::googlechat:group:`.
+4. DM access is pairing by default. Unknown senders receive a pairing code; approve with:
+ - `openclaw pairing approve googlechat `
+5. Group spaces require @-mention by default. Use `botUser` if mention detection needs the app’s user name.
+
+## Targets
+
+Use these identifiers for delivery and allowlists:
+
+- Direct messages: `users/` or `users/` (email addresses are accepted).
+- Spaces: `spaces/`.
+
+## Config highlights
+
+```json5
+{
+ channels: {
+ googlechat: {
+ enabled: true,
+ serviceAccountFile: "/path/to/service-account.json",
+ audienceType: "app-url",
+ audience: "https://gateway.example.com/googlechat",
+ webhookPath: "/googlechat",
+ botUser: "users/1234567890", // optional; helps mention detection
+ dm: {
+ policy: "pairing",
+ allowFrom: ["users/1234567890", "name@example.com"],
+ },
+ groupPolicy: "allowlist",
+ groups: {
+ "spaces/AAAA": {
+ allow: true,
+ requireMention: true,
+ users: ["users/1234567890"],
+ systemPrompt: "Short answers only.",
+ },
+ },
+ actions: { reactions: true },
+ typingIndicator: "message",
+ mediaMaxMb: 20,
+ },
+ },
+}
+```
+
+Notes:
+
+- Service account credentials can also be passed inline with `serviceAccount` (JSON string).
+- Default webhook path is `/googlechat` if `webhookPath` isn’t set.
+- Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled.
+- `typingIndicator` supports `none`, `message` (default), and `reaction` (reaction requires user OAuth).
+- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`).
+
+## Troubleshooting
+
+### 405 Method Not Allowed
+
+If Google Cloud Logs Explorer shows errors like:
+
+```
+status code: 405, reason phrase: HTTP error response: HTTP/1.1 405 Method Not Allowed
+```
+
+This means the webhook handler isn't registered. Common causes:
+
+1. **Channel not configured**: The `channels.googlechat` section is missing from your config. Verify with:
+
+ ```bash
+ openclaw config get channels.googlechat
+ ```
+
+ If it returns "Config path not found", add the configuration (see [Config highlights](#config-highlights)).
+
+2. **Plugin not enabled**: Check plugin status:
+
+ ```bash
+ openclaw plugins list | grep googlechat
+ ```
+
+ If it shows "disabled", add `plugins.entries.googlechat.enabled: true` to your config.
+
+3. **Gateway not restarted**: After adding config, restart the gateway:
+ ```bash
+ openclaw gateway restart
+ ```
+
+Verify the channel is running:
+
+```bash
+openclaw channels status
+# Should show: Google Chat default: enabled, configured, ...
+```
+
+### Other issues
+
+- Check `openclaw channels status --probe` for auth errors or missing audience config.
+- If no messages arrive, confirm the Chat app's webhook URL + event subscriptions.
+- If mention gating blocks replies, set `botUser` to the app's user resource name and verify `requireMention`.
+- Use `openclaw logs --follow` while sending a test message to see if requests reach the gateway.
+
+Related docs:
+
+- [Gateway configuration](/gateway/configuration)
+- [Security](/gateway/security)
+- [Reactions](/tools/reactions)
diff --git a/docs/channels/grammy.md b/docs/channels/grammy.md
new file mode 100644
index 0000000000000000000000000000000000000000..1b73394ef7ebd25de3430a1e501f4d104444da6e
--- /dev/null
+++ b/docs/channels/grammy.md
@@ -0,0 +1,31 @@
+---
+summary: "Telegram Bot API integration via grammY with setup notes"
+read_when:
+ - Working on Telegram or grammY pathways
+title: grammY
+---
+
+# grammY Integration (Telegram Bot API)
+
+# Why grammY
+
+- TS-first Bot API client with built-in long-poll + webhook helpers, middleware, error handling, rate limiter.
+- Cleaner media helpers than hand-rolling fetch + FormData; supports all Bot API methods.
+- Extensible: proxy support via custom fetch, session middleware (optional), type-safe context.
+
+# What we shipped
+
+- **Single client path:** fetch-based implementation removed; grammY is now the sole Telegram client (send + gateway) with the grammY throttler enabled by default.
+- **Gateway:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`.
+- **Proxy:** optional `channels.telegram.proxy` uses `undici.ProxyAgent` through grammY’s `client.baseFetch`.
+- **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` are set (otherwise it long-polls).
+- **Sessions:** direct chats collapse into the agent main session (`agent::`); groups use `agent::telegram:group:`; replies route back to the same channel.
+- **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.linkPreview`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`.
+- **Draft streaming:** optional `channels.telegram.streamMode` uses `sendMessageDraft` in private topic chats (Bot API 9.3+). This is separate from channel block streaming.
+- **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
+
+Open questions
+
+- Optional grammY plugins (throttler) if we hit Bot API 429s.
+- Add more structured media tests (stickers, voice notes).
+- Make webhook listen port configurable (currently fixed to 8787 unless wired through the gateway).
diff --git a/docs/channels/imessage.md b/docs/channels/imessage.md
new file mode 100644
index 0000000000000000000000000000000000000000..e28e9340d3e425bfaf0be9e6ca9f3fe074816dfa
--- /dev/null
+++ b/docs/channels/imessage.md
@@ -0,0 +1,295 @@
+---
+summary: "iMessage support via imsg (JSON-RPC over stdio), setup, and chat_id routing"
+read_when:
+ - Setting up iMessage support
+ - Debugging iMessage send/receive
+title: iMessage
+---
+
+# iMessage (imsg)
+
+Status: external CLI integration. Gateway spawns `imsg rpc` (JSON-RPC over stdio).
+
+## Quick setup (beginner)
+
+1. Ensure Messages is signed in on this Mac.
+2. Install `imsg`:
+ - `brew install steipete/tap/imsg`
+3. Configure OpenClaw with `channels.imessage.cliPath` and `channels.imessage.dbPath`.
+4. Start the gateway and approve any macOS prompts (Automation + Full Disk Access).
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ imessage: {
+ enabled: true,
+ cliPath: "/usr/local/bin/imsg",
+ dbPath: "/Users//Library/Messages/chat.db",
+ },
+ },
+}
+```
+
+## What it is
+
+- iMessage channel backed by `imsg` on macOS.
+- Deterministic routing: replies always go back to iMessage.
+- DMs share the agent's main session; groups are isolated (`agent::imessage:group:`).
+- If a multi-participant thread arrives with `is_group=false`, you can still isolate it by `chat_id` using `channels.imessage.groups` (see “Group-ish threads” below).
+
+## Config writes
+
+By default, iMessage is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
+
+Disable with:
+
+```json5
+{
+ channels: { imessage: { configWrites: false } },
+}
+```
+
+## Requirements
+
+- macOS with Messages signed in.
+- Full Disk Access for OpenClaw + `imsg` (Messages DB access).
+- Automation permission when sending.
+- `channels.imessage.cliPath` can point to any command that proxies stdin/stdout (for example, a wrapper script that SSHes to another Mac and runs `imsg rpc`).
+
+## Setup (fast path)
+
+1. Ensure Messages is signed in on this Mac.
+2. Configure iMessage and start the gateway.
+
+### Dedicated bot macOS user (for isolated identity)
+
+If you want the bot to send from a **separate iMessage identity** (and keep your personal Messages clean), use a dedicated Apple ID + a dedicated macOS user.
+
+1. Create a dedicated Apple ID (example: `my-cool-bot@icloud.com`).
+ - Apple may require a phone number for verification / 2FA.
+2. Create a macOS user (example: `openclawhome`) and sign into it.
+3. Open Messages in that macOS user and sign into iMessage using the bot Apple ID.
+4. Enable Remote Login (System Settings → General → Sharing → Remote Login).
+5. Install `imsg`:
+ - `brew install steipete/tap/imsg`
+6. Set up SSH so `ssh @localhost true` works without a password.
+7. Point `channels.imessage.accounts.bot.cliPath` at an SSH wrapper that runs `imsg` as the bot user.
+
+First-run note: sending/receiving may require GUI approvals (Automation + Full Disk Access) in the _bot macOS user_. If `imsg rpc` looks stuck or exits, log into that user (Screen Sharing helps), run a one-time `imsg chats --limit 1` / `imsg send ...`, approve prompts, then retry.
+
+Example wrapper (`chmod +x`). Replace `` with your actual macOS username:
+
+```bash
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Run an interactive SSH once first to accept host keys:
+# ssh @localhost true
+exec /usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 -T @localhost \
+ "/usr/local/bin/imsg" "$@"
+```
+
+Example config:
+
+```json5
+{
+ channels: {
+ imessage: {
+ enabled: true,
+ accounts: {
+ bot: {
+ name: "Bot",
+ enabled: true,
+ cliPath: "/path/to/imsg-bot",
+ dbPath: "/Users//Library/Messages/chat.db",
+ },
+ },
+ },
+ },
+}
+```
+
+For single-account setups, use flat options (`channels.imessage.cliPath`, `channels.imessage.dbPath`) instead of the `accounts` map.
+
+### Remote/SSH variant (optional)
+
+If you want iMessage on another Mac, set `channels.imessage.cliPath` to a wrapper that runs `imsg` on the remote macOS host over SSH. OpenClaw only needs stdio.
+
+Example wrapper:
+
+```bash
+#!/usr/bin/env bash
+exec ssh -T gateway-host imsg "$@"
+```
+
+**Remote attachments:** When `cliPath` points to a remote host via SSH, attachment paths in the Messages database reference files on the remote machine. OpenClaw can automatically fetch these over SCP by setting `channels.imessage.remoteHost`:
+
+```json5
+{
+ channels: {
+ imessage: {
+ cliPath: "~/imsg-ssh", // SSH wrapper to remote Mac
+ remoteHost: "user@gateway-host", // for SCP file transfer
+ includeAttachments: true,
+ },
+ },
+}
+```
+
+If `remoteHost` is not set, OpenClaw attempts to auto-detect it by parsing the SSH command in your wrapper script. Explicit configuration is recommended for reliability.
+
+#### Remote Mac via Tailscale (example)
+
+If the Gateway runs on a Linux host/VM but iMessage must run on a Mac, Tailscale is the simplest bridge: the Gateway talks to the Mac over the tailnet, runs `imsg` via SSH, and SCPs attachments back.
+
+Architecture:
+
+```
+┌──────────────────────────────┐ SSH (imsg rpc) ┌──────────────────────────┐
+│ Gateway host (Linux/VM) │──────────────────────────────────▶│ Mac with Messages + imsg │
+│ - openclaw gateway │ SCP (attachments) │ - Messages signed in │
+│ - channels.imessage.cliPath │◀──────────────────────────────────│ - Remote Login enabled │
+└──────────────────────────────┘ └──────────────────────────┘
+ ▲
+ │ Tailscale tailnet (hostname or 100.x.y.z)
+ ▼
+ user@gateway-host
+```
+
+Concrete config example (Tailscale hostname):
+
+```json5
+{
+ channels: {
+ imessage: {
+ enabled: true,
+ cliPath: "~/.openclaw/scripts/imsg-ssh",
+ remoteHost: "bot@mac-mini.tailnet-1234.ts.net",
+ includeAttachments: true,
+ dbPath: "/Users/bot/Library/Messages/chat.db",
+ },
+ },
+}
+```
+
+Example wrapper (`~/.openclaw/scripts/imsg-ssh`):
+
+```bash
+#!/usr/bin/env bash
+exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"
+```
+
+Notes:
+
+- Ensure the Mac is signed in to Messages, and Remote Login is enabled.
+- Use SSH keys so `ssh bot@mac-mini.tailnet-1234.ts.net` works without prompts.
+- `remoteHost` should match the SSH target so SCP can fetch attachments.
+
+Multi-account support: use `channels.imessage.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. Don't commit `~/.openclaw/openclaw.json` (it often contains tokens).
+
+## Access control (DMs + groups)
+
+DMs:
+
+- Default: `channels.imessage.dmPolicy = "pairing"`.
+- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
+- Approve via:
+ - `openclaw pairing list imessage`
+ - `openclaw pairing approve imessage `
+- Pairing is the default token exchange for iMessage DMs. Details: [Pairing](/start/pairing)
+
+Groups:
+
+- `channels.imessage.groupPolicy = open | allowlist | disabled`.
+- `channels.imessage.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
+- Mention gating uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) because iMessage has no native mention metadata.
+- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
+
+## How it works (behavior)
+
+- `imsg` streams message events; the gateway normalizes them into the shared channel envelope.
+- Replies always route back to the same chat id or handle.
+
+## Group-ish threads (`is_group=false`)
+
+Some iMessage threads can have multiple participants but still arrive with `is_group=false` depending on how Messages stores the chat identifier.
+
+If you explicitly configure a `chat_id` under `channels.imessage.groups`, OpenClaw treats that thread as a “group” for:
+
+- session isolation (separate `agent::imessage:group:` session key)
+- group allowlisting / mention gating behavior
+
+Example:
+
+```json5
+{
+ channels: {
+ imessage: {
+ groupPolicy: "allowlist",
+ groupAllowFrom: ["+15555550123"],
+ groups: {
+ "42": { requireMention: false },
+ },
+ },
+ },
+}
+```
+
+This is useful when you want an isolated personality/model for a specific thread (see [Multi-agent routing](/concepts/multi-agent)). For filesystem isolation, see [Sandboxing](/gateway/sandboxing).
+
+## Media + limits
+
+- Optional attachment ingestion via `channels.imessage.includeAttachments`.
+- Media cap via `channels.imessage.mediaMaxMb`.
+
+## Limits
+
+- Outbound text is chunked to `channels.imessage.textChunkLimit` (default 4000).
+- Optional newline chunking: set `channels.imessage.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
+- Media uploads are capped by `channels.imessage.mediaMaxMb` (default 16).
+
+## Addressing / delivery targets
+
+Prefer `chat_id` for stable routing:
+
+- `chat_id:123` (preferred)
+- `chat_guid:...`
+- `chat_identifier:...`
+- direct handles: `imessage:+1555` / `sms:+1555` / `user@example.com`
+
+List chats:
+
+```
+imsg chats --limit 20
+```
+
+## Configuration reference (iMessage)
+
+Full configuration: [Configuration](/gateway/configuration)
+
+Provider options:
+
+- `channels.imessage.enabled`: enable/disable channel startup.
+- `channels.imessage.cliPath`: path to `imsg`.
+- `channels.imessage.dbPath`: Messages DB path.
+- `channels.imessage.remoteHost`: SSH host for SCP attachment transfer when `cliPath` points to a remote Mac (e.g., `user@gateway-host`). Auto-detected from SSH wrapper if not set.
+- `channels.imessage.service`: `imessage | sms | auto`.
+- `channels.imessage.region`: SMS region.
+- `channels.imessage.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
+- `channels.imessage.allowFrom`: DM allowlist (handles, emails, E.164 numbers, or `chat_id:*`). `open` requires `"*"`. iMessage has no usernames; use handles or chat targets.
+- `channels.imessage.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
+- `channels.imessage.groupAllowFrom`: group sender allowlist.
+- `channels.imessage.historyLimit` / `channels.imessage.accounts.*.historyLimit`: max group messages to include as context (0 disables).
+- `channels.imessage.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.imessage.dms[""].historyLimit`.
+- `channels.imessage.groups`: per-group defaults + allowlist (use `"*"` for global defaults).
+- `channels.imessage.includeAttachments`: ingest attachments into context.
+- `channels.imessage.mediaMaxMb`: inbound/outbound media cap (MB).
+- `channels.imessage.textChunkLimit`: outbound chunk size (chars).
+- `channels.imessage.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
+
+Related global options:
+
+- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).
+- `messages.responsePrefix`.
diff --git a/docs/channels/index.md b/docs/channels/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..eba433a7fd44529768dc3ac00687608920cf1cee
--- /dev/null
+++ b/docs/channels/index.md
@@ -0,0 +1,45 @@
+---
+summary: "Messaging platforms OpenClaw can connect to"
+read_when:
+ - You want to choose a chat channel for OpenClaw
+ - You need a quick overview of supported messaging platforms
+title: "Chat Channels"
+---
+
+# Chat Channels
+
+OpenClaw can talk to you on any chat app you already use. Each channel connects via the Gateway.
+Text is supported everywhere; media and reactions vary by channel.
+
+## Supported channels
+
+- [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing.
+- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
+- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
+- [Slack](/channels/slack) — Bolt SDK; workspace apps.
+- [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook.
+- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).
+- [Signal](/channels/signal) — signal-cli; privacy-focused.
+- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
+- [iMessage](/channels/imessage) — macOS only; native integration via imsg (legacy, consider BlueBubbles for new setups).
+- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
+- [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately).
+- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
+- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
+- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
+- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
+- [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately).
+- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
+- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
+- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
+
+## Notes
+
+- Channels can run simultaneously; configure multiple and OpenClaw will route per chat.
+- Fastest setup is usually **Telegram** (simple bot token). WhatsApp requires QR pairing and
+ stores more state on disk.
+- Group behavior varies by channel; see [Groups](/concepts/groups).
+- DM pairing and allowlists are enforced for safety; see [Security](/gateway/security).
+- Telegram internals: [grammY notes](/channels/grammy).
+- Troubleshooting: [Channel troubleshooting](/channels/troubleshooting).
+- Model providers are documented separately; see [Model Providers](/providers/models).
diff --git a/docs/channels/line.md b/docs/channels/line.md
new file mode 100644
index 0000000000000000000000000000000000000000..f68ae5aa1e8ca435e13bd7354d6385c448c5c128
--- /dev/null
+++ b/docs/channels/line.md
@@ -0,0 +1,186 @@
+---
+summary: "LINE Messaging API plugin setup, config, and usage"
+read_when:
+ - You want to connect OpenClaw to LINE
+ - You need LINE webhook + credential setup
+ - You want LINE-specific message options
+title: LINE
+---
+
+# LINE (plugin)
+
+LINE connects to OpenClaw via the LINE Messaging API. The plugin runs as a webhook
+receiver on the gateway and uses your channel access token + channel secret for
+authentication.
+
+Status: supported via plugin. Direct messages, group chats, media, locations, Flex
+messages, template messages, and quick replies are supported. Reactions and threads
+are not supported.
+
+## Plugin required
+
+Install the LINE plugin:
+
+```bash
+openclaw plugins install @openclaw/line
+```
+
+Local checkout (when running from a git repo):
+
+```bash
+openclaw plugins install ./extensions/line
+```
+
+## Setup
+
+1. Create a LINE Developers account and open the Console:
+ https://developers.line.biz/console/
+2. Create (or pick) a Provider and add a **Messaging API** channel.
+3. Copy the **Channel access token** and **Channel secret** from the channel settings.
+4. Enable **Use webhook** in the Messaging API settings.
+5. Set the webhook URL to your gateway endpoint (HTTPS required):
+
+```
+https://gateway-host/line/webhook
+```
+
+The gateway responds to LINE’s webhook verification (GET) and inbound events (POST).
+If you need a custom path, set `channels.line.webhookPath` or
+`channels.line.accounts..webhookPath` and update the URL accordingly.
+
+## Configure
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ line: {
+ enabled: true,
+ channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
+ channelSecret: "LINE_CHANNEL_SECRET",
+ dmPolicy: "pairing",
+ },
+ },
+}
+```
+
+Env vars (default account only):
+
+- `LINE_CHANNEL_ACCESS_TOKEN`
+- `LINE_CHANNEL_SECRET`
+
+Token/secret files:
+
+```json5
+{
+ channels: {
+ line: {
+ tokenFile: "/path/to/line-token.txt",
+ secretFile: "/path/to/line-secret.txt",
+ },
+ },
+}
+```
+
+Multiple accounts:
+
+```json5
+{
+ channels: {
+ line: {
+ accounts: {
+ marketing: {
+ channelAccessToken: "...",
+ channelSecret: "...",
+ webhookPath: "/line/marketing",
+ },
+ },
+ },
+ },
+}
+```
+
+## Access control
+
+Direct messages default to pairing. Unknown senders get a pairing code and their
+messages are ignored until approved.
+
+```bash
+openclaw pairing list line
+openclaw pairing approve line
+```
+
+Allowlists and policies:
+
+- `channels.line.dmPolicy`: `pairing | allowlist | open | disabled`
+- `channels.line.allowFrom`: allowlisted LINE user IDs for DMs
+- `channels.line.groupPolicy`: `allowlist | open | disabled`
+- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups
+- Per-group overrides: `channels.line.groups..allowFrom`
+
+LINE IDs are case-sensitive. Valid IDs look like:
+
+- User: `U` + 32 hex chars
+- Group: `C` + 32 hex chars
+- Room: `R` + 32 hex chars
+
+## Message behavior
+
+- Text is chunked at 5000 characters.
+- Markdown formatting is stripped; code blocks and tables are converted into Flex
+ cards when possible.
+- Streaming responses are buffered; LINE receives full chunks with a loading
+ animation while the agent works.
+- Media downloads are capped by `channels.line.mediaMaxMb` (default 10).
+
+## Channel data (rich messages)
+
+Use `channelData.line` to send quick replies, locations, Flex cards, or template
+messages.
+
+```json5
+{
+ text: "Here you go",
+ channelData: {
+ line: {
+ quickReplies: ["Status", "Help"],
+ location: {
+ title: "Office",
+ address: "123 Main St",
+ latitude: 35.681236,
+ longitude: 139.767125,
+ },
+ flexMessage: {
+ altText: "Status card",
+ contents: {
+ /* Flex payload */
+ },
+ },
+ templateMessage: {
+ type: "confirm",
+ text: "Proceed?",
+ confirmLabel: "Yes",
+ confirmData: "yes",
+ cancelLabel: "No",
+ cancelData: "no",
+ },
+ },
+ },
+}
+```
+
+The LINE plugin also ships a `/card` command for Flex message presets:
+
+```
+/card info "Welcome" "Thanks for joining!"
+```
+
+## Troubleshooting
+
+- **Webhook verification fails:** ensure the webhook URL is HTTPS and the
+ `channelSecret` matches the LINE console.
+- **No inbound events:** confirm the webhook path matches `channels.line.webhookPath`
+ and that the gateway is reachable from LINE.
+- **Media download errors:** raise `channels.line.mediaMaxMb` if media exceeds the
+ default limit.
diff --git a/docs/channels/location.md b/docs/channels/location.md
new file mode 100644
index 0000000000000000000000000000000000000000..103f57663c4b1171bd5a6dfc5bdf3ba839079eec
--- /dev/null
+++ b/docs/channels/location.md
@@ -0,0 +1,56 @@
+---
+summary: "Inbound channel location parsing (Telegram + WhatsApp) and context fields"
+read_when:
+ - Adding or modifying channel location parsing
+ - Using location context fields in agent prompts or tools
+title: "Channel Location Parsing"
+---
+
+# Channel location parsing
+
+OpenClaw normalizes shared locations from chat channels into:
+
+- human-readable text appended to the inbound body, and
+- structured fields in the auto-reply context payload.
+
+Currently supported:
+
+- **Telegram** (location pins + venues + live locations)
+- **WhatsApp** (locationMessage + liveLocationMessage)
+- **Matrix** (`m.location` with `geo_uri`)
+
+## Text formatting
+
+Locations are rendered as friendly lines without brackets:
+
+- Pin:
+ - `📍 48.858844, 2.294351 ±12m`
+- Named place:
+ - `📍 Eiffel Tower — Champ de Mars, Paris (48.858844, 2.294351 ±12m)`
+- Live share:
+ - `🛰 Live location: 48.858844, 2.294351 ±12m`
+
+If the channel includes a caption/comment, it is appended on the next line:
+
+```
+📍 48.858844, 2.294351 ±12m
+Meet here
+```
+
+## Context fields
+
+When a location is present, these fields are added to `ctx`:
+
+- `LocationLat` (number)
+- `LocationLon` (number)
+- `LocationAccuracy` (number, meters; optional)
+- `LocationName` (string; optional)
+- `LocationAddress` (string; optional)
+- `LocationSource` (`pin | place | live`)
+- `LocationIsLive` (boolean)
+
+## Channel notes
+
+- **Telegram**: venues map to `LocationName/LocationAddress`; live locations use `live_period`.
+- **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` are appended as the caption line.
+- **Matrix**: `geo_uri` is parsed as a pin location; altitude is ignored and `LocationIsLive` is always false.
diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md
new file mode 100644
index 0000000000000000000000000000000000000000..c6a818fab928d25d272589e4c7836ac3d1cce09b
--- /dev/null
+++ b/docs/channels/matrix.md
@@ -0,0 +1,233 @@
+---
+summary: "Matrix support status, capabilities, and configuration"
+read_when:
+ - Working on Matrix channel features
+title: "Matrix"
+---
+
+# Matrix (plugin)
+
+Matrix is an open, decentralized messaging protocol. OpenClaw connects as a Matrix **user**
+on any homeserver, so you need a Matrix account for the bot. Once it is logged in, you can DM
+the bot directly or invite it to rooms (Matrix "groups"). Beeper is a valid client option too,
+but it requires E2EE to be enabled.
+
+Status: supported via plugin (@vector-im/matrix-bot-sdk). Direct messages, rooms, threads, media, reactions,
+polls (send + poll-start as text), location, and E2EE (with crypto support).
+
+## Plugin required
+
+Matrix ships as a plugin and is not bundled with the core install.
+
+Install via CLI (npm registry):
+
+```bash
+openclaw plugins install @openclaw/matrix
+```
+
+Local checkout (when running from a git repo):
+
+```bash
+openclaw plugins install ./extensions/matrix
+```
+
+If you choose Matrix during configure/onboarding and a git checkout is detected,
+OpenClaw will offer the local install path automatically.
+
+Details: [Plugins](/plugin)
+
+## Setup
+
+1. Install the Matrix plugin:
+ - From npm: `openclaw plugins install @openclaw/matrix`
+ - From a local checkout: `openclaw plugins install ./extensions/matrix`
+2. Create a Matrix account on a homeserver:
+ - Browse hosting options at [https://matrix.org/ecosystem/hosting/](https://matrix.org/ecosystem/hosting/)
+ - Or host it yourself.
+3. Get an access token for the bot account:
+ - Use the Matrix login API with `curl` at your home server:
+
+ ```bash
+ curl --request POST \
+ --url https://matrix.example.org/_matrix/client/v3/login \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "type": "m.login.password",
+ "identifier": {
+ "type": "m.id.user",
+ "user": "your-user-name"
+ },
+ "password": "your-password"
+ }'
+ ```
+
+ - Replace `matrix.example.org` with your homeserver URL.
+ - Or set `channels.matrix.userId` + `channels.matrix.password`: OpenClaw calls the same
+ login endpoint, stores the access token in `~/.openclaw/credentials/matrix/credentials.json`,
+ and reuses it on next start.
+
+4. Configure credentials:
+ - Env: `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_USER_ID` + `MATRIX_PASSWORD`)
+ - Or config: `channels.matrix.*`
+ - If both are set, config takes precedence.
+ - With access token: user ID is fetched automatically via `/whoami`.
+ - When set, `channels.matrix.userId` should be the full Matrix ID (example: `@bot:example.org`).
+5. Restart the gateway (or finish onboarding).
+6. Start a DM with the bot or invite it to a room from any Matrix client
+ (Element, Beeper, etc.; see https://matrix.org/ecosystem/clients/). Beeper requires E2EE,
+ so set `channels.matrix.encryption: true` and verify the device.
+
+Minimal config (access token, user ID auto-fetched):
+
+```json5
+{
+ channels: {
+ matrix: {
+ enabled: true,
+ homeserver: "https://matrix.example.org",
+ accessToken: "syt_***",
+ dm: { policy: "pairing" },
+ },
+ },
+}
+```
+
+E2EE config (end to end encryption enabled):
+
+```json5
+{
+ channels: {
+ matrix: {
+ enabled: true,
+ homeserver: "https://matrix.example.org",
+ accessToken: "syt_***",
+ encryption: true,
+ dm: { policy: "pairing" },
+ },
+ },
+}
+```
+
+## Encryption (E2EE)
+
+End-to-end encryption is **supported** via the Rust crypto SDK.
+
+Enable with `channels.matrix.encryption: true`:
+
+- If the crypto module loads, encrypted rooms are decrypted automatically.
+- Outbound media is encrypted when sending to encrypted rooms.
+- On first connection, OpenClaw requests device verification from your other sessions.
+- Verify the device in another Matrix client (Element, etc.) to enable key sharing.
+- If the crypto module cannot be loaded, E2EE is disabled and encrypted rooms will not decrypt;
+ OpenClaw logs a warning.
+- If you see missing crypto module errors (for example, `@matrix-org/matrix-sdk-crypto-nodejs-*`),
+ allow build scripts for `@matrix-org/matrix-sdk-crypto-nodejs` and run
+ `pnpm rebuild @matrix-org/matrix-sdk-crypto-nodejs` or fetch the binary with
+ `node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js`.
+
+Crypto state is stored per account + access token in
+`~/.openclaw/matrix/accounts//__//crypto/`
+(SQLite database). Sync state lives alongside it in `bot-storage.json`.
+If the access token (device) changes, a new store is created and the bot must be
+re-verified for encrypted rooms.
+
+**Device verification:**
+When E2EE is enabled, the bot will request verification from your other sessions on startup.
+Open Element (or another client) and approve the verification request to establish trust.
+Once verified, the bot can decrypt messages in encrypted rooms.
+
+## Routing model
+
+- Replies always go back to Matrix.
+- DMs share the agent's main session; rooms map to group sessions.
+
+## Access control (DMs)
+
+- Default: `channels.matrix.dm.policy = "pairing"`. Unknown senders get a pairing code.
+- Approve via:
+ - `openclaw pairing list matrix`
+ - `openclaw pairing approve matrix `
+- Public DMs: `channels.matrix.dm.policy="open"` plus `channels.matrix.dm.allowFrom=["*"]`.
+- `channels.matrix.dm.allowFrom` accepts user IDs or display names. The wizard resolves display names to user IDs when directory search is available.
+
+## Rooms (groups)
+
+- Default: `channels.matrix.groupPolicy = "allowlist"` (mention-gated). Use `channels.defaults.groupPolicy` to override the default when unset.
+- Allowlist rooms with `channels.matrix.groups` (room IDs, aliases, or names):
+
+```json5
+{
+ channels: {
+ matrix: {
+ groupPolicy: "allowlist",
+ groups: {
+ "!roomId:example.org": { allow: true },
+ "#alias:example.org": { allow: true },
+ },
+ groupAllowFrom: ["@owner:example.org"],
+ },
+ },
+}
+```
+
+- `requireMention: false` enables auto-reply in that room.
+- `groups."*"` can set defaults for mention gating across rooms.
+- `groupAllowFrom` restricts which senders can trigger the bot in rooms (optional).
+- Per-room `users` allowlists can further restrict senders inside a specific room.
+- The configure wizard prompts for room allowlists (room IDs, aliases, or names) and resolves names when possible.
+- On startup, OpenClaw resolves room/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed.
+- Invites are auto-joined by default; control with `channels.matrix.autoJoin` and `channels.matrix.autoJoinAllowlist`.
+- To allow **no rooms**, set `channels.matrix.groupPolicy: "disabled"` (or keep an empty allowlist).
+- Legacy key: `channels.matrix.rooms` (same shape as `groups`).
+
+## Threads
+
+- Reply threading is supported.
+- `channels.matrix.threadReplies` controls whether replies stay in threads:
+ - `off`, `inbound` (default), `always`
+- `channels.matrix.replyToMode` controls reply-to metadata when not replying in a thread:
+ - `off` (default), `first`, `all`
+
+## Capabilities
+
+| Feature | Status |
+| --------------- | ------------------------------------------------------------------------------------- |
+| Direct messages | ✅ Supported |
+| Rooms | ✅ Supported |
+| Threads | ✅ Supported |
+| Media | ✅ Supported |
+| E2EE | ✅ Supported (crypto module required) |
+| Reactions | ✅ Supported (send/read via tools) |
+| Polls | ✅ Send supported; inbound poll starts are converted to text (responses/ends ignored) |
+| Location | ✅ Supported (geo URI; altitude ignored) |
+| Native commands | ✅ Supported |
+
+## Configuration reference (Matrix)
+
+Full configuration: [Configuration](/gateway/configuration)
+
+Provider options:
+
+- `channels.matrix.enabled`: enable/disable channel startup.
+- `channels.matrix.homeserver`: homeserver URL.
+- `channels.matrix.userId`: Matrix user ID (optional with access token).
+- `channels.matrix.accessToken`: access token.
+- `channels.matrix.password`: password for login (token stored).
+- `channels.matrix.deviceName`: device display name.
+- `channels.matrix.encryption`: enable E2EE (default: false).
+- `channels.matrix.initialSyncLimit`: initial sync limit.
+- `channels.matrix.threadReplies`: `off | inbound | always` (default: inbound).
+- `channels.matrix.textChunkLimit`: outbound text chunk size (chars).
+- `channels.matrix.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
+- `channels.matrix.dm.policy`: `pairing | allowlist | open | disabled` (default: pairing).
+- `channels.matrix.dm.allowFrom`: DM allowlist (user IDs or display names). `open` requires `"*"`. The wizard resolves names to IDs when possible.
+- `channels.matrix.groupPolicy`: `allowlist | open | disabled` (default: allowlist).
+- `channels.matrix.groupAllowFrom`: allowlisted senders for group messages.
+- `channels.matrix.allowlistOnly`: force allowlist rules for DMs + rooms.
+- `channels.matrix.groups`: group allowlist + per-room settings map.
+- `channels.matrix.rooms`: legacy group allowlist/config.
+- `channels.matrix.replyToMode`: reply-to mode for threads/tags.
+- `channels.matrix.mediaMaxMb`: inbound/outbound media cap (MB).
+- `channels.matrix.autoJoin`: invite handling (`always | allowlist | off`, default: always).
+- `channels.matrix.autoJoinAllowlist`: allowed room IDs/aliases for auto-join.
+- `channels.matrix.actions`: per-action tool gating (reactions/messages/pins/memberInfo/channelInfo).
diff --git a/docs/channels/mattermost.md b/docs/channels/mattermost.md
new file mode 100644
index 0000000000000000000000000000000000000000..8958f5b5b7e73cdb2f5aa4535d05578f45644fb1
--- /dev/null
+++ b/docs/channels/mattermost.md
@@ -0,0 +1,138 @@
+---
+summary: "Mattermost bot setup and OpenClaw config"
+read_when:
+ - Setting up Mattermost
+ - Debugging Mattermost routing
+title: "Mattermost"
+---
+
+# Mattermost (plugin)
+
+Status: supported via plugin (bot token + WebSocket events). Channels, groups, and DMs are supported.
+Mattermost is a self-hostable team messaging platform; see the official site at
+[mattermost.com](https://mattermost.com) for product details and downloads.
+
+## Plugin required
+
+Mattermost ships as a plugin and is not bundled with the core install.
+
+Install via CLI (npm registry):
+
+```bash
+openclaw plugins install @openclaw/mattermost
+```
+
+Local checkout (when running from a git repo):
+
+```bash
+openclaw plugins install ./extensions/mattermost
+```
+
+If you choose Mattermost during configure/onboarding and a git checkout is detected,
+OpenClaw will offer the local install path automatically.
+
+Details: [Plugins](/plugin)
+
+## Quick setup
+
+1. Install the Mattermost plugin.
+2. Create a Mattermost bot account and copy the **bot token**.
+3. Copy the Mattermost **base URL** (e.g., `https://chat.example.com`).
+4. Configure OpenClaw and start the gateway.
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ mattermost: {
+ enabled: true,
+ botToken: "mm-token",
+ baseUrl: "https://chat.example.com",
+ dmPolicy: "pairing",
+ },
+ },
+}
+```
+
+## Environment variables (default account)
+
+Set these on the gateway host if you prefer env vars:
+
+- `MATTERMOST_BOT_TOKEN=...`
+- `MATTERMOST_URL=https://chat.example.com`
+
+Env vars apply only to the **default** account (`default`). Other accounts must use config values.
+
+## Chat modes
+
+Mattermost responds to DMs automatically. Channel behavior is controlled by `chatmode`:
+
+- `oncall` (default): respond only when @mentioned in channels.
+- `onmessage`: respond to every channel message.
+- `onchar`: respond when a message starts with a trigger prefix.
+
+Config example:
+
+```json5
+{
+ channels: {
+ mattermost: {
+ chatmode: "onchar",
+ oncharPrefixes: [">", "!"],
+ },
+ },
+}
+```
+
+Notes:
+
+- `onchar` still responds to explicit @mentions.
+- `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred.
+
+## Access control (DMs)
+
+- Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code).
+- Approve via:
+ - `openclaw pairing list mattermost`
+ - `openclaw pairing approve mattermost `
+- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`.
+
+## Channels (groups)
+
+- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
+- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs or `@username`).
+- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
+
+## Targets for outbound delivery
+
+Use these target formats with `openclaw message send` or cron/webhooks:
+
+- `channel:` for a channel
+- `user:` for a DM
+- `@username` for a DM (resolved via the Mattermost API)
+
+Bare IDs are treated as channels.
+
+## Multi-account
+
+Mattermost supports multiple accounts under `channels.mattermost.accounts`:
+
+```json5
+{
+ channels: {
+ mattermost: {
+ accounts: {
+ default: { name: "Primary", botToken: "mm-token", baseUrl: "https://chat.example.com" },
+ alerts: { name: "Alerts", botToken: "mm-token-2", baseUrl: "https://alerts.example.com" },
+ },
+ },
+ },
+}
+```
+
+## Troubleshooting
+
+- No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`.
+- Auth errors: check the bot token, base URL, and whether the account is enabled.
+- Multi-account issues: env vars only apply to the `default` account.
diff --git a/docs/channels/msteams.md b/docs/channels/msteams.md
new file mode 100644
index 0000000000000000000000000000000000000000..8de3c4bf2cc46459569eafd10eba3268d1983573
--- /dev/null
+++ b/docs/channels/msteams.md
@@ -0,0 +1,766 @@
+---
+summary: "Microsoft Teams bot support status, capabilities, and configuration"
+read_when:
+ - Working on MS Teams channel features
+title: "Microsoft Teams"
+---
+
+# Microsoft Teams (plugin)
+
+> "Abandon all hope, ye who enter here."
+
+Updated: 2026-01-21
+
+Status: text + DM attachments are supported; channel/group file sending requires `sharePointSiteId` + Graph permissions (see [Sending files in group chats](#sending-files-in-group-chats)). Polls are sent via Adaptive Cards.
+
+## Plugin required
+
+Microsoft Teams ships as a plugin and is not bundled with the core install.
+
+**Breaking change (2026.1.15):** MS Teams moved out of core. If you use it, you must install the plugin.
+
+Explainable: keeps core installs lighter and lets MS Teams dependencies update independently.
+
+Install via CLI (npm registry):
+
+```bash
+openclaw plugins install @openclaw/msteams
+```
+
+Local checkout (when running from a git repo):
+
+```bash
+openclaw plugins install ./extensions/msteams
+```
+
+If you choose Teams during configure/onboarding and a git checkout is detected,
+OpenClaw will offer the local install path automatically.
+
+Details: [Plugins](/plugin)
+
+## Quick setup (beginner)
+
+1. Install the Microsoft Teams plugin.
+2. Create an **Azure Bot** (App ID + client secret + tenant ID).
+3. Configure OpenClaw with those credentials.
+4. Expose `/api/messages` (port 3978 by default) via a public URL or tunnel.
+5. Install the Teams app package and start the gateway.
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ msteams: {
+ enabled: true,
+ appId: "",
+ appPassword: "",
+ tenantId: "",
+ webhook: { port: 3978, path: "/api/messages" },
+ },
+ },
+}
+```
+
+Note: group chats are blocked by default (`channels.msteams.groupPolicy: "allowlist"`). To allow group replies, set `channels.msteams.groupAllowFrom` (or use `groupPolicy: "open"` to allow any member, mention-gated).
+
+## Goals
+
+- Talk to OpenClaw via Teams DMs, group chats, or channels.
+- Keep routing deterministic: replies always go back to the channel they arrived on.
+- Default to safe channel behavior (mentions required unless configured otherwise).
+
+## Config writes
+
+By default, Microsoft Teams is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
+
+Disable with:
+
+```json5
+{
+ channels: { msteams: { configWrites: false } },
+}
+```
+
+## Access control (DMs + groups)
+
+**DM access**
+
+- Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved.
+- `channels.msteams.allowFrom` accepts AAD object IDs, UPNs, or display names. The wizard resolves names to IDs via Microsoft Graph when credentials allow.
+
+**Group access**
+
+- Default: `channels.msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`). Use `channels.defaults.groupPolicy` to override the default when unset.
+- `channels.msteams.groupAllowFrom` controls which senders can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`).
+- Set `groupPolicy: "open"` to allow any member (still mention‑gated by default).
+- To allow **no channels**, set `channels.msteams.groupPolicy: "disabled"`.
+
+Example:
+
+```json5
+{
+ channels: {
+ msteams: {
+ groupPolicy: "allowlist",
+ groupAllowFrom: ["user@org.com"],
+ },
+ },
+}
+```
+
+**Teams + channel allowlist**
+
+- Scope group/channel replies by listing teams and channels under `channels.msteams.teams`.
+- Keys can be team IDs or names; channel keys can be conversation IDs or names.
+- When `groupPolicy="allowlist"` and a teams allowlist is present, only listed teams/channels are accepted (mention‑gated).
+- The configure wizard accepts `Team/Channel` entries and stores them for you.
+- On startup, OpenClaw resolves team/channel and user allowlist names to IDs (when Graph permissions allow)
+ and logs the mapping; unresolved entries are kept as typed.
+
+Example:
+
+```json5
+{
+ channels: {
+ msteams: {
+ groupPolicy: "allowlist",
+ teams: {
+ "My Team": {
+ channels: {
+ General: { requireMention: true },
+ },
+ },
+ },
+ },
+ },
+}
+```
+
+## How it works
+
+1. Install the Microsoft Teams plugin.
+2. Create an **Azure Bot** (App ID + secret + tenant ID).
+3. Build a **Teams app package** that references the bot and includes the RSC permissions below.
+4. Upload/install the Teams app into a team (or personal scope for DMs).
+5. Configure `msteams` in `~/.openclaw/openclaw.json` (or env vars) and start the gateway.
+6. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default.
+
+## Azure Bot Setup (Prerequisites)
+
+Before configuring OpenClaw, you need to create an Azure Bot resource.
+
+### Step 1: Create Azure Bot
+
+1. Go to [Create Azure Bot](https://portal.azure.com/#create/Microsoft.AzureBot)
+2. Fill in the **Basics** tab:
+
+ | Field | Value |
+ | ------------------ | -------------------------------------------------------- |
+ | **Bot handle** | Your bot name, e.g., `openclaw-msteams` (must be unique) |
+ | **Subscription** | Select your Azure subscription |
+ | **Resource group** | Create new or use existing |
+ | **Pricing tier** | **Free** for dev/testing |
+ | **Type of App** | **Single Tenant** (recommended - see note below) |
+ | **Creation type** | **Create new Microsoft App ID** |
+
+> **Deprecation notice:** Creation of new multi-tenant bots was deprecated after 2025-07-31. Use **Single Tenant** for new bots.
+
+3. Click **Review + create** → **Create** (wait ~1-2 minutes)
+
+### Step 2: Get Credentials
+
+1. Go to your Azure Bot resource → **Configuration**
+2. Copy **Microsoft App ID** → this is your `appId`
+3. Click **Manage Password** → go to the App Registration
+4. Under **Certificates & secrets** → **New client secret** → copy the **Value** → this is your `appPassword`
+5. Go to **Overview** → copy **Directory (tenant) ID** → this is your `tenantId`
+
+### Step 3: Configure Messaging Endpoint
+
+1. In Azure Bot → **Configuration**
+2. Set **Messaging endpoint** to your webhook URL:
+ - Production: `https://your-domain.com/api/messages`
+ - Local dev: Use a tunnel (see [Local Development](#local-development-tunneling) below)
+
+### Step 4: Enable Teams Channel
+
+1. In Azure Bot → **Channels**
+2. Click **Microsoft Teams** → Configure → Save
+3. Accept the Terms of Service
+
+## Local Development (Tunneling)
+
+Teams can't reach `localhost`. Use a tunnel for local development:
+
+**Option A: ngrok**
+
+```bash
+ngrok http 3978
+# Copy the https URL, e.g., https://abc123.ngrok.io
+# Set messaging endpoint to: https://abc123.ngrok.io/api/messages
+```
+
+**Option B: Tailscale Funnel**
+
+```bash
+tailscale funnel 3978
+# Use your Tailscale funnel URL as the messaging endpoint
+```
+
+## Teams Developer Portal (Alternative)
+
+Instead of manually creating a manifest ZIP, you can use the [Teams Developer Portal](https://dev.teams.microsoft.com/apps):
+
+1. Click **+ New app**
+2. Fill in basic info (name, description, developer info)
+3. Go to **App features** → **Bot**
+4. Select **Enter a bot ID manually** and paste your Azure Bot App ID
+5. Check scopes: **Personal**, **Team**, **Group Chat**
+6. Click **Distribute** → **Download app package**
+7. In Teams: **Apps** → **Manage your apps** → **Upload a custom app** → select the ZIP
+
+This is often easier than hand-editing JSON manifests.
+
+## Testing the Bot
+
+**Option A: Azure Web Chat (verify webhook first)**
+
+1. In Azure Portal → your Azure Bot resource → **Test in Web Chat**
+2. Send a message - you should see a response
+3. This confirms your webhook endpoint works before Teams setup
+
+**Option B: Teams (after app installation)**
+
+1. Install the Teams app (sideload or org catalog)
+2. Find the bot in Teams and send a DM
+3. Check gateway logs for incoming activity
+
+## Setup (minimal text-only)
+
+1. **Install the Microsoft Teams plugin**
+ - From npm: `openclaw plugins install @openclaw/msteams`
+ - From a local checkout: `openclaw plugins install ./extensions/msteams`
+
+2. **Bot registration**
+ - Create an Azure Bot (see above) and note:
+ - App ID
+ - Client secret (App password)
+ - Tenant ID (single-tenant)
+
+3. **Teams app manifest**
+ - Include a `bot` entry with `botId = `.
+ - Scopes: `personal`, `team`, `groupChat`.
+ - `supportsFiles: true` (required for personal scope file handling).
+ - Add RSC permissions (below).
+ - Create icons: `outline.png` (32x32) and `color.png` (192x192).
+ - Zip all three files together: `manifest.json`, `outline.png`, `color.png`.
+
+4. **Configure OpenClaw**
+
+ ```json
+ {
+ "msteams": {
+ "enabled": true,
+ "appId": "",
+ "appPassword": "",
+ "tenantId": "",
+ "webhook": { "port": 3978, "path": "/api/messages" }
+ }
+ }
+ ```
+
+ You can also use environment variables instead of config keys:
+ - `MSTEAMS_APP_ID`
+ - `MSTEAMS_APP_PASSWORD`
+ - `MSTEAMS_TENANT_ID`
+
+5. **Bot endpoint**
+ - Set the Azure Bot Messaging Endpoint to:
+ - `https://:3978/api/messages` (or your chosen path/port).
+
+6. **Run the gateway**
+ - The Teams channel starts automatically when the plugin is installed and `msteams` config exists with credentials.
+
+## History context
+
+- `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt.
+- Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
+- DM history can be limited with `channels.msteams.dmHistoryLimit` (user turns). Per-user overrides: `channels.msteams.dms[""].historyLimit`.
+
+## Current Teams RSC Permissions (Manifest)
+
+These are the **existing resourceSpecific permissions** in our Teams app manifest. They only apply inside the team/chat where the app is installed.
+
+**For channels (team scope):**
+
+- `ChannelMessage.Read.Group` (Application) - receive all channel messages without @mention
+- `ChannelMessage.Send.Group` (Application)
+- `Member.Read.Group` (Application)
+- `Owner.Read.Group` (Application)
+- `ChannelSettings.Read.Group` (Application)
+- `TeamMember.Read.Group` (Application)
+- `TeamSettings.Read.Group` (Application)
+
+**For group chats:**
+
+- `ChatMessage.Read.Chat` (Application) - receive all group chat messages without @mention
+
+## Example Teams Manifest (redacted)
+
+Minimal, valid example with the required fields. Replace IDs and URLs.
+
+```json
+{
+ "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
+ "manifestVersion": "1.23",
+ "version": "1.0.0",
+ "id": "00000000-0000-0000-0000-000000000000",
+ "name": { "short": "OpenClaw" },
+ "developer": {
+ "name": "Your Org",
+ "websiteUrl": "https://example.com",
+ "privacyUrl": "https://example.com/privacy",
+ "termsOfUseUrl": "https://example.com/terms"
+ },
+ "description": { "short": "OpenClaw in Teams", "full": "OpenClaw in Teams" },
+ "icons": { "outline": "outline.png", "color": "color.png" },
+ "accentColor": "#5B6DEF",
+ "bots": [
+ {
+ "botId": "11111111-1111-1111-1111-111111111111",
+ "scopes": ["personal", "team", "groupChat"],
+ "isNotificationOnly": false,
+ "supportsCalling": false,
+ "supportsVideo": false,
+ "supportsFiles": true
+ }
+ ],
+ "webApplicationInfo": {
+ "id": "11111111-1111-1111-1111-111111111111"
+ },
+ "authorization": {
+ "permissions": {
+ "resourceSpecific": [
+ { "name": "ChannelMessage.Read.Group", "type": "Application" },
+ { "name": "ChannelMessage.Send.Group", "type": "Application" },
+ { "name": "Member.Read.Group", "type": "Application" },
+ { "name": "Owner.Read.Group", "type": "Application" },
+ { "name": "ChannelSettings.Read.Group", "type": "Application" },
+ { "name": "TeamMember.Read.Group", "type": "Application" },
+ { "name": "TeamSettings.Read.Group", "type": "Application" },
+ { "name": "ChatMessage.Read.Chat", "type": "Application" }
+ ]
+ }
+ }
+}
+```
+
+### Manifest caveats (must-have fields)
+
+- `bots[].botId` **must** match the Azure Bot App ID.
+- `webApplicationInfo.id` **must** match the Azure Bot App ID.
+- `bots[].scopes` must include the surfaces you plan to use (`personal`, `team`, `groupChat`).
+- `bots[].supportsFiles: true` is required for file handling in personal scope.
+- `authorization.permissions.resourceSpecific` must include channel read/send if you want channel traffic.
+
+### Updating an existing app
+
+To update an already-installed Teams app (e.g., to add RSC permissions):
+
+1. Update your `manifest.json` with the new settings
+2. **Increment the `version` field** (e.g., `1.0.0` → `1.1.0`)
+3. **Re-zip** the manifest with icons (`manifest.json`, `outline.png`, `color.png`)
+4. Upload the new zip:
+ - **Option A (Teams Admin Center):** Teams Admin Center → Teams apps → Manage apps → find your app → Upload new version
+ - **Option B (Sideload):** In Teams → Apps → Manage your apps → Upload a custom app
+5. **For team channels:** Reinstall the app in each team for new permissions to take effect
+6. **Fully quit and relaunch Teams** (not just close the window) to clear cached app metadata
+
+## Capabilities: RSC only vs Graph
+
+### With **Teams RSC only** (app installed, no Graph API permissions)
+
+Works:
+
+- Read channel message **text** content.
+- Send channel message **text** content.
+- Receive **personal (DM)** file attachments.
+
+Does NOT work:
+
+- Channel/group **image or file contents** (payload only includes HTML stub).
+- Downloading attachments stored in SharePoint/OneDrive.
+- Reading message history (beyond the live webhook event).
+
+### With **Teams RSC + Microsoft Graph Application permissions**
+
+Adds:
+
+- Downloading hosted contents (images pasted into messages).
+- Downloading file attachments stored in SharePoint/OneDrive.
+- Reading channel/chat message history via Graph.
+
+### RSC vs Graph API
+
+| Capability | RSC Permissions | Graph API |
+| ----------------------- | -------------------- | ----------------------------------- |
+| **Real-time messages** | Yes (via webhook) | No (polling only) |
+| **Historical messages** | No | Yes (can query history) |
+| **Setup complexity** | App manifest only | Requires admin consent + token flow |
+| **Works offline** | No (must be running) | Yes (query anytime) |
+
+**Bottom line:** RSC is for real-time listening; Graph API is for historical access. For catching up on missed messages while offline, you need Graph API with `ChannelMessage.Read.All` (requires admin consent).
+
+## Graph-enabled media + history (required for channels)
+
+If you need images/files in **channels** or want to fetch **message history**, you must enable Microsoft Graph permissions and grant admin consent.
+
+1. In Entra ID (Azure AD) **App Registration**, add Microsoft Graph **Application permissions**:
+ - `ChannelMessage.Read.All` (channel attachments + history)
+ - `Chat.Read.All` or `ChatMessage.Read.All` (group chats)
+2. **Grant admin consent** for the tenant.
+3. Bump the Teams app **manifest version**, re-upload, and **reinstall the app in Teams**.
+4. **Fully quit and relaunch Teams** to clear cached app metadata.
+
+## Known Limitations
+
+### Webhook timeouts
+
+Teams delivers messages via HTTP webhook. If processing takes too long (e.g., slow LLM responses), you may see:
+
+- Gateway timeouts
+- Teams retrying the message (causing duplicates)
+- Dropped replies
+
+OpenClaw handles this by returning quickly and sending replies proactively, but very slow responses may still cause issues.
+
+### Formatting
+
+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)
+
+## Configuration
+
+Key settings (see `/gateway/configuration` for shared channel patterns):
+
+- `channels.msteams.enabled`: enable/disable the channel.
+- `channels.msteams.appId`, `channels.msteams.appPassword`, `channels.msteams.tenantId`: bot credentials.
+- `channels.msteams.webhook.port` (default `3978`)
+- `channels.msteams.webhook.path` (default `/api/messages`)
+- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
+- `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs, UPNs, or display names). The wizard resolves names to IDs during setup when Graph access is available.
+- `channels.msteams.textChunkLimit`: outbound text chunk size.
+- `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
+- `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).
+- `channels.msteams.requireMention`: require @mention in channels/groups (default true).
+- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).
+- `channels.msteams.teams..replyStyle`: per-team override.
+- `channels.msteams.teams..requireMention`: per-team override.
+- `channels.msteams.teams..tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing.
+- `channels.msteams.teams..toolsBySender`: default per-team per-sender tool policy overrides (`"*"` wildcard supported).
+- `channels.msteams.teams..channels..replyStyle`: per-channel override.
+- `channels.msteams.teams..channels..requireMention`: per-channel override.
+- `channels.msteams.teams..channels..tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
+- `channels.msteams.teams..channels..toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported).
+- `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)).
+
+## Routing & Sessions
+
+- Session keys follow the standard agent format (see [/concepts/session](/concepts/session)):
+ - Direct messages share the main session (`agent::`).
+ - Channel/group messages use conversation id:
+ - `agent::msteams:channel:`
+ - `agent::msteams:group:`
+
+## Reply Style: Threads vs Posts
+
+Teams recently introduced two channel UI styles over the same underlying data model:
+
+| Style | Description | Recommended `replyStyle` |
+| ------------------------ | --------------------------------------------------------- | ------------------------ |
+| **Posts** (classic) | Messages appear as cards with threaded replies underneath | `thread` (default) |
+| **Threads** (Slack-like) | Messages flow linearly, more like Slack | `top-level` |
+
+**The problem:** The Teams API does not expose which UI style a channel uses. If you use the wrong `replyStyle`:
+
+- `thread` in a Threads-style channel → replies appear nested awkwardly
+- `top-level` in a Posts-style channel → replies appear as separate top-level posts instead of in-thread
+
+**Solution:** Configure `replyStyle` per-channel based on how the channel is set up:
+
+```json
+{
+ "msteams": {
+ "replyStyle": "thread",
+ "teams": {
+ "19:abc...@thread.tacv2": {
+ "channels": {
+ "19:xyz...@thread.tacv2": {
+ "replyStyle": "top-level"
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+## Attachments & Images
+
+**Current limitations:**
+
+- **DMs:** Images and file attachments work via Teams bot file APIs.
+- **Channels/groups:** Attachments live in M365 storage (SharePoint/OneDrive). The webhook payload only includes an HTML stub, not the actual file bytes. **Graph API permissions are required** to download channel attachments.
+
+Without Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot).
+By default, OpenClaw only downloads media from Microsoft/Teams hostnames. Override with `channels.msteams.mediaAllowHosts` (use `["*"]` to allow any host).
+
+## Sending files in group chats
+
+Bots can send files in DMs using the FileConsentCard flow (built-in). However, **sending files in group chats/channels** requires additional setup:
+
+| Context | How files are sent | Setup needed |
+| ------------------------ | -------------------------------------------- | ----------------------------------------------- |
+| **DMs** | FileConsentCard → user accepts → bot uploads | Works out of the box |
+| **Group chats/channels** | Upload to SharePoint → share link | Requires `sharePointSiteId` + Graph permissions |
+| **Images (any context)** | Base64-encoded inline | Works out of the box |
+
+### Why group chats need SharePoint
+
+Bots don't have a personal OneDrive drive (the `/me/drive` Graph API endpoint doesn't work for application identities). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link.
+
+### Setup
+
+1. **Add Graph API permissions** in Entra ID (Azure AD) → App Registration:
+ - `Sites.ReadWrite.All` (Application) - upload files to SharePoint
+ - `Chat.Read.All` (Application) - optional, enables per-user sharing links
+
+2. **Grant admin consent** for the tenant.
+
+3. **Get your SharePoint site ID:**
+
+ ```bash
+ # Via Graph Explorer or curl with a valid token:
+ curl -H "Authorization: Bearer $TOKEN" \
+ "https://graph.microsoft.com/v1.0/sites/{hostname}:/{site-path}"
+
+ # Example: for a site at "contoso.sharepoint.com/sites/BotFiles"
+ curl -H "Authorization: Bearer $TOKEN" \
+ "https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/BotFiles"
+
+ # Response includes: "id": "contoso.sharepoint.com,guid1,guid2"
+ ```
+
+4. **Configure OpenClaw:**
+ ```json5
+ {
+ channels: {
+ msteams: {
+ // ... other config ...
+ sharePointSiteId: "contoso.sharepoint.com,guid1,guid2",
+ },
+ },
+ }
+ ```
+
+### Sharing behavior
+
+| Permission | Sharing behavior |
+| --------------------------------------- | --------------------------------------------------------- |
+| `Sites.ReadWrite.All` only | Organization-wide sharing link (anyone in org can access) |
+| `Sites.ReadWrite.All` + `Chat.Read.All` | Per-user sharing link (only chat members can access) |
+
+Per-user sharing is more secure as only the chat participants can access the file. If `Chat.Read.All` permission is missing, the bot falls back to organization-wide sharing.
+
+### Fallback behavior
+
+| Scenario | Result |
+| ------------------------------------------------- | -------------------------------------------------- |
+| Group chat + file + `sharePointSiteId` configured | Upload to SharePoint, send sharing link |
+| Group chat + file + no `sharePointSiteId` | Attempt OneDrive upload (may fail), send text only |
+| Personal chat + file | FileConsentCard flow (works without SharePoint) |
+| Any context + image | Base64-encoded inline (works without SharePoint) |
+
+### Files stored location
+
+Uploaded files are stored in a `/OpenClawShared/` folder in the configured SharePoint site's default document library.
+
+## Polls (Adaptive Cards)
+
+OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API).
+
+- CLI: `openclaw message poll --channel msteams --target conversation: ...`
+- Votes are recorded by the gateway in `~/.openclaw/msteams-polls.json`.
+- 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)
+
+Send any Adaptive Card JSON to Teams users or conversations using the `message` tool or CLI.
+
+The `card` parameter accepts an Adaptive Card JSON object. When `card` is provided, the message text is optional.
+
+**Agent tool:**
+
+```json
+{
+ "action": "send",
+ "channel": "msteams",
+ "target": "user:",
+ "card": {
+ "type": "AdaptiveCard",
+ "version": "1.5",
+ "body": [{ "type": "TextBlock", "text": "Hello!" }]
+ }
+}
+```
+
+**CLI:**
+
+```bash
+openclaw message send --channel msteams \
+ --target "conversation:19:abc...@thread.tacv2" \
+ --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}'
+```
+
+See [Adaptive Cards documentation](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below.
+
+## Target formats
+
+MSTeams targets use prefixes to distinguish between users and conversations:
+
+| Target type | Format | Example |
+| ------------------- | -------------------------------- | --------------------------------------------------- |
+| User (by ID) | `user:` | `user:40a1a0ed-4ff2-4164-a219-55518990c197` |
+| User (by name) | `user:` | `user:John Smith` (requires Graph API) |
+| Group/channel | `conversation:` | `conversation:19:abc123...@thread.tacv2` |
+| Group/channel (raw) | `` | `19:abc123...@thread.tacv2` (if contains `@thread`) |
+
+**CLI examples:**
+
+```bash
+# Send to a user by ID
+openclaw message send --channel msteams --target "user:40a1a0ed-..." --message "Hello"
+
+# Send to a user by display name (triggers Graph API lookup)
+openclaw message send --channel msteams --target "user:John Smith" --message "Hello"
+
+# 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
+openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \
+ --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}'
+```
+
+**Agent tool examples:**
+
+```json
+{
+ "action": "send",
+ "channel": "msteams",
+ "target": "user:John Smith",
+ "message": "Hello!"
+}
+```
+
+```json
+{
+ "action": "send",
+ "channel": "msteams",
+ "target": "conversation:19:abc...@thread.tacv2",
+ "card": {
+ "type": "AdaptiveCard",
+ "version": "1.5",
+ "body": [{ "type": "TextBlock", "text": "Hello" }]
+ }
+}
+```
+
+Note: Without the `user:` prefix, names default to group/team resolution. Always use `user:` when targeting people by display name.
+
+## Proactive messaging
+
+- Proactive messages are only possible **after** a user has interacted, because we store conversation references at that point.
+- See `/gateway/configuration` for `dmPolicy` and allowlist gating.
+
+## Team and Channel IDs (Common Gotcha)
+
+The `groupId` query parameter in Teams URLs is **NOT** the team ID used for configuration. Extract IDs from the URL path instead:
+
+**Team URL:**
+
+```
+https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations?groupId=...
+ └────────────────────────────┘
+ Team ID (URL-decode this)
+```
+
+**Channel URL:**
+
+```
+https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?groupId=...
+ └─────────────────────────┘
+ Channel ID (URL-decode this)
+```
+
+**For config:**
+
+- Team ID = path segment after `/team/` (URL-decoded, e.g., `19:Bk4j...@thread.tacv2`)
+- Channel ID = path segment after `/channel/` (URL-decoded)
+- **Ignore** the `groupId` query parameter
+
+## Private Channels
+
+Bots have limited support in private channels:
+
+| Feature | Standard Channels | Private Channels |
+| ---------------------------- | ----------------- | ---------------------- |
+| Bot installation | Yes | Limited |
+| Real-time messages (webhook) | Yes | May not work |
+| RSC permissions | Yes | May behave differently |
+| @mentions | Yes | If bot is accessible |
+| Graph API history | Yes | Yes (with permissions) |
+
+**Workarounds if private channels don't work:**
+
+1. Use standard channels for bot interactions
+2. Use DMs - users can always message the bot directly
+3. Use Graph API for historical access (requires `ChannelMessage.Read.All`)
+
+## Troubleshooting
+
+### Common issues
+
+- **Images not showing in channels:** Graph permissions or admin consent missing. Reinstall the Teams app and fully quit/reopen Teams.
+- **No responses in channel:** mentions are required by default; set `channels.msteams.requireMention=false` or configure per team/channel.
+- **Version mismatch (Teams still shows old manifest):** remove + re-add the app and fully quit Teams to refresh.
+- **401 Unauthorized from webhook:** Expected when testing manually without Azure JWT - means endpoint is reachable but auth failed. Use Azure Web Chat to test properly.
+
+### Manifest upload errors
+
+- **"Icon file cannot be empty":** The manifest references icon files that are 0 bytes. Create valid PNG icons (32x32 for `outline.png`, 192x192 for `color.png`).
+- **"webApplicationInfo.Id already in use":** The app is still installed in another team/chat. Find and uninstall it first, or wait 5-10 minutes for propagation.
+- **"Something went wrong" on upload:** Upload via https://admin.teams.microsoft.com instead, open browser DevTools (F12) → Network tab, and check the response body for the actual error.
+- **Sideload failing:** Try "Upload an app to your org's app catalog" instead of "Upload a custom app" - this often bypasses sideload restrictions.
+
+### RSC permissions not working
+
+1. Verify `webApplicationInfo.id` matches your bot's App ID exactly
+2. Re-upload the app and reinstall in the team/chat
+3. Check if your org admin has blocked RSC permissions
+4. Confirm you're using the right scope: `ChannelMessage.Read.Group` for teams, `ChatMessage.Read.Chat` for group chats
+
+## References
+
+- [Create Azure Bot](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration) - Azure Bot setup guide
+- [Teams Developer Portal](https://dev.teams.microsoft.com/apps) - create/manage Teams apps
+- [Teams app manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema)
+- [Receive channel messages with RSC](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/channel-messages-with-rsc)
+- [RSC permissions reference](https://learn.microsoft.com/en-us/microsoftteams/platform/graph-api/rsc/resource-specific-consent)
+- [Teams bot file handling](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4) (channel/group requires Graph)
+- [Proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages)
diff --git a/docs/channels/nextcloud-talk.md b/docs/channels/nextcloud-talk.md
new file mode 100644
index 0000000000000000000000000000000000000000..2c8698513b729a7fff5bf2d740d04ced228171d6
--- /dev/null
+++ b/docs/channels/nextcloud-talk.md
@@ -0,0 +1,135 @@
+---
+summary: "Nextcloud Talk support status, capabilities, and configuration"
+read_when:
+ - Working on Nextcloud Talk channel features
+title: "Nextcloud Talk"
+---
+
+# Nextcloud Talk (plugin)
+
+Status: supported via plugin (webhook bot). Direct messages, rooms, reactions, and markdown messages are supported.
+
+## Plugin required
+
+Nextcloud Talk ships as a plugin and is not bundled with the core install.
+
+Install via CLI (npm registry):
+
+```bash
+openclaw plugins install @openclaw/nextcloud-talk
+```
+
+Local checkout (when running from a git repo):
+
+```bash
+openclaw plugins install ./extensions/nextcloud-talk
+```
+
+If you choose Nextcloud Talk during configure/onboarding and a git checkout is detected,
+OpenClaw will offer the local install path automatically.
+
+Details: [Plugins](/plugin)
+
+## Quick setup (beginner)
+
+1. Install the Nextcloud Talk plugin.
+2. On your Nextcloud server, create a bot:
+ ```bash
+ ./occ talk:bot:install "OpenClaw" "" "" --feature reaction
+ ```
+3. Enable the bot in the target room settings.
+4. Configure OpenClaw:
+ - Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret`
+ - Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only)
+5. Restart the gateway (or finish onboarding).
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ "nextcloud-talk": {
+ enabled: true,
+ baseUrl: "https://cloud.example.com",
+ botSecret: "shared-secret",
+ dmPolicy: "pairing",
+ },
+ },
+}
+```
+
+## Notes
+
+- Bots cannot initiate DMs. The user must message the bot first.
+- Webhook URL must be reachable by the Gateway; set `webhookPublicUrl` if behind a proxy.
+- Media uploads are not supported by the bot API; media is sent as URLs.
+- The webhook payload does not distinguish DMs vs rooms; set `apiUser` + `apiPassword` to enable room-type lookups (otherwise DMs are treated as rooms).
+
+## Access control (DMs)
+
+- Default: `channels.nextcloud-talk.dmPolicy = "pairing"`. Unknown senders get a pairing code.
+- Approve via:
+ - `openclaw pairing list nextcloud-talk`
+ - `openclaw pairing approve nextcloud-talk `
+- Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`.
+
+## Rooms (groups)
+
+- Default: `channels.nextcloud-talk.groupPolicy = "allowlist"` (mention-gated).
+- Allowlist rooms with `channels.nextcloud-talk.rooms`:
+
+```json5
+{
+ channels: {
+ "nextcloud-talk": {
+ rooms: {
+ "room-token": { requireMention: true },
+ },
+ },
+ },
+}
+```
+
+- To allow no rooms, keep the allowlist empty or set `channels.nextcloud-talk.groupPolicy="disabled"`.
+
+## Capabilities
+
+| Feature | Status |
+| --------------- | ------------- |
+| Direct messages | Supported |
+| Rooms | Supported |
+| Threads | Not supported |
+| Media | URL-only |
+| Reactions | Supported |
+| Native commands | Not supported |
+
+## Configuration reference (Nextcloud Talk)
+
+Full configuration: [Configuration](/gateway/configuration)
+
+Provider options:
+
+- `channels.nextcloud-talk.enabled`: enable/disable channel startup.
+- `channels.nextcloud-talk.baseUrl`: Nextcloud instance URL.
+- `channels.nextcloud-talk.botSecret`: bot shared secret.
+- `channels.nextcloud-talk.botSecretFile`: secret file path.
+- `channels.nextcloud-talk.apiUser`: API user for room lookups (DM detection).
+- `channels.nextcloud-talk.apiPassword`: API/app password for room lookups.
+- `channels.nextcloud-talk.apiPasswordFile`: API password file path.
+- `channels.nextcloud-talk.webhookPort`: webhook listener port (default: 8788).
+- `channels.nextcloud-talk.webhookHost`: webhook host (default: 0.0.0.0).
+- `channels.nextcloud-talk.webhookPath`: webhook path (default: /nextcloud-talk-webhook).
+- `channels.nextcloud-talk.webhookPublicUrl`: externally reachable webhook URL.
+- `channels.nextcloud-talk.dmPolicy`: `pairing | allowlist | open | disabled`.
+- `channels.nextcloud-talk.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`.
+- `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled`.
+- `channels.nextcloud-talk.groupAllowFrom`: group allowlist (user IDs).
+- `channels.nextcloud-talk.rooms`: per-room settings and allowlist.
+- `channels.nextcloud-talk.historyLimit`: group history limit (0 disables).
+- `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables).
+- `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit).
+- `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size (chars).
+- `channels.nextcloud-talk.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
+- `channels.nextcloud-talk.blockStreaming`: disable block streaming for this channel.
+- `channels.nextcloud-talk.blockStreamingCoalesce`: block streaming coalesce tuning.
+- `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB).
diff --git a/docs/channels/nostr.md b/docs/channels/nostr.md
new file mode 100644
index 0000000000000000000000000000000000000000..3368933d6c4ce5475263e628b24c2024ded7eb10
--- /dev/null
+++ b/docs/channels/nostr.md
@@ -0,0 +1,233 @@
+---
+summary: "Nostr DM channel via NIP-04 encrypted messages"
+read_when:
+ - You want OpenClaw to receive DMs via Nostr
+ - You're setting up decentralized messaging
+title: "Nostr"
+---
+
+# Nostr
+
+**Status:** Optional plugin (disabled by default).
+
+Nostr is a decentralized protocol for social networking. This channel enables OpenClaw to receive and respond to encrypted direct messages (DMs) via NIP-04.
+
+## Install (on demand)
+
+### Onboarding (recommended)
+
+- The onboarding wizard (`openclaw onboard`) and `openclaw channels add` list optional channel plugins.
+- Selecting Nostr prompts you to install the plugin on demand.
+
+Install defaults:
+
+- **Dev channel + git checkout available:** uses the local plugin path.
+- **Stable/Beta:** downloads from npm.
+
+You can always override the choice in the prompt.
+
+### Manual install
+
+```bash
+openclaw plugins install @openclaw/nostr
+```
+
+Use a local checkout (dev workflows):
+
+```bash
+openclaw plugins install --link /extensions/nostr
+```
+
+Restart the Gateway after installing or enabling plugins.
+
+## Quick setup
+
+1. Generate a Nostr keypair (if needed):
+
+```bash
+# Using nak
+nak key generate
+```
+
+2. Add to config:
+
+```json
+{
+ "channels": {
+ "nostr": {
+ "privateKey": "${NOSTR_PRIVATE_KEY}"
+ }
+ }
+}
+```
+
+3. Export the key:
+
+```bash
+export NOSTR_PRIVATE_KEY="nsec1..."
+```
+
+4. Restart the Gateway.
+
+## Configuration reference
+
+| Key | Type | Default | Description |
+| ------------ | -------- | ------------------------------------------- | ----------------------------------- |
+| `privateKey` | string | required | Private key in `nsec` or hex format |
+| `relays` | string[] | `['wss://relay.damus.io', 'wss://nos.lol']` | Relay URLs (WebSocket) |
+| `dmPolicy` | string | `pairing` | DM access policy |
+| `allowFrom` | string[] | `[]` | Allowed sender pubkeys |
+| `enabled` | boolean | `true` | Enable/disable channel |
+| `name` | string | - | Display name |
+| `profile` | object | - | NIP-01 profile metadata |
+
+## Profile metadata
+
+Profile data is published as a NIP-01 `kind:0` event. You can manage it from the Control UI (Channels -> Nostr -> Profile) or set it directly in config.
+
+Example:
+
+```json
+{
+ "channels": {
+ "nostr": {
+ "privateKey": "${NOSTR_PRIVATE_KEY}",
+ "profile": {
+ "name": "openclaw",
+ "displayName": "OpenClaw",
+ "about": "Personal assistant DM bot",
+ "picture": "https://example.com/avatar.png",
+ "banner": "https://example.com/banner.png",
+ "website": "https://example.com",
+ "nip05": "openclaw@example.com",
+ "lud16": "openclaw@example.com"
+ }
+ }
+ }
+}
+```
+
+Notes:
+
+- Profile URLs must use `https://`.
+- Importing from relays merges fields and preserves local overrides.
+
+## Access control
+
+### DM policies
+
+- **pairing** (default): unknown senders get a pairing code.
+- **allowlist**: only pubkeys in `allowFrom` can DM.
+- **open**: public inbound DMs (requires `allowFrom: ["*"]`).
+- **disabled**: ignore inbound DMs.
+
+### Allowlist example
+
+```json
+{
+ "channels": {
+ "nostr": {
+ "privateKey": "${NOSTR_PRIVATE_KEY}",
+ "dmPolicy": "allowlist",
+ "allowFrom": ["npub1abc...", "npub1xyz..."]
+ }
+ }
+}
+```
+
+## Key formats
+
+Accepted formats:
+
+- **Private key:** `nsec...` or 64-char hex
+- **Pubkeys (`allowFrom`):** `npub...` or hex
+
+## Relays
+
+Defaults: `relay.damus.io` and `nos.lol`.
+
+```json
+{
+ "channels": {
+ "nostr": {
+ "privateKey": "${NOSTR_PRIVATE_KEY}",
+ "relays": ["wss://relay.damus.io", "wss://relay.primal.net", "wss://nostr.wine"]
+ }
+ }
+}
+```
+
+Tips:
+
+- Use 2-3 relays for redundancy.
+- Avoid too many relays (latency, duplication).
+- Paid relays can improve reliability.
+- Local relays are fine for testing (`ws://localhost:7777`).
+
+## Protocol support
+
+| NIP | Status | Description |
+| ------ | --------- | ------------------------------------- |
+| NIP-01 | Supported | Basic event format + profile metadata |
+| NIP-04 | Supported | Encrypted DMs (`kind:4`) |
+| NIP-17 | Planned | Gift-wrapped DMs |
+| NIP-44 | Planned | Versioned encryption |
+
+## Testing
+
+### Local relay
+
+```bash
+# Start strfry
+docker run -p 7777:7777 ghcr.io/hoytech/strfry
+```
+
+```json
+{
+ "channels": {
+ "nostr": {
+ "privateKey": "${NOSTR_PRIVATE_KEY}",
+ "relays": ["ws://localhost:7777"]
+ }
+ }
+}
+```
+
+### Manual test
+
+1. Note the bot pubkey (npub) from logs.
+2. Open a Nostr client (Damus, Amethyst, etc.).
+3. DM the bot pubkey.
+4. Verify the response.
+
+## Troubleshooting
+
+### Not receiving messages
+
+- Verify the private key is valid.
+- Ensure relay URLs are reachable and use `wss://` (or `ws://` for local).
+- Confirm `enabled` is not `false`.
+- Check Gateway logs for relay connection errors.
+
+### Not sending responses
+
+- Check relay accepts writes.
+- Verify outbound connectivity.
+- Watch for relay rate limits.
+
+### Duplicate responses
+
+- Expected when using multiple relays.
+- Messages are deduplicated by event ID; only the first delivery triggers a response.
+
+## Security
+
+- Never commit private keys.
+- Use environment variables for keys.
+- Consider `allowlist` for production bots.
+
+## Limitations (MVP)
+
+- Direct messages only (no group chats).
+- No media attachments.
+- NIP-04 only (NIP-17 gift-wrap planned).
diff --git a/docs/channels/signal.md b/docs/channels/signal.md
new file mode 100644
index 0000000000000000000000000000000000000000..fc211f1538a974321240bd420d371d1ea6b13b6a
--- /dev/null
+++ b/docs/channels/signal.md
@@ -0,0 +1,202 @@
+---
+summary: "Signal support via signal-cli (JSON-RPC + SSE), setup, and number model"
+read_when:
+ - Setting up Signal support
+ - Debugging Signal send/receive
+title: "Signal"
+---
+
+# Signal (signal-cli)
+
+Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE.
+
+## Quick setup (beginner)
+
+1. Use a **separate Signal number** for the bot (recommended).
+2. Install `signal-cli` (Java required).
+3. Link the bot device and start the daemon:
+ - `signal-cli link -n "OpenClaw"`
+4. Configure OpenClaw and start the gateway.
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ signal: {
+ enabled: true,
+ account: "+15551234567",
+ cliPath: "signal-cli",
+ dmPolicy: "pairing",
+ allowFrom: ["+15557654321"],
+ },
+ },
+}
+```
+
+## What it is
+
+- Signal channel via `signal-cli` (not embedded libsignal).
+- Deterministic routing: replies always go back to Signal.
+- DMs share the agent's main session; groups are isolated (`agent::signal:group:`).
+
+## Config writes
+
+By default, Signal is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
+
+Disable with:
+
+```json5
+{
+ channels: { signal: { configWrites: false } },
+}
+```
+
+## The number model (important)
+
+- The gateway connects to a **Signal device** (the `signal-cli` account).
+- If you run the bot on **your personal Signal account**, it will ignore your own messages (loop protection).
+- For "I text the bot and it replies," use a **separate bot number**.
+
+## Setup (fast path)
+
+1. Install `signal-cli` (Java required).
+2. Link a bot account:
+ - `signal-cli link -n "OpenClaw"` then scan the QR in Signal.
+3. Configure Signal and start the gateway.
+
+Example:
+
+```json5
+{
+ channels: {
+ signal: {
+ enabled: true,
+ account: "+15551234567",
+ cliPath: "signal-cli",
+ dmPolicy: "pairing",
+ allowFrom: ["+15557654321"],
+ },
+ },
+}
+```
+
+Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
+
+## External daemon mode (httpUrl)
+
+If you want to manage `signal-cli` yourself (slow JVM cold starts, container init, or shared CPUs), run the daemon separately and point OpenClaw at it:
+
+```json5
+{
+ channels: {
+ signal: {
+ httpUrl: "http://127.0.0.1:8080",
+ autoStart: false,
+ },
+ },
+}
+```
+
+This skips auto-spawn and the startup wait inside OpenClaw. For slow starts when auto-spawning, set `channels.signal.startupTimeoutMs`.
+
+## Access control (DMs + groups)
+
+DMs:
+
+- Default: `channels.signal.dmPolicy = "pairing"`.
+- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
+- Approve via:
+ - `openclaw pairing list signal`
+ - `openclaw pairing approve signal `
+- Pairing is the default token exchange for Signal DMs. Details: [Pairing](/start/pairing)
+- UUID-only senders (from `sourceUuid`) are stored as `uuid:` in `channels.signal.allowFrom`.
+
+Groups:
+
+- `channels.signal.groupPolicy = open | allowlist | disabled`.
+- `channels.signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
+
+## How it works (behavior)
+
+- `signal-cli` runs as a daemon; the gateway reads events via SSE.
+- Inbound messages are normalized into the shared channel envelope.
+- Replies always route back to the same number or group.
+
+## Media + limits
+
+- Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000).
+- Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
+- Attachments supported (base64 fetched from `signal-cli`).
+- Default media cap: `channels.signal.mediaMaxMb` (default 8).
+- Use `channels.signal.ignoreAttachments` to skip downloading media.
+- Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
+
+## Typing + read receipts
+
+- **Typing indicators**: OpenClaw sends typing signals via `signal-cli sendTyping` and refreshes them while a reply is running.
+- **Read receipts**: when `channels.signal.sendReadReceipts` is true, OpenClaw forwards read receipts for allowed DMs.
+- Signal-cli does not expose read receipts for groups.
+
+## Reactions (message tool)
+
+- Use `message action=react` with `channel=signal`.
+- Targets: sender E.164 or UUID (use `uuid:` from pairing output; bare UUID works too).
+- `messageId` is the Signal timestamp for the message you’re reacting to.
+- Group reactions require `targetAuthor` or `targetAuthorUuid`.
+
+Examples:
+
+```
+message action=react channel=signal target=uuid:123e4567-e89b-12d3-a456-426614174000 messageId=1737630212345 emoji=🔥
+message action=react channel=signal target=+15551234567 messageId=1737630212345 emoji=🔥 remove=true
+message action=react channel=signal target=signal:group: targetAuthor=uuid: messageId=1737630212345 emoji=✅
+```
+
+Config:
+
+- `channels.signal.actions.reactions`: enable/disable reaction actions (default true).
+- `channels.signal.reactionLevel`: `off | ack | minimal | extensive`.
+ - `off`/`ack` disables agent reactions (message tool `react` will error).
+ - `minimal`/`extensive` enables agent reactions and sets the guidance level.
+- Per-account overrides: `channels.signal.accounts..actions.reactions`, `channels.signal.accounts..reactionLevel`.
+
+## Delivery targets (CLI/cron)
+
+- DMs: `signal:+15551234567` (or plain E.164).
+- UUID DMs: `uuid:` (or bare UUID).
+- Groups: `signal:group:`.
+- Usernames: `username:` (if supported by your Signal account).
+
+## Configuration reference (Signal)
+
+Full configuration: [Configuration](/gateway/configuration)
+
+Provider options:
+
+- `channels.signal.enabled`: enable/disable channel startup.
+- `channels.signal.account`: E.164 for the bot account.
+- `channels.signal.cliPath`: path to `signal-cli`.
+- `channels.signal.httpUrl`: full daemon URL (overrides host/port).
+- `channels.signal.httpHost`, `channels.signal.httpPort`: daemon bind (default 127.0.0.1:8080).
+- `channels.signal.autoStart`: auto-spawn daemon (default true if `httpUrl` unset).
+- `channels.signal.startupTimeoutMs`: startup wait timeout in ms (cap 120000).
+- `channels.signal.receiveMode`: `on-start | manual`.
+- `channels.signal.ignoreAttachments`: skip attachment downloads.
+- `channels.signal.ignoreStories`: ignore stories from the daemon.
+- `channels.signal.sendReadReceipts`: forward read receipts.
+- `channels.signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
+- `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:`). `open` requires `"*"`. Signal has no usernames; use phone/UUID ids.
+- `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
+- `channels.signal.groupAllowFrom`: group sender allowlist.
+- `channels.signal.historyLimit`: max group messages to include as context (0 disables).
+- `channels.signal.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.signal.dms[""].historyLimit`.
+- `channels.signal.textChunkLimit`: outbound chunk size (chars).
+- `channels.signal.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
+- `channels.signal.mediaMaxMb`: inbound/outbound media cap (MB).
+
+Related global options:
+
+- `agents.list[].groupChat.mentionPatterns` (Signal does not support native mentions).
+- `messages.groupChat.mentionPatterns` (global fallback).
+- `messages.responsePrefix`.
diff --git a/docs/channels/slack.md b/docs/channels/slack.md
new file mode 100644
index 0000000000000000000000000000000000000000..a9dbc2466720424784f9c1fda8b530b099eafb32
--- /dev/null
+++ b/docs/channels/slack.md
@@ -0,0 +1,548 @@
+---
+summary: "Slack setup for socket or HTTP webhook mode"
+read_when: "Setting up Slack or debugging Slack socket/HTTP mode"
+title: "Slack"
+---
+
+# Slack
+
+## Socket mode (default)
+
+### Quick setup (beginner)
+
+1. Create a Slack app and enable **Socket Mode**.
+2. Create an **App Token** (`xapp-...`) and **Bot Token** (`xoxb-...`).
+3. Set tokens for OpenClaw and start the gateway.
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ slack: {
+ enabled: true,
+ appToken: "xapp-...",
+ botToken: "xoxb-...",
+ },
+ },
+}
+```
+
+### Setup
+
+1. Create a Slack app (From scratch) in https://api.slack.com/apps.
+2. **Socket Mode** → toggle on. Then go to **Basic Information** → **App-Level Tokens** → **Generate Token and Scopes** with scope `connections:write`. Copy the **App Token** (`xapp-...`).
+3. **OAuth & Permissions** → add bot token scopes (use the manifest below). Click **Install to Workspace**. Copy the **Bot User OAuth Token** (`xoxb-...`).
+4. Optional: **OAuth & Permissions** → add **User Token Scopes** (see the read-only list below). Reinstall the app and copy the **User OAuth Token** (`xoxp-...`).
+5. **Event Subscriptions** → enable events and subscribe to:
+ - `message.*` (includes edits/deletes/thread broadcasts)
+ - `app_mention`
+ - `reaction_added`, `reaction_removed`
+ - `member_joined_channel`, `member_left_channel`
+ - `channel_rename`
+ - `pin_added`, `pin_removed`
+6. Invite the bot to channels you want it to read.
+7. Slash Commands → create `/openclaw` if you use `channels.slack.slashCommand`. If you enable native commands, add one slash command per built-in command (same names as `/help`). Native defaults to off for Slack unless you set `channels.slack.commands.native: true` (global `commands.native` is `"auto"` which leaves Slack off).
+8. App Home → enable the **Messages Tab** so users can DM the bot.
+
+Use the manifest below so scopes and events stay in sync.
+
+Multi-account support: use `channels.slack.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
+
+### OpenClaw config (minimal)
+
+Set tokens via env vars (recommended):
+
+- `SLACK_APP_TOKEN=xapp-...`
+- `SLACK_BOT_TOKEN=xoxb-...`
+
+Or via config:
+
+```json5
+{
+ channels: {
+ slack: {
+ enabled: true,
+ appToken: "xapp-...",
+ botToken: "xoxb-...",
+ },
+ },
+}
+```
+
+### User token (optional)
+
+OpenClaw can use a Slack user token (`xoxp-...`) for read operations (history,
+pins, reactions, emoji, member info). By default this stays read-only: reads
+prefer the user token when present, and writes still use the bot token unless
+you explicitly opt in. Even with `userTokenReadOnly: false`, the bot token stays
+preferred for writes when it is available.
+
+User tokens are configured in the config file (no env var support). For
+multi-account, set `channels.slack.accounts..userToken`.
+
+Example with bot + app + user tokens:
+
+```json5
+{
+ channels: {
+ slack: {
+ enabled: true,
+ appToken: "xapp-...",
+ botToken: "xoxb-...",
+ userToken: "xoxp-...",
+ },
+ },
+}
+```
+
+Example with userTokenReadOnly explicitly set (allow user token writes):
+
+```json5
+{
+ channels: {
+ slack: {
+ enabled: true,
+ appToken: "xapp-...",
+ botToken: "xoxb-...",
+ userToken: "xoxp-...",
+ userTokenReadOnly: false,
+ },
+ },
+}
+```
+
+#### Token usage
+
+- Read operations (history, reactions list, pins list, emoji list, member info,
+ search) prefer the user token when configured, otherwise the bot token.
+- Write operations (send/edit/delete messages, add/remove reactions, pin/unpin,
+ file uploads) use the bot token by default. If `userTokenReadOnly: false` and
+ no bot token is available, OpenClaw falls back to the user token.
+
+### History context
+
+- `channels.slack.historyLimit` (or `channels.slack.accounts.*.historyLimit`) controls how many recent channel/group messages are wrapped into the prompt.
+- Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
+
+## HTTP mode (Events API)
+
+Use HTTP webhook mode when your Gateway is reachable by Slack over HTTPS (typical for server deployments).
+HTTP mode uses the Events API + Interactivity + Slash Commands with a shared request URL.
+
+### Setup
+
+1. Create a Slack app and **disable Socket Mode** (optional if you only use HTTP).
+2. **Basic Information** → copy the **Signing Secret**.
+3. **OAuth & Permissions** → install the app and copy the **Bot User OAuth Token** (`xoxb-...`).
+4. **Event Subscriptions** → enable events and set the **Request URL** to your gateway webhook path (default `/slack/events`).
+5. **Interactivity & Shortcuts** → enable and set the same **Request URL**.
+6. **Slash Commands** → set the same **Request URL** for your command(s).
+
+Example request URL:
+`https://gateway-host/slack/events`
+
+### OpenClaw config (minimal)
+
+```json5
+{
+ channels: {
+ slack: {
+ enabled: true,
+ mode: "http",
+ botToken: "xoxb-...",
+ signingSecret: "your-signing-secret",
+ webhookPath: "/slack/events",
+ },
+ },
+}
+```
+
+Multi-account HTTP mode: set `channels.slack.accounts..mode = "http"` and provide a unique
+`webhookPath` per account so each Slack app can point to its own URL.
+
+### Manifest (optional)
+
+Use this Slack app manifest to create the app quickly (adjust the name/command if you want). Include the
+user scopes if you plan to configure a user token.
+
+```json
+{
+ "display_information": {
+ "name": "OpenClaw",
+ "description": "Slack connector for OpenClaw"
+ },
+ "features": {
+ "bot_user": {
+ "display_name": "OpenClaw",
+ "always_online": false
+ },
+ "app_home": {
+ "messages_tab_enabled": true,
+ "messages_tab_read_only_enabled": false
+ },
+ "slash_commands": [
+ {
+ "command": "/openclaw",
+ "description": "Send a message to OpenClaw",
+ "should_escape": false
+ }
+ ]
+ },
+ "oauth_config": {
+ "scopes": {
+ "bot": [
+ "chat:write",
+ "channels:history",
+ "channels:read",
+ "groups:history",
+ "groups:read",
+ "groups:write",
+ "im:history",
+ "im:read",
+ "im:write",
+ "mpim:history",
+ "mpim:read",
+ "mpim:write",
+ "users:read",
+ "app_mentions:read",
+ "reactions:read",
+ "reactions:write",
+ "pins:read",
+ "pins:write",
+ "emoji:read",
+ "commands",
+ "files:read",
+ "files:write"
+ ],
+ "user": [
+ "channels:history",
+ "channels:read",
+ "groups:history",
+ "groups:read",
+ "im:history",
+ "im:read",
+ "mpim:history",
+ "mpim:read",
+ "users:read",
+ "reactions:read",
+ "pins:read",
+ "emoji:read",
+ "search:read"
+ ]
+ }
+ },
+ "settings": {
+ "socket_mode_enabled": true,
+ "event_subscriptions": {
+ "bot_events": [
+ "app_mention",
+ "message.channels",
+ "message.groups",
+ "message.im",
+ "message.mpim",
+ "reaction_added",
+ "reaction_removed",
+ "member_joined_channel",
+ "member_left_channel",
+ "channel_rename",
+ "pin_added",
+ "pin_removed"
+ ]
+ }
+ }
+}
+```
+
+If you enable native commands, add one `slash_commands` entry per command you want to expose (matching the `/help` list). Override with `channels.slack.commands.native`.
+
+## Scopes (current vs optional)
+
+Slack's Conversations API is type-scoped: you only need the scopes for the
+conversation types you actually touch (channels, groups, im, mpim). See
+https://docs.slack.dev/apis/web-api/using-the-conversations-api/ for the overview.
+
+### Bot token scopes (required)
+
+- `chat:write` (send/update/delete messages via `chat.postMessage`)
+ https://docs.slack.dev/reference/methods/chat.postMessage
+- `im:write` (open DMs via `conversations.open` for user DMs)
+ https://docs.slack.dev/reference/methods/conversations.open
+- `channels:history`, `groups:history`, `im:history`, `mpim:history`
+ https://docs.slack.dev/reference/methods/conversations.history
+- `channels:read`, `groups:read`, `im:read`, `mpim:read`
+ https://docs.slack.dev/reference/methods/conversations.info
+- `users:read` (user lookup)
+ https://docs.slack.dev/reference/methods/users.info
+- `reactions:read`, `reactions:write` (`reactions.get` / `reactions.add`)
+ https://docs.slack.dev/reference/methods/reactions.get
+ https://docs.slack.dev/reference/methods/reactions.add
+- `pins:read`, `pins:write` (`pins.list` / `pins.add` / `pins.remove`)
+ https://docs.slack.dev/reference/scopes/pins.read
+ https://docs.slack.dev/reference/scopes/pins.write
+- `emoji:read` (`emoji.list`)
+ https://docs.slack.dev/reference/scopes/emoji.read
+- `files:write` (uploads via `files.uploadV2`)
+ https://docs.slack.dev/messaging/working-with-files/#upload
+
+### User token scopes (optional, read-only by default)
+
+Add these under **User Token Scopes** if you configure `channels.slack.userToken`.
+
+- `channels:history`, `groups:history`, `im:history`, `mpim:history`
+- `channels:read`, `groups:read`, `im:read`, `mpim:read`
+- `users:read`
+- `reactions:read`
+- `pins:read`
+- `emoji:read`
+- `search:read`
+
+### Not needed today (but likely future)
+
+- `mpim:write` (only if we add group-DM open/DM start via `conversations.open`)
+- `groups:write` (only if we add private-channel management: create/rename/invite/archive)
+- `chat:write.public` (only if we want to post to channels the bot isn't in)
+ https://docs.slack.dev/reference/scopes/chat.write.public
+- `users:read.email` (only if we need email fields from `users.info`)
+ https://docs.slack.dev/changelog/2017-04-narrowing-email-access
+- `files:read` (only if we start listing/reading file metadata)
+
+## Config
+
+Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
+
+```json
+{
+ "slack": {
+ "enabled": true,
+ "botToken": "xoxb-...",
+ "appToken": "xapp-...",
+ "groupPolicy": "allowlist",
+ "dm": {
+ "enabled": true,
+ "policy": "pairing",
+ "allowFrom": ["U123", "U456", "*"],
+ "groupEnabled": false,
+ "groupChannels": ["G123"],
+ "replyToMode": "all"
+ },
+ "channels": {
+ "C123": { "allow": true, "requireMention": true },
+ "#general": {
+ "allow": true,
+ "requireMention": true,
+ "users": ["U123"],
+ "skills": ["search", "docs"],
+ "systemPrompt": "Keep answers short."
+ }
+ },
+ "reactionNotifications": "own",
+ "reactionAllowlist": ["U123"],
+ "replyToMode": "off",
+ "actions": {
+ "reactions": true,
+ "messages": true,
+ "pins": true,
+ "memberInfo": true,
+ "emojiList": true
+ },
+ "slashCommand": {
+ "enabled": true,
+ "name": "openclaw",
+ "sessionPrefix": "slack:slash",
+ "ephemeral": true
+ },
+ "textChunkLimit": 4000,
+ "mediaMaxMb": 20
+ }
+}
+```
+
+Tokens can also be supplied via env vars:
+
+- `SLACK_BOT_TOKEN`
+- `SLACK_APP_TOKEN`
+
+Ack reactions are controlled globally via `messages.ackReaction` +
+`messages.ackReactionScope`. Use `messages.removeAckAfterReply` to clear the
+ack reaction after the bot replies.
+
+## Limits
+
+- Outbound text is chunked to `channels.slack.textChunkLimit` (default 4000).
+- Optional newline chunking: set `channels.slack.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
+- Media uploads are capped by `channels.slack.mediaMaxMb` (default 20).
+
+## Reply threading
+
+By default, OpenClaw replies in the main channel. Use `channels.slack.replyToMode` to control automatic threading:
+
+| Mode | Behavior |
+| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `off` | **Default.** Reply in main channel. Only thread if the triggering message was already in a thread. |
+| `first` | First reply goes to thread (under the triggering message), subsequent replies go to main channel. Useful for keeping context visible while avoiding thread clutter. |
+| `all` | All replies go to thread. Keeps conversations contained but may reduce visibility. |
+
+The mode applies to both auto-replies and agent tool calls (`slack sendMessage`).
+
+### Per-chat-type threading
+
+You can configure different threading behavior per chat type by setting `channels.slack.replyToModeByChatType`:
+
+```json5
+{
+ channels: {
+ slack: {
+ replyToMode: "off", // default for channels
+ replyToModeByChatType: {
+ direct: "all", // DMs always thread
+ group: "first", // group DMs/MPIM thread first reply
+ },
+ },
+ },
+}
+```
+
+Supported chat types:
+
+- `direct`: 1:1 DMs (Slack `im`)
+- `group`: group DMs / MPIMs (Slack `mpim`)
+- `channel`: standard channels (public/private)
+
+Precedence:
+
+1. `replyToModeByChatType.`
+2. `replyToMode`
+3. Provider default (`off`)
+
+Legacy `channels.slack.dm.replyToMode` is still accepted as a fallback for `direct` when no chat-type override is set.
+
+Examples:
+
+Thread DMs only:
+
+```json5
+{
+ channels: {
+ slack: {
+ replyToMode: "off",
+ replyToModeByChatType: { direct: "all" },
+ },
+ },
+}
+```
+
+Thread group DMs but keep channels in the root:
+
+```json5
+{
+ channels: {
+ slack: {
+ replyToMode: "off",
+ replyToModeByChatType: { group: "first" },
+ },
+ },
+}
+```
+
+Make channels thread, keep DMs in the root:
+
+```json5
+{
+ channels: {
+ slack: {
+ replyToMode: "first",
+ replyToModeByChatType: { direct: "off", group: "off" },
+ },
+ },
+}
+```
+
+### Manual threading tags
+
+For fine-grained control, use these tags in agent responses:
+
+- `[[reply_to_current]]` — reply to the triggering message (start/continue thread).
+- `[[reply_to:]]` — reply to a specific message id.
+
+## Sessions + routing
+
+- DMs share the `main` session (like WhatsApp/Telegram).
+- Channels map to `agent::slack:channel:` sessions.
+- Slash commands use `agent::slack:slash:` sessions (prefix configurable via `channels.slack.slashCommand.sessionPrefix`).
+- If Slack doesn’t provide `channel_type`, OpenClaw infers it from the channel ID prefix (`D`, `C`, `G`) and defaults to `channel` to keep session keys stable.
+- Native command registration uses `commands.native` (global default `"auto"` → Slack off) and can be overridden per-workspace with `channels.slack.commands.native`. Text commands require standalone `/...` messages and can be disabled with `commands.text: false`. Slack slash commands are managed in the Slack app and are not removed automatically. Use `commands.useAccessGroups: false` to bypass access-group checks for commands.
+- Full command list + config: [Slash commands](/tools/slash-commands)
+
+## DM security (pairing)
+
+- Default: `channels.slack.dm.policy="pairing"` — unknown DM senders get a pairing code (expires after 1 hour).
+- Approve via: `openclaw pairing approve slack `.
+- To allow anyone: set `channels.slack.dm.policy="open"` and `channels.slack.dm.allowFrom=["*"]`.
+- `channels.slack.dm.allowFrom` accepts user IDs, @handles, or emails (resolved at startup when tokens allow). The wizard accepts usernames and resolves them to ids during setup when tokens allow.
+
+## Group policy
+
+- `channels.slack.groupPolicy` controls channel handling (`open|disabled|allowlist`).
+- `allowlist` requires channels to be listed in `channels.slack.channels`.
+- If you only set `SLACK_BOT_TOKEN`/`SLACK_APP_TOKEN` and never create a `channels.slack` section,
+ the runtime defaults `groupPolicy` to `open`. Add `channels.slack.groupPolicy`,
+ `channels.defaults.groupPolicy`, or a channel allowlist to lock it down.
+- The configure wizard accepts `#channel` names and resolves them to IDs when possible
+ (public + private); if multiple matches exist, it prefers the active channel.
+- On startup, OpenClaw resolves channel/user names in allowlists to IDs (when tokens allow)
+ and logs the mapping; unresolved entries are kept as typed.
+- To allow **no channels**, set `channels.slack.groupPolicy: "disabled"` (or keep an empty allowlist).
+
+Channel options (`channels.slack.channels.` or `channels.slack.channels.`):
+
+- `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
+- `requireMention`: mention gating for the channel.
+- `tools`: optional per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
+- `toolsBySender`: optional per-sender tool policy overrides within the channel (keys are sender ids/@handles/emails; `"*"` wildcard supported).
+- `allowBots`: allow bot-authored messages in this channel (default: false).
+- `users`: optional per-channel user allowlist.
+- `skills`: skill filter (omit = all skills, empty = none).
+- `systemPrompt`: extra system prompt for the channel (combined with topic/purpose).
+- `enabled`: set `false` to disable the channel.
+
+## Delivery targets
+
+Use these with cron/CLI sends:
+
+- `user:` for DMs
+- `channel:` for channels
+
+## Tool actions
+
+Slack tool actions can be gated with `channels.slack.actions.*`:
+
+| Action group | Default | Notes |
+| ------------ | ------- | ---------------------- |
+| reactions | enabled | React + list reactions |
+| messages | enabled | Read/send/edit/delete |
+| pins | enabled | Pin/unpin/list |
+| memberInfo | enabled | Member info |
+| emojiList | enabled | Custom emoji list |
+
+## Security notes
+
+- Writes default to the bot token so state-changing actions stay scoped to the
+ app's bot permissions and identity.
+- Setting `userTokenReadOnly: false` allows the user token to be used for write
+ operations when a bot token is unavailable, which means actions run with the
+ installing user's access. Treat the user token as highly privileged and keep
+ action gates and allowlists tight.
+- If you enable user-token writes, make sure the user token includes the write
+ scopes you expect (`chat:write`, `reactions:write`, `pins:write`,
+ `files:write`) or those operations will fail.
+
+## Notes
+
+- Mention gating is controlled via `channels.slack.channels` (set `requireMention` to `true`); `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions.
+- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
+- Reaction notifications follow `channels.slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
+- Bot-authored messages are ignored by default; enable via `channels.slack.allowBots` or `channels.slack.channels..allowBots`.
+- Warning: If you allow replies to other bots (`channels.slack.allowBots=true` or `channels.slack.channels..allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.slack.channels..users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
+- For the Slack tool, reaction removal semantics are in [/tools/reactions](/tools/reactions).
+- Attachments are downloaded to the media store when permitted and under the size limit.
diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md
new file mode 100644
index 0000000000000000000000000000000000000000..45f6d30f4b5818045dfbc37ebf59f537f84539dc
--- /dev/null
+++ b/docs/channels/telegram.md
@@ -0,0 +1,750 @@
+---
+summary: "Telegram bot support status, capabilities, and configuration"
+read_when:
+ - Working on Telegram features or webhooks
+title: "Telegram"
+---
+
+# Telegram (Bot API)
+
+Status: production-ready for bot DMs + groups via grammY. Long-polling by default; webhook optional.
+
+## Quick setup (beginner)
+
+1. Create a bot with **@BotFather** ([direct link](https://t.me/BotFather)). Confirm the handle is exactly `@BotFather`, then copy the token.
+2. Set the token:
+ - Env: `TELEGRAM_BOT_TOKEN=...`
+ - Or config: `channels.telegram.botToken: "..."`.
+ - If both are set, config takes precedence (env fallback is default-account only).
+3. Start the gateway.
+4. DM access is pairing by default; approve the pairing code on first contact.
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ telegram: {
+ enabled: true,
+ botToken: "123:abc",
+ dmPolicy: "pairing",
+ },
+ },
+}
+```
+
+## What it is
+
+- A Telegram Bot API channel owned by the Gateway.
+- Deterministic routing: replies go back to Telegram; the model never chooses channels.
+- DMs share the agent's main session; groups stay isolated (`agent::telegram:group:`).
+
+## Setup (fast path)
+
+### 1) Create a bot token (BotFather)
+
+1. Open Telegram and chat with **@BotFather** ([direct link](https://t.me/BotFather)). Confirm the handle is exactly `@BotFather`.
+2. Run `/newbot`, then follow the prompts (name + username ending in `bot`).
+3. Copy the token and store it safely.
+
+Optional BotFather settings:
+
+- `/setjoingroups` — allow/deny adding the bot to groups.
+- `/setprivacy` — control whether the bot sees all group messages.
+
+### 2) Configure the token (env or config)
+
+Example:
+
+```json5
+{
+ channels: {
+ telegram: {
+ enabled: true,
+ botToken: "123:abc",
+ dmPolicy: "pairing",
+ groups: { "*": { requireMention: true } },
+ },
+ },
+}
+```
+
+Env option: `TELEGRAM_BOT_TOKEN=...` (works for the default account).
+If both env and config are set, config takes precedence.
+
+Multi-account support: use `channels.telegram.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
+
+3. Start the gateway. Telegram starts when a token is resolved (config first, env fallback).
+4. DM access defaults to pairing. Approve the code when the bot is first contacted.
+5. For groups: add the bot, decide privacy/admin behavior (below), then set `channels.telegram.groups` to control mention gating + allowlists.
+
+## Token + privacy + permissions (Telegram side)
+
+### Token creation (BotFather)
+
+- `/newbot` creates the bot and returns the token (keep it secret).
+- If a token leaks, revoke/regenerate it via @BotFather and update your config.
+
+### Group message visibility (Privacy Mode)
+
+Telegram bots default to **Privacy Mode**, which limits which group messages they receive.
+If your bot must see _all_ group messages, you have two options:
+
+- Disable privacy mode with `/setprivacy` **or**
+- Add the bot as a group **admin** (admin bots receive all messages).
+
+**Note:** When you toggle privacy mode, Telegram requires removing + re‑adding the bot
+to each group for the change to take effect.
+
+### Group permissions (admin rights)
+
+Admin status is set inside the group (Telegram UI). Admin bots always receive all
+group messages, so use admin if you need full visibility.
+
+## How it works (behavior)
+
+- Inbound messages are normalized into the shared channel envelope with reply context and media placeholders.
+- Group replies require a mention by default (native @mention or `agents.list[].groupChat.mentionPatterns` / `messages.groupChat.mentionPatterns`).
+- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
+- Replies always route back to the same Telegram chat.
+- Long-polling uses grammY runner with per-chat sequencing; overall concurrency is capped by `agents.defaults.maxConcurrent`.
+- Telegram Bot API does not support read receipts; there is no `sendReadReceipts` option.
+
+## Draft streaming
+
+OpenClaw can stream partial replies in Telegram DMs using `sendMessageDraft`.
+
+Requirements:
+
+- Threaded Mode enabled for the bot in @BotFather (forum topic mode).
+- Private chat threads only (Telegram includes `message_thread_id` on inbound messages).
+- `channels.telegram.streamMode` not set to `"off"` (default: `"partial"`, `"block"` enables chunked draft updates).
+
+Draft streaming is DM-only; Telegram does not support it in groups or channels.
+
+## Formatting (Telegram HTML)
+
+- Outbound Telegram text uses `parse_mode: "HTML"` (Telegram’s supported tag subset).
+- Markdown-ish input is rendered into **Telegram-safe HTML** (bold/italic/strike/code/links); block elements are flattened to text with newlines/bullets.
+- Raw HTML from models is escaped to avoid Telegram parse errors.
+- If Telegram rejects the HTML payload, OpenClaw retries the same message as plain text.
+
+## Commands (native + custom)
+
+OpenClaw registers native commands (like `/status`, `/reset`, `/model`) with Telegram’s bot menu on startup.
+You can add custom commands to the menu via config:
+
+```json5
+{
+ channels: {
+ telegram: {
+ customCommands: [
+ { command: "backup", description: "Git backup" },
+ { command: "generate", description: "Create an image" },
+ ],
+ },
+ },
+}
+```
+
+## Troubleshooting
+
+- `setMyCommands failed` in logs usually means outbound HTTPS/DNS is blocked to `api.telegram.org`.
+- If you see `sendMessage` or `sendChatAction` failures, check IPv6 routing and DNS.
+
+More help: [Channel troubleshooting](/channels/troubleshooting).
+
+Notes:
+
+- Custom commands are **menu entries only**; OpenClaw does not implement them unless you handle them elsewhere.
+- Command names are normalized (leading `/` stripped, lowercased) and must match `a-z`, `0-9`, `_` (1–32 chars).
+- Custom commands **cannot override native commands**. Conflicts are ignored and logged.
+- If `commands.native` is disabled, only custom commands are registered (or cleared if none).
+
+## Limits
+
+- Outbound text is chunked to `channels.telegram.textChunkLimit` (default 4000).
+- Optional newline chunking: set `channels.telegram.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
+- Media downloads/uploads are capped by `channels.telegram.mediaMaxMb` (default 5).
+- Telegram Bot API requests time out after `channels.telegram.timeoutSeconds` (default 500 via grammY). Set lower to avoid long hangs.
+- Group history context uses `channels.telegram.historyLimit` (or `channels.telegram.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
+- DM history can be limited with `channels.telegram.dmHistoryLimit` (user turns). Per-user overrides: `channels.telegram.dms[""].historyLimit`.
+
+## Group activation modes
+
+By default, the bot only responds to mentions in groups (`@botname` or patterns in `agents.list[].groupChat.mentionPatterns`). To change this behavior:
+
+### Via config (recommended)
+
+```json5
+{
+ channels: {
+ telegram: {
+ groups: {
+ "-1001234567890": { requireMention: false }, // always respond in this group
+ },
+ },
+ },
+}
+```
+
+**Important:** Setting `channels.telegram.groups` creates an **allowlist** - only listed groups (or `"*"`) will be accepted.
+Forum topics inherit their parent group config (allowFrom, requireMention, skills, prompts) unless you add per-topic overrides under `channels.telegram.groups..topics.`.
+
+To allow all groups with always-respond:
+
+```json5
+{
+ channels: {
+ telegram: {
+ groups: {
+ "*": { requireMention: false }, // all groups, always respond
+ },
+ },
+ },
+}
+```
+
+To keep mention-only for all groups (default behavior):
+
+```json5
+{
+ channels: {
+ telegram: {
+ groups: {
+ "*": { requireMention: true }, // or omit groups entirely
+ },
+ },
+ },
+}
+```
+
+### Via command (session-level)
+
+Send in the group:
+
+- `/activation always` - respond to all messages
+- `/activation mention` - require mentions (default)
+
+**Note:** Commands update session state only. For persistent behavior across restarts, use config.
+
+### Getting the group chat ID
+
+Forward any message from the group to `@userinfobot` or `@getidsbot` on Telegram to see the chat ID (negative number like `-1001234567890`).
+
+**Tip:** For your own user ID, DM the bot and it will reply with your user ID (pairing message), or use `/whoami` once commands are enabled.
+
+**Privacy note:** `@userinfobot` is a third-party bot. If you prefer, add the bot to the group, send a message, and use `openclaw logs --follow` to read `chat.id`, or use the Bot API `getUpdates`.
+
+## Config writes
+
+By default, Telegram is allowed to write config updates triggered by channel events or `/config set|unset`.
+
+This happens when:
+
+- A group is upgraded to a supergroup and Telegram emits `migrate_to_chat_id` (chat ID changes). OpenClaw can migrate `channels.telegram.groups` automatically.
+- You run `/config set` or `/config unset` in a Telegram chat (requires `commands.config: true`).
+
+Disable with:
+
+```json5
+{
+ channels: { telegram: { configWrites: false } },
+}
+```
+
+## Topics (forum supergroups)
+
+Telegram forum topics include a `message_thread_id` per message. OpenClaw:
+
+- Appends `:topic:` to the Telegram group session key so each topic is isolated.
+- Sends typing indicators and replies with `message_thread_id` so responses stay in the topic.
+- General topic (thread id `1`) is special: message sends omit `message_thread_id` (Telegram rejects it), but typing indicators still include it.
+- Exposes `MessageThreadId` + `IsForum` in template context for routing/templating.
+- Topic-specific configuration is available under `channels.telegram.groups..topics.` (skills, allowlists, auto-reply, system prompts, disable).
+- Topic configs inherit group settings (requireMention, allowlists, skills, prompts, enabled) unless overridden per topic.
+
+Private chats can include `message_thread_id` in some edge cases. OpenClaw keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present.
+
+## Inline Buttons
+
+Telegram supports inline keyboards with callback buttons.
+
+```json5
+{
+ channels: {
+ telegram: {
+ capabilities: {
+ inlineButtons: "allowlist",
+ },
+ },
+ },
+}
+```
+
+For per-account configuration:
+
+```json5
+{
+ channels: {
+ telegram: {
+ accounts: {
+ main: {
+ capabilities: {
+ inlineButtons: "allowlist",
+ },
+ },
+ },
+ },
+ },
+}
+```
+
+Scopes:
+
+- `off` — inline buttons disabled
+- `dm` — only DMs (group targets blocked)
+- `group` — only groups (DM targets blocked)
+- `all` — DMs + groups
+- `allowlist` — DMs + groups, but only senders allowed by `allowFrom`/`groupAllowFrom` (same rules as control commands)
+
+Default: `allowlist`.
+Legacy: `capabilities: ["inlineButtons"]` = `inlineButtons: "all"`.
+
+### Sending buttons
+
+Use the message tool with the `buttons` parameter:
+
+```json5
+{
+ action: "send",
+ channel: "telegram",
+ to: "123456789",
+ message: "Choose an option:",
+ buttons: [
+ [
+ { text: "Yes", callback_data: "yes" },
+ { text: "No", callback_data: "no" },
+ ],
+ [{ text: "Cancel", callback_data: "cancel" }],
+ ],
+}
+```
+
+When a user clicks a button, the callback data is sent back to the agent as a message with the format:
+`callback_data: value`
+
+### Configuration options
+
+Telegram capabilities can be configured at two levels (object form shown above; legacy string arrays still supported):
+
+- `channels.telegram.capabilities`: Global default capability config applied to all Telegram accounts unless overridden.
+- `channels.telegram.accounts..capabilities`: Per-account capabilities that override the global defaults for that specific account.
+
+Use the global setting when all Telegram bots/accounts should behave the same. Use per-account configuration when different bots need different behaviors (for example, one account only handles DMs while another is allowed in groups).
+
+## Access control (DMs + groups)
+
+### DM access
+
+- Default: `channels.telegram.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
+- Approve via:
+ - `openclaw pairing list telegram`
+ - `openclaw pairing approve telegram `
+- Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/start/pairing)
+- `channels.telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. It is **not** the bot username; use the human sender’s ID. The wizard accepts `@username` and resolves it to the numeric ID when possible.
+
+#### Finding your Telegram user ID
+
+Safer (no third-party bot):
+
+1. Start the gateway and DM your bot.
+2. Run `openclaw logs --follow` and look for `from.id`.
+
+Alternate (official Bot API):
+
+1. DM your bot.
+2. Fetch updates with your bot token and read `message.from.id`:
+ ```bash
+ curl "https://api.telegram.org/bot/getUpdates"
+ ```
+
+Third-party (less private):
+
+- DM `@userinfobot` or `@getidsbot` and use the returned user id.
+
+### Group access
+
+Two independent controls:
+
+**1. Which groups are allowed** (group allowlist via `channels.telegram.groups`):
+
+- No `groups` config = all groups allowed
+- With `groups` config = only listed groups or `"*"` are allowed
+- Example: `"groups": { "-1001234567890": {}, "*": {} }` allows all groups
+
+**2. Which senders are allowed** (sender filtering via `channels.telegram.groupPolicy`):
+
+- `"open"` = all senders in allowed groups can message
+- `"allowlist"` = only senders in `channels.telegram.groupAllowFrom` can message
+- `"disabled"` = no group messages accepted at all
+ Default is `groupPolicy: "allowlist"` (blocked unless you add `groupAllowFrom`).
+
+Most users want: `groupPolicy: "allowlist"` + `groupAllowFrom` + specific groups listed in `channels.telegram.groups`
+
+## Long-polling vs webhook
+
+- Default: long-polling (no public URL required).
+- Webhook mode: set `channels.telegram.webhookUrl` and `channels.telegram.webhookSecret` (optionally `channels.telegram.webhookPath`).
+ - The local listener binds to `0.0.0.0:8787` and serves `POST /telegram-webhook` by default.
+ - If your public URL is different, use a reverse proxy and point `channels.telegram.webhookUrl` at the public endpoint.
+
+## Reply threading
+
+Telegram supports optional threaded replies via tags:
+
+- `[[reply_to_current]]` -- reply to the triggering message.
+- `[[reply_to:]]` -- reply to a specific message id.
+
+Controlled by `channels.telegram.replyToMode`:
+
+- `first` (default), `all`, `off`.
+
+## Audio messages (voice vs file)
+
+Telegram distinguishes **voice notes** (round bubble) from **audio files** (metadata card).
+OpenClaw defaults to audio files for backward compatibility.
+
+To force a voice note bubble in agent replies, include this tag anywhere in the reply:
+
+- `[[audio_as_voice]]` — send audio as a voice note instead of a file.
+
+The tag is stripped from the delivered text. Other channels ignore this tag.
+
+For message tool sends, set `asVoice: true` with a voice-compatible audio `media` URL
+(`message` is optional when media is present):
+
+```json5
+{
+ action: "send",
+ channel: "telegram",
+ to: "123456789",
+ media: "https://example.com/voice.ogg",
+ asVoice: true,
+}
+```
+
+## Stickers
+
+OpenClaw supports receiving and sending Telegram stickers with intelligent caching.
+
+### Receiving stickers
+
+When a user sends a sticker, OpenClaw handles it based on the sticker type:
+
+- **Static stickers (WEBP):** Downloaded and processed through vision. The sticker appears as a `` placeholder in the message content.
+- **Animated stickers (TGS):** Skipped (Lottie format not supported for processing).
+- **Video stickers (WEBM):** Skipped (video format not supported for processing).
+
+Template context field available when receiving stickers:
+
+- `Sticker` — object with:
+ - `emoji` — emoji associated with the sticker
+ - `setName` — name of the sticker set
+ - `fileId` — Telegram file ID (send the same sticker back)
+ - `fileUniqueId` — stable ID for cache lookup
+ - `cachedDescription` — cached vision description when available
+
+### Sticker cache
+
+Stickers are processed through the AI's vision capabilities to generate descriptions. Since the same stickers are often sent repeatedly, OpenClaw caches these descriptions to avoid redundant API calls.
+
+**How it works:**
+
+1. **First encounter:** The sticker image is sent to the AI for vision analysis. The AI generates a description (e.g., "A cartoon cat waving enthusiastically").
+2. **Cache storage:** The description is saved along with the sticker's file ID, emoji, and set name.
+3. **Subsequent encounters:** When the same sticker is seen again, the cached description is used directly. The image is not sent to the AI.
+
+**Cache location:** `~/.openclaw/telegram/sticker-cache.json`
+
+**Cache entry format:**
+
+```json
+{
+ "fileId": "CAACAgIAAxkBAAI...",
+ "fileUniqueId": "AgADBAADb6cxG2Y",
+ "emoji": "👋",
+ "setName": "CoolCats",
+ "description": "A cartoon cat waving enthusiastically",
+ "cachedAt": "2026-01-15T10:30:00.000Z"
+}
+```
+
+**Benefits:**
+
+- Reduces API costs by avoiding repeated vision calls for the same sticker
+- Faster response times for cached stickers (no vision processing delay)
+- Enables sticker search functionality based on cached descriptions
+
+The cache is populated automatically as stickers are received. There is no manual cache management required.
+
+### Sending stickers
+
+The agent can send and search stickers using the `sticker` and `sticker-search` actions. These are disabled by default and must be enabled in config:
+
+```json5
+{
+ channels: {
+ telegram: {
+ actions: {
+ sticker: true,
+ },
+ },
+ },
+}
+```
+
+**Send a sticker:**
+
+```json5
+{
+ action: "sticker",
+ channel: "telegram",
+ to: "123456789",
+ fileId: "CAACAgIAAxkBAAI...",
+}
+```
+
+Parameters:
+
+- `fileId` (required) — the Telegram file ID of the sticker. Obtain this from `Sticker.fileId` when receiving a sticker, or from a `sticker-search` result.
+- `replyTo` (optional) — message ID to reply to.
+- `threadId` (optional) — message thread ID for forum topics.
+
+**Search for stickers:**
+
+The agent can search cached stickers by description, emoji, or set name:
+
+```json5
+{
+ action: "sticker-search",
+ channel: "telegram",
+ query: "cat waving",
+ limit: 5,
+}
+```
+
+Returns matching stickers from the cache:
+
+```json5
+{
+ ok: true,
+ count: 2,
+ stickers: [
+ {
+ fileId: "CAACAgIAAxkBAAI...",
+ emoji: "👋",
+ description: "A cartoon cat waving enthusiastically",
+ setName: "CoolCats",
+ },
+ ],
+}
+```
+
+The search uses fuzzy matching across description text, emoji characters, and set names.
+
+**Example with threading:**
+
+```json5
+{
+ action: "sticker",
+ channel: "telegram",
+ to: "-1001234567890",
+ fileId: "CAACAgIAAxkBAAI...",
+ replyTo: 42,
+ threadId: 123,
+}
+```
+
+## Streaming (drafts)
+
+Telegram can stream **draft bubbles** while the agent is generating a response.
+OpenClaw uses Bot API `sendMessageDraft` (not real messages) and then sends the
+final reply as a normal message.
+
+Requirements (Telegram Bot API 9.3+):
+
+- **Private chats with topics enabled** (forum topic mode for the bot).
+- Incoming messages must include `message_thread_id` (private topic thread).
+- Streaming is ignored for groups/supergroups/channels.
+
+Config:
+
+- `channels.telegram.streamMode: "off" | "partial" | "block"` (default: `partial`)
+ - `partial`: update the draft bubble with the latest streaming text.
+ - `block`: update the draft bubble in larger blocks (chunked).
+ - `off`: disable draft streaming.
+- Optional (only for `streamMode: "block"`):
+ - `channels.telegram.draftChunk: { minChars?, maxChars?, breakPreference? }`
+ - defaults: `minChars: 200`, `maxChars: 800`, `breakPreference: "paragraph"` (clamped to `channels.telegram.textChunkLimit`).
+
+Note: draft streaming is separate from **block streaming** (channel messages).
+Block streaming is off by default and requires `channels.telegram.blockStreaming: true`
+if you want early Telegram messages instead of draft updates.
+
+Reasoning stream (Telegram only):
+
+- `/reasoning stream` streams reasoning into the draft bubble while the reply is
+ generating, then sends the final answer without reasoning.
+- If `channels.telegram.streamMode` is `off`, reasoning stream is disabled.
+ More context: [Streaming + chunking](/concepts/streaming).
+
+## Retry policy
+
+Outbound Telegram API calls retry on transient network/429 errors with exponential backoff and jitter. Configure via `channels.telegram.retry`. See [Retry policy](/concepts/retry).
+
+## Agent tool (messages + reactions)
+
+- Tool: `telegram` with `sendMessage` action (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`).
+- Tool: `telegram` with `react` action (`chatId`, `messageId`, `emoji`).
+- Tool: `telegram` with `deleteMessage` action (`chatId`, `messageId`).
+- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
+- Tool gating: `channels.telegram.actions.reactions`, `channels.telegram.actions.sendMessage`, `channels.telegram.actions.deleteMessage` (default: enabled), and `channels.telegram.actions.sticker` (default: disabled).
+
+## Reaction notifications
+
+**How reactions work:**
+Telegram reactions arrive as **separate `message_reaction` events**, not as properties in message payloads. When a user adds a reaction, OpenClaw:
+
+1. Receives the `message_reaction` update from Telegram API
+2. Converts it to a **system event** with format: `"Telegram reaction added: {emoji} by {user} on msg {id}"`
+3. Enqueues the system event using the **same session key** as regular messages
+4. When the next message arrives in that conversation, system events are drained and prepended to the agent's context
+
+The agent sees reactions as **system notifications** in the conversation history, not as message metadata.
+
+**Configuration:**
+
+- `channels.telegram.reactionNotifications`: Controls which reactions trigger notifications
+ - `"off"` — ignore all reactions
+ - `"own"` — notify when users react to bot messages (best-effort; in-memory) (default)
+ - `"all"` — notify for all reactions
+
+- `channels.telegram.reactionLevel`: Controls agent's reaction capability
+ - `"off"` — agent cannot react to messages
+ - `"ack"` — bot sends acknowledgment reactions (👀 while processing) (default)
+ - `"minimal"` — agent can react sparingly (guideline: 1 per 5-10 exchanges)
+ - `"extensive"` — agent can react liberally when appropriate
+
+**Forum groups:** Reactions in forum groups include `message_thread_id` and use session keys like `agent:main:telegram:group:{chatId}:topic:{threadId}`. This ensures reactions and messages in the same topic stay together.
+
+**Example config:**
+
+```json5
+{
+ channels: {
+ telegram: {
+ reactionNotifications: "all", // See all reactions
+ reactionLevel: "minimal", // Agent can react sparingly
+ },
+ },
+}
+```
+
+**Requirements:**
+
+- Telegram bots must explicitly request `message_reaction` in `allowed_updates` (configured automatically by OpenClaw)
+- For webhook mode, reactions are included in the webhook `allowed_updates`
+- For polling mode, reactions are included in the `getUpdates` `allowed_updates`
+
+## Delivery targets (CLI/cron)
+
+- Use a chat id (`123456789`) or a username (`@name`) as the target.
+- Example: `openclaw message send --channel telegram --target 123456789 --message "hi"`.
+
+## Troubleshooting
+
+**Bot doesn’t respond to non-mention messages in a group:**
+
+- If you set `channels.telegram.groups.*.requireMention=false`, Telegram’s Bot API **privacy mode** must be disabled.
+ - BotFather: `/setprivacy` → **Disable** (then remove + re-add the bot to the group)
+- `openclaw channels status` shows a warning when config expects unmentioned group messages.
+- `openclaw channels status --probe` can additionally check membership for explicit numeric group IDs (it can’t audit wildcard `"*"` rules).
+- Quick test: `/activation always` (session-only; use config for persistence)
+
+**Bot not seeing group messages at all:**
+
+- If `channels.telegram.groups` is set, the group must be listed or use `"*"`
+- Check Privacy Settings in @BotFather → "Group Privacy" should be **OFF**
+- Verify bot is actually a member (not just an admin with no read access)
+- Check gateway logs: `openclaw logs --follow` (look for "skipping group message")
+
+**Bot responds to mentions but not `/activation always`:**
+
+- The `/activation` command updates session state but doesn't persist to config
+- For persistent behavior, add group to `channels.telegram.groups` with `requireMention: false`
+
+**Commands like `/status` don't work:**
+
+- Make sure your Telegram user ID is authorized (via pairing or `channels.telegram.allowFrom`)
+- Commands require authorization even in groups with `groupPolicy: "open"`
+
+**Long-polling aborts immediately on Node 22+ (often with proxies/custom fetch):**
+
+- Node 22+ is stricter about `AbortSignal` instances; foreign signals can abort `fetch` calls right away.
+- Upgrade to a OpenClaw build that normalizes abort signals, or run the gateway on Node 20 until you can upgrade.
+
+**Bot starts, then silently stops responding (or logs `HttpError: Network request ... failed`):**
+
+- Some hosts resolve `api.telegram.org` to IPv6 first. If your server does not have working IPv6 egress, grammY can get stuck on IPv6-only requests.
+- Fix by enabling IPv6 egress **or** forcing IPv4 resolution for `api.telegram.org` (for example, add an `/etc/hosts` entry using the IPv4 A record, or prefer IPv4 in your OS DNS stack), then restart the gateway.
+- Quick check: `dig +short api.telegram.org A` and `dig +short api.telegram.org AAAA` to confirm what DNS returns.
+
+## Configuration reference (Telegram)
+
+Full configuration: [Configuration](/gateway/configuration)
+
+Provider options:
+
+- `channels.telegram.enabled`: enable/disable channel startup.
+- `channels.telegram.botToken`: bot token (BotFather).
+- `channels.telegram.tokenFile`: read token from file path.
+- `channels.telegram.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
+- `channels.telegram.allowFrom`: DM allowlist (ids/usernames). `open` requires `"*"`.
+- `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
+- `channels.telegram.groupAllowFrom`: group sender allowlist (ids/usernames).
+- `channels.telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults).
+ - `channels.telegram.groups..requireMention`: mention gating default.
+ - `channels.telegram.groups..skills`: skill filter (omit = all skills, empty = none).
+ - `channels.telegram.groups..allowFrom`: per-group sender allowlist override.
+ - `channels.telegram.groups..systemPrompt`: extra system prompt for the group.
+ - `channels.telegram.groups..enabled`: disable the group when `false`.
+ - `channels.telegram.groups..topics..*`: per-topic overrides (same fields as group).
+ - `channels.telegram.groups..topics..requireMention`: per-topic mention gating override.
+- `channels.telegram.capabilities.inlineButtons`: `off | dm | group | all | allowlist` (default: allowlist).
+- `channels.telegram.accounts..capabilities.inlineButtons`: per-account override.
+- `channels.telegram.replyToMode`: `off | first | all` (default: `first`).
+- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
+- `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
+- `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true).
+- `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
+- `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).
+- `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).
+- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts.
+- `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP).
+- `channels.telegram.webhookUrl`: enable webhook mode (requires `channels.telegram.webhookSecret`).
+- `channels.telegram.webhookSecret`: webhook secret (required when webhookUrl is set).
+- `channels.telegram.webhookPath`: local webhook path (default `/telegram-webhook`).
+- `channels.telegram.actions.reactions`: gate Telegram tool reactions.
+- `channels.telegram.actions.sendMessage`: gate Telegram tool message sends.
+- `channels.telegram.actions.deleteMessage`: gate Telegram tool message deletes.
+- `channels.telegram.actions.sticker`: gate Telegram sticker actions — send and search (default: false).
+- `channels.telegram.reactionNotifications`: `off | own | all` — control which reactions trigger system events (default: `own` when not set).
+- `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` — control agent's reaction capability (default: `minimal` when not set).
+
+Related global options:
+
+- `agents.list[].groupChat.mentionPatterns` (mention gating patterns).
+- `messages.groupChat.mentionPatterns` (global fallback).
+- `commands.native` (defaults to `"auto"` → on for Telegram/Discord, off for Slack), `commands.text`, `commands.useAccessGroups` (command behavior). Override with `channels.telegram.commands.native`.
+- `messages.responsePrefix`, `messages.ackReaction`, `messages.ackReactionScope`, `messages.removeAckAfterReply`.
diff --git a/docs/channels/tlon.md b/docs/channels/tlon.md
new file mode 100644
index 0000000000000000000000000000000000000000..3b632a92744af07673786b798234244bbc8c0784
--- /dev/null
+++ b/docs/channels/tlon.md
@@ -0,0 +1,132 @@
+---
+summary: "Tlon/Urbit support status, capabilities, and configuration"
+read_when:
+ - Working on Tlon/Urbit channel features
+title: "Tlon"
+---
+
+# Tlon (plugin)
+
+Tlon is a decentralized messenger built on Urbit. OpenClaw connects to your Urbit ship and can
+respond to DMs and group chat messages. Group replies require an @ mention by default and can
+be further restricted via allowlists.
+
+Status: supported via plugin. DMs, group mentions, thread replies, and text-only media fallback
+(URL appended to caption). Reactions, polls, and native media uploads are not supported.
+
+## Plugin required
+
+Tlon ships as a plugin and is not bundled with the core install.
+
+Install via CLI (npm registry):
+
+```bash
+openclaw plugins install @openclaw/tlon
+```
+
+Local checkout (when running from a git repo):
+
+```bash
+openclaw plugins install ./extensions/tlon
+```
+
+Details: [Plugins](/plugin)
+
+## Setup
+
+1. Install the Tlon plugin.
+2. Gather your ship URL and login code.
+3. Configure `channels.tlon`.
+4. Restart the gateway.
+5. DM the bot or mention it in a group channel.
+
+Minimal config (single account):
+
+```json5
+{
+ channels: {
+ tlon: {
+ enabled: true,
+ ship: "~sampel-palnet",
+ url: "https://your-ship-host",
+ code: "lidlut-tabwed-pillex-ridrup",
+ },
+ },
+}
+```
+
+## Group channels
+
+Auto-discovery is enabled by default. You can also pin channels manually:
+
+```json5
+{
+ channels: {
+ tlon: {
+ groupChannels: ["chat/~host-ship/general", "chat/~host-ship/support"],
+ },
+ },
+}
+```
+
+Disable auto-discovery:
+
+```json5
+{
+ channels: {
+ tlon: {
+ autoDiscoverChannels: false,
+ },
+ },
+}
+```
+
+## Access control
+
+DM allowlist (empty = allow all):
+
+```json5
+{
+ channels: {
+ tlon: {
+ dmAllowlist: ["~zod", "~nec"],
+ },
+ },
+}
+```
+
+Group authorization (restricted by default):
+
+```json5
+{
+ channels: {
+ tlon: {
+ defaultAuthorizedShips: ["~zod"],
+ authorization: {
+ channelRules: {
+ "chat/~host-ship/general": {
+ mode: "restricted",
+ allowedShips: ["~zod", "~nec"],
+ },
+ "chat/~host-ship/announcements": {
+ mode: "open",
+ },
+ },
+ },
+ },
+ },
+}
+```
+
+## Delivery targets (CLI/cron)
+
+Use these with `openclaw message send` or cron delivery:
+
+- DM: `~sampel-palnet` or `dm/~sampel-palnet`
+- Group: `chat/~host-ship/channel` or `group:~host-ship/channel`
+
+## Notes
+
+- Group replies require a mention (e.g. `~your-bot-ship`) to respond.
+- Thread replies: if the inbound message is in a thread, OpenClaw replies in-thread.
+- Media: `sendMedia` falls back to text + URL (no native upload).
diff --git a/docs/channels/troubleshooting.md b/docs/channels/troubleshooting.md
new file mode 100644
index 0000000000000000000000000000000000000000..929b0c776c50ddb312936f7de45eb33a7fd1cfc1
--- /dev/null
+++ b/docs/channels/troubleshooting.md
@@ -0,0 +1,29 @@
+---
+summary: "Channel-specific troubleshooting shortcuts (Discord/Telegram/WhatsApp)"
+read_when:
+ - A channel connects but messages don’t flow
+ - Investigating channel misconfiguration (intents, permissions, privacy mode)
+title: "Channel Troubleshooting"
+---
+
+# Channel troubleshooting
+
+Start with:
+
+```bash
+openclaw doctor
+openclaw channels status --probe
+```
+
+`channels status --probe` prints warnings when it can detect common channel misconfigurations, and includes small live checks (credentials, some permissions/membership).
+
+## Channels
+
+- Discord: [/channels/discord#troubleshooting](/channels/discord#troubleshooting)
+- Telegram: [/channels/telegram#troubleshooting](/channels/telegram#troubleshooting)
+- WhatsApp: [/channels/whatsapp#troubleshooting-quick](/channels/whatsapp#troubleshooting-quick)
+
+## Telegram quick fixes
+
+- Logs show `HttpError: Network request for 'sendMessage' failed` or `sendChatAction` → check IPv6 DNS. If `api.telegram.org` resolves to IPv6 first and the host lacks IPv6 egress, force IPv4 or enable IPv6. See [/channels/telegram#troubleshooting](/channels/telegram#troubleshooting).
+- Logs show `setMyCommands failed` → check outbound HTTPS and DNS reachability to `api.telegram.org` (common on locked-down VPS or proxies).
diff --git a/docs/channels/twitch.md b/docs/channels/twitch.md
new file mode 100644
index 0000000000000000000000000000000000000000..7901c042781d79e298ba2ac0383d7143f8bbe505
--- /dev/null
+++ b/docs/channels/twitch.md
@@ -0,0 +1,379 @@
+---
+summary: "Twitch chat bot configuration and setup"
+read_when:
+ - Setting up Twitch chat integration for OpenClaw
+title: "Twitch"
+---
+
+# Twitch (plugin)
+
+Twitch chat support via IRC connection. OpenClaw connects as a Twitch user (bot account) to receive and send messages in channels.
+
+## Plugin required
+
+Twitch ships as a plugin and is not bundled with the core install.
+
+Install via CLI (npm registry):
+
+```bash
+openclaw plugins install @openclaw/twitch
+```
+
+Local checkout (when running from a git repo):
+
+```bash
+openclaw plugins install ./extensions/twitch
+```
+
+Details: [Plugins](/plugin)
+
+## Quick setup (beginner)
+
+1. Create a dedicated Twitch account for the bot (or use an existing account).
+2. Generate credentials: [Twitch Token Generator](https://twitchtokengenerator.com/)
+ - Select **Bot Token**
+ - Verify scopes `chat:read` and `chat:write` are selected
+ - Copy the **Client ID** and **Access Token**
+3. Find your Twitch user ID: https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/
+4. Configure the token:
+ - Env: `OPENCLAW_TWITCH_ACCESS_TOKEN=...` (default account only)
+ - Or config: `channels.twitch.accessToken`
+ - If both are set, config takes precedence (env fallback is default-account only).
+5. Start the gateway.
+
+**⚠️ Important:** Add access control (`allowFrom` or `allowedRoles`) to prevent unauthorized users from triggering the bot. `requireMention` defaults to `true`.
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ twitch: {
+ enabled: true,
+ username: "openclaw", // Bot's Twitch account
+ accessToken: "oauth:abc123...", // OAuth Access Token (or use OPENCLAW_TWITCH_ACCESS_TOKEN env var)
+ clientId: "xyz789...", // Client ID from Token Generator
+ channel: "vevisk", // Which Twitch channel's chat to join (required)
+ allowFrom: ["123456789"], // (recommended) Your Twitch user ID only - get it from https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/
+ },
+ },
+}
+```
+
+## What it is
+
+- A Twitch channel owned by the Gateway.
+- Deterministic routing: replies always go back to Twitch.
+- Each account maps to an isolated session key `agent::twitch:`.
+- `username` is the bot's account (who authenticates), `channel` is which chat room to join.
+
+## Setup (detailed)
+
+### Generate credentials
+
+Use [Twitch Token Generator](https://twitchtokengenerator.com/):
+
+- Select **Bot Token**
+- Verify scopes `chat:read` and `chat:write` are selected
+- Copy the **Client ID** and **Access Token**
+
+No manual app registration needed. Tokens expire after several hours.
+
+### Configure the bot
+
+**Env var (default account only):**
+
+```bash
+OPENCLAW_TWITCH_ACCESS_TOKEN=oauth:abc123...
+```
+
+**Or config:**
+
+```json5
+{
+ channels: {
+ twitch: {
+ enabled: true,
+ username: "openclaw",
+ accessToken: "oauth:abc123...",
+ clientId: "xyz789...",
+ channel: "vevisk",
+ },
+ },
+}
+```
+
+If both env and config are set, config takes precedence.
+
+### Access control (recommended)
+
+```json5
+{
+ channels: {
+ twitch: {
+ allowFrom: ["123456789"], // (recommended) Your Twitch user ID only
+ },
+ },
+}
+```
+
+Prefer `allowFrom` for a hard allowlist. Use `allowedRoles` instead if you want role-based access.
+
+**Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`.
+
+**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent.
+
+Find your Twitch user ID: https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/ (Convert your Twitch username to ID)
+
+## Token refresh (optional)
+
+Tokens from [Twitch Token Generator](https://twitchtokengenerator.com/) cannot be automatically refreshed - regenerate when expired.
+
+For automatic token refresh, create your own Twitch application at [Twitch Developer Console](https://dev.twitch.tv/console) and add to config:
+
+```json5
+{
+ channels: {
+ twitch: {
+ clientSecret: "your_client_secret",
+ refreshToken: "your_refresh_token",
+ },
+ },
+}
+```
+
+The bot automatically refreshes tokens before expiration and logs refresh events.
+
+## Multi-account support
+
+Use `channels.twitch.accounts` with per-account tokens. See [`gateway/configuration`](/gateway/configuration) for the shared pattern.
+
+Example (one bot account in two channels):
+
+```json5
+{
+ channels: {
+ twitch: {
+ accounts: {
+ channel1: {
+ username: "openclaw",
+ accessToken: "oauth:abc123...",
+ clientId: "xyz789...",
+ channel: "vevisk",
+ },
+ channel2: {
+ username: "openclaw",
+ accessToken: "oauth:def456...",
+ clientId: "uvw012...",
+ channel: "secondchannel",
+ },
+ },
+ },
+ },
+}
+```
+
+**Note:** Each account needs its own token (one token per channel).
+
+## Access control
+
+### Role-based restrictions
+
+```json5
+{
+ channels: {
+ twitch: {
+ accounts: {
+ default: {
+ allowedRoles: ["moderator", "vip"],
+ },
+ },
+ },
+ },
+}
+```
+
+### Allowlist by User ID (most secure)
+
+```json5
+{
+ channels: {
+ twitch: {
+ accounts: {
+ default: {
+ allowFrom: ["123456789", "987654321"],
+ },
+ },
+ },
+ },
+}
+```
+
+### Role-based access (alternative)
+
+`allowFrom` is a hard allowlist. When set, only those user IDs are allowed.
+If you want role-based access, leave `allowFrom` unset and configure `allowedRoles` instead:
+
+```json5
+{
+ channels: {
+ twitch: {
+ accounts: {
+ default: {
+ allowedRoles: ["moderator"],
+ },
+ },
+ },
+ },
+}
+```
+
+### Disable @mention requirement
+
+By default, `requireMention` is `true`. To disable and respond to all messages:
+
+```json5
+{
+ channels: {
+ twitch: {
+ accounts: {
+ default: {
+ requireMention: false,
+ },
+ },
+ },
+ },
+}
+```
+
+## Troubleshooting
+
+First, run diagnostic commands:
+
+```bash
+openclaw doctor
+openclaw channels status --probe
+```
+
+### Bot doesn't respond to messages
+
+**Check access control:** Ensure your user ID is in `allowFrom`, or temporarily remove
+`allowFrom` and set `allowedRoles: ["all"]` to test.
+
+**Check the bot is in the channel:** The bot must join the channel specified in `channel`.
+
+### Token issues
+
+**"Failed to connect" or authentication errors:**
+
+- Verify `accessToken` is the OAuth access token value (typically starts with `oauth:` prefix)
+- Check token has `chat:read` and `chat:write` scopes
+- If using token refresh, verify `clientSecret` and `refreshToken` are set
+
+### Token refresh not working
+
+**Check logs for refresh events:**
+
+```
+Using env token source for mybot
+Access token refreshed for user 123456 (expires in 14400s)
+```
+
+If you see "token refresh disabled (no refresh token)":
+
+- Ensure `clientSecret` is provided
+- Ensure `refreshToken` is provided
+
+## Config
+
+**Account config:**
+
+- `username` - Bot username
+- `accessToken` - OAuth access token with `chat:read` and `chat:write`
+- `clientId` - Twitch Client ID (from Token Generator or your app)
+- `channel` - Channel to join (required)
+- `enabled` - Enable this account (default: `true`)
+- `clientSecret` - Optional: For automatic token refresh
+- `refreshToken` - Optional: For automatic token refresh
+- `expiresIn` - Token expiry in seconds
+- `obtainmentTimestamp` - Token obtained timestamp
+- `allowFrom` - User ID allowlist
+- `allowedRoles` - Role-based access control (`"moderator" | "owner" | "vip" | "subscriber" | "all"`)
+- `requireMention` - Require @mention (default: `true`)
+
+**Provider options:**
+
+- `channels.twitch.enabled` - Enable/disable channel startup
+- `channels.twitch.username` - Bot username (simplified single-account config)
+- `channels.twitch.accessToken` - OAuth access token (simplified single-account config)
+- `channels.twitch.clientId` - Twitch Client ID (simplified single-account config)
+- `channels.twitch.channel` - Channel to join (simplified single-account config)
+- `channels.twitch.accounts.` - Multi-account config (all account fields above)
+
+Full example:
+
+```json5
+{
+ channels: {
+ twitch: {
+ enabled: true,
+ username: "openclaw",
+ accessToken: "oauth:abc123...",
+ clientId: "xyz789...",
+ channel: "vevisk",
+ clientSecret: "secret123...",
+ refreshToken: "refresh456...",
+ allowFrom: ["123456789"],
+ allowedRoles: ["moderator", "vip"],
+ accounts: {
+ default: {
+ username: "mybot",
+ accessToken: "oauth:abc123...",
+ clientId: "xyz789...",
+ channel: "your_channel",
+ enabled: true,
+ clientSecret: "secret123...",
+ refreshToken: "refresh456...",
+ expiresIn: 14400,
+ obtainmentTimestamp: 1706092800000,
+ allowFrom: ["123456789", "987654321"],
+ allowedRoles: ["moderator"],
+ },
+ },
+ },
+ },
+}
+```
+
+## Tool actions
+
+The agent can call `twitch` with action:
+
+- `send` - Send a message to a channel
+
+Example:
+
+```json5
+{
+ action: "twitch",
+ params: {
+ message: "Hello Twitch!",
+ to: "#mychannel",
+ },
+}
+```
+
+## Safety & ops
+
+- **Treat tokens like passwords** - Never commit tokens to git
+- **Use automatic token refresh** for long-running bots
+- **Use user ID allowlists** instead of usernames for access control
+- **Monitor logs** for token refresh events and connection status
+- **Scope tokens minimally** - Only request `chat:read` and `chat:write`
+- **If stuck**: Restart the gateway after confirming no other process owns the session
+
+## Limits
+
+- **500 characters** per message (auto-chunked at word boundaries)
+- Markdown is stripped before chunking
+- No rate limiting (uses Twitch's built-in rate limits)
diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md
new file mode 100644
index 0000000000000000000000000000000000000000..1741ee1b7e0a4688b8efb010e1e367d7b021f909
--- /dev/null
+++ b/docs/channels/whatsapp.md
@@ -0,0 +1,404 @@
+---
+summary: "WhatsApp (web channel) integration: login, inbox, replies, media, and ops"
+read_when:
+ - Working on WhatsApp/web channel behavior or inbox routing
+title: "WhatsApp"
+---
+
+# WhatsApp (web channel)
+
+Status: WhatsApp Web via Baileys only. Gateway owns the session(s).
+
+## Quick setup (beginner)
+
+1. Use a **separate phone number** if possible (recommended).
+2. Configure WhatsApp in `~/.openclaw/openclaw.json`.
+3. Run `openclaw channels login` to scan the QR code (Linked Devices).
+4. Start the gateway.
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ whatsapp: {
+ dmPolicy: "allowlist",
+ allowFrom: ["+15551234567"],
+ },
+ },
+}
+```
+
+## Goals
+
+- Multiple WhatsApp accounts (multi-account) in one Gateway process.
+- Deterministic routing: replies return to WhatsApp, no model routing.
+- Model sees enough context to understand quoted replies.
+
+## Config writes
+
+By default, WhatsApp is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
+
+Disable with:
+
+```json5
+{
+ channels: { whatsapp: { configWrites: false } },
+}
+```
+
+## Architecture (who owns what)
+
+- **Gateway** owns the Baileys socket and inbox loop.
+- **CLI / macOS app** talk to the gateway; no direct Baileys use.
+- **Active listener** is required for outbound sends; otherwise send fails fast.
+
+## Getting a phone number (two modes)
+
+WhatsApp requires a real mobile number for verification. VoIP and virtual numbers are usually blocked. There are two supported ways to run OpenClaw on WhatsApp:
+
+### Dedicated number (recommended)
+
+Use a **separate phone number** for OpenClaw. Best UX, clean routing, no self-chat quirks. Ideal setup: **spare/old Android phone + eSIM**. Leave it on Wi‑Fi and power, and link it via QR.
+
+**WhatsApp Business:** You can use WhatsApp Business on the same device with a different number. Great for keeping your personal WhatsApp separate — install WhatsApp Business and register the OpenClaw number there.
+
+**Sample config (dedicated number, single-user allowlist):**
+
+```json5
+{
+ channels: {
+ whatsapp: {
+ dmPolicy: "allowlist",
+ allowFrom: ["+15551234567"],
+ },
+ },
+}
+```
+
+**Pairing mode (optional):**
+If you want pairing instead of allowlist, set `channels.whatsapp.dmPolicy` to `pairing`. Unknown senders get a pairing code; approve with:
+`openclaw pairing approve whatsapp `
+
+### Personal number (fallback)
+
+Quick fallback: run OpenClaw on **your own number**. Message yourself (WhatsApp “Message yourself”) for testing so you don’t spam contacts. Expect to read verification codes on your main phone during setup and experiments. **Must enable self-chat mode.**
+When the wizard asks for your personal WhatsApp number, enter the phone you will message from (the owner/sender), not the assistant number.
+
+**Sample config (personal number, self-chat):**
+
+```json
+{
+ "whatsapp": {
+ "selfChatMode": true,
+ "dmPolicy": "allowlist",
+ "allowFrom": ["+15551234567"]
+ }
+}
+```
+
+Self-chat replies default to `[{identity.name}]` when set (otherwise `[openclaw]`)
+if `messages.responsePrefix` is unset. Set it explicitly to customize or disable
+the prefix (use `""` to remove it).
+
+### Number sourcing tips
+
+- **Local eSIM** from your country's mobile carrier (most reliable)
+ - Austria: [hot.at](https://www.hot.at)
+ - UK: [giffgaff](https://www.giffgaff.com) — free SIM, no contract
+- **Prepaid SIM** — cheap, just needs to receive one SMS for verification
+
+**Avoid:** TextNow, Google Voice, most "free SMS" services — WhatsApp blocks these aggressively.
+
+**Tip:** The number only needs to receive one verification SMS. After that, WhatsApp Web sessions persist via `creds.json`.
+
+## Why Not Twilio?
+
+- Early OpenClaw builds supported Twilio’s WhatsApp Business integration.
+- WhatsApp Business numbers are a poor fit for a personal assistant.
+- Meta enforces a 24‑hour reply window; if you haven’t responded in the last 24 hours, the business number can’t initiate new messages.
+- High-volume or “chatty” usage triggers aggressive blocking, because business accounts aren’t meant to send dozens of personal assistant messages.
+- Result: unreliable delivery and frequent blocks, so support was removed.
+
+## Login + credentials
+
+- Login command: `openclaw channels login` (QR via Linked Devices).
+- Multi-account login: `openclaw channels login --account ` (`` = `accountId`).
+- Default account (when `--account` is omitted): `default` if present, otherwise the first configured account id (sorted).
+- Credentials stored in `~/.openclaw/credentials/whatsapp//creds.json`.
+- Backup copy at `creds.json.bak` (restored on corruption).
+- Legacy compatibility: older installs stored Baileys files directly in `~/.openclaw/credentials/`.
+- Logout: `openclaw channels logout` (or `--account `) deletes WhatsApp auth state (but keeps shared `oauth.json`).
+- Logged-out socket => error instructs re-link.
+
+## Inbound flow (DM + group)
+
+- WhatsApp events come from `messages.upsert` (Baileys).
+- Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts.
+- Status/broadcast chats are ignored.
+- Direct chats use E.164; groups use group JID.
+- **DM policy**: `channels.whatsapp.dmPolicy` controls direct chat access (default: `pairing`).
+ - Pairing: unknown senders get a pairing code (approve via `openclaw pairing approve whatsapp `; codes expire after 1 hour).
+ - Open: requires `channels.whatsapp.allowFrom` to include `"*"`.
+ - Your linked WhatsApp number is implicitly trusted, so self messages skip `channels.whatsapp.dmPolicy` and `channels.whatsapp.allowFrom` checks.
+
+### Personal-number mode (fallback)
+
+If you run OpenClaw on your **personal WhatsApp number**, enable `channels.whatsapp.selfChatMode` (see sample above).
+
+Behavior:
+
+- Outbound DMs never trigger pairing replies (prevents spamming contacts).
+- Inbound unknown senders still follow `channels.whatsapp.dmPolicy`.
+- Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs.
+- Read receipts sent for non-self-chat DMs.
+
+## Read receipts
+
+By default, the gateway marks inbound WhatsApp messages as read (blue ticks) once they are accepted.
+
+Disable globally:
+
+```json5
+{
+ channels: { whatsapp: { sendReadReceipts: false } },
+}
+```
+
+Disable per account:
+
+```json5
+{
+ channels: {
+ whatsapp: {
+ accounts: {
+ personal: { sendReadReceipts: false },
+ },
+ },
+ },
+}
+```
+
+Notes:
+
+- Self-chat mode always skips read receipts.
+
+## WhatsApp FAQ: sending messages + pairing
+
+**Will OpenClaw message random contacts when I link WhatsApp?**
+No. Default DM policy is **pairing**, so unknown senders only get a pairing code and their message is **not processed**. OpenClaw only replies to chats it receives, or to sends you explicitly trigger (agent/CLI).
+
+**How does pairing work on WhatsApp?**
+Pairing is a DM gate for unknown senders:
+
+- First DM from a new sender returns a short code (message is not processed).
+- Approve with: `openclaw pairing approve whatsapp ` (list with `openclaw pairing list whatsapp`).
+- Codes expire after 1 hour; pending requests are capped at 3 per channel.
+
+**Can multiple people use different OpenClaw instances on one WhatsApp number?**
+Yes, by routing each sender to a different agent via `bindings` (peer `kind: "dm"`, sender E.164 like `+15551234567`). Replies still come from the **same WhatsApp account**, and direct chats collapse to each agent’s main session, so use **one agent per person**. DM access control (`dmPolicy`/`allowFrom`) is global per WhatsApp account. See [Multi-Agent Routing](/concepts/multi-agent).
+
+**Why do you ask for my phone number in the wizard?**
+The wizard uses it to set your **allowlist/owner** so your own DMs are permitted. It’s not used for auto-sending. If you run on your personal WhatsApp number, use that same number and enable `channels.whatsapp.selfChatMode`.
+
+## Message normalization (what the model sees)
+
+- `Body` is the current message body with envelope.
+- Quoted reply context is **always appended**:
+ ```
+ [Replying to +1555 id:ABC123]
+ >
+ [/Replying]
+ ```
+- Reply metadata also set:
+ - `ReplyToId` = stanzaId
+ - `ReplyToBody` = quoted body or media placeholder
+ - `ReplyToSender` = E.164 when known
+- Media-only inbound messages use placeholders:
+ - ``
+
+## Groups
+
+- Groups map to `agent::whatsapp:group:` sessions.
+- Group policy: `channels.whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`).
+- Activation modes:
+ - `mention` (default): requires @mention or regex match.
+ - `always`: always triggers.
+- `/activation mention|always` is owner-only and must be sent as a standalone message.
+- Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset).
+- **History injection** (pending-only):
+ - Recent _unprocessed_ messages (default 50) inserted under:
+ `[Chat messages since your last reply - for context]` (messages already in the session are not re-injected)
+ - Current message under:
+ `[Current message - respond to this]`
+ - Sender suffix appended: `[from: Name (+E164)]`
+- Group metadata cached 5 min (subject + participants).
+
+## Reply delivery (threading)
+
+- WhatsApp Web sends standard messages (no quoted reply threading in the current gateway).
+- Reply tags are ignored on this channel.
+
+## Acknowledgment reactions (auto-react on receipt)
+
+WhatsApp can automatically send emoji reactions to incoming messages immediately upon receipt, before the bot generates a reply. This provides instant feedback to users that their message was received.
+
+**Configuration:**
+
+```json
+{
+ "whatsapp": {
+ "ackReaction": {
+ "emoji": "👀",
+ "direct": true,
+ "group": "mentions"
+ }
+ }
+}
+```
+
+**Options:**
+
+- `emoji` (string): Emoji to use for acknowledgment (e.g., "👀", "✅", "📨"). Empty or omitted = feature disabled.
+- `direct` (boolean, default: `true`): Send reactions in direct/DM chats.
+- `group` (string, default: `"mentions"`): Group chat behavior:
+ - `"always"`: React to all group messages (even without @mention)
+ - `"mentions"`: React only when bot is @mentioned
+ - `"never"`: Never react in groups
+
+**Per-account override:**
+
+```json
+{
+ "whatsapp": {
+ "accounts": {
+ "work": {
+ "ackReaction": {
+ "emoji": "✅",
+ "direct": false,
+ "group": "always"
+ }
+ }
+ }
+ }
+}
+```
+
+**Behavior notes:**
+
+- Reactions are sent **immediately** upon message receipt, before typing indicators or bot replies.
+- In groups with `requireMention: false` (activation: always), `group: "mentions"` will react to all messages (not just @mentions).
+- Fire-and-forget: reaction failures are logged but don't prevent the bot from replying.
+- Participant JID is automatically included for group reactions.
+- WhatsApp ignores `messages.ackReaction`; use `channels.whatsapp.ackReaction` instead.
+
+## Agent tool (reactions)
+
+- Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`).
+- Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account).
+- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
+- Tool gating: `channels.whatsapp.actions.reactions` (default: enabled).
+
+## Limits
+
+- Outbound text is chunked to `channels.whatsapp.textChunkLimit` (default 4000).
+- Optional newline chunking: set `channels.whatsapp.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
+- Inbound media saves are capped by `channels.whatsapp.mediaMaxMb` (default 50 MB).
+- Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB).
+
+## Outbound send (text + media)
+
+- Uses active web listener; error if gateway not running.
+- Text chunking: 4k max per message (configurable via `channels.whatsapp.textChunkLimit`, optional `channels.whatsapp.chunkMode`).
+- Media:
+ - Image/video/audio/document supported.
+ - Audio sent as PTT; `audio/ogg` => `audio/ogg; codecs=opus`.
+ - Caption only on first media item.
+ - Media fetch supports HTTP(S) and local paths.
+ - Animated GIFs: WhatsApp expects MP4 with `gifPlayback: true` for inline looping.
+ - CLI: `openclaw message send --media --gif-playback`
+ - Gateway: `send` params include `gifPlayback: true`
+
+## Voice notes (PTT audio)
+
+WhatsApp sends audio as **voice notes** (PTT bubble).
+
+- Best results: OGG/Opus. OpenClaw rewrites `audio/ogg` to `audio/ogg; codecs=opus`.
+- `[[audio_as_voice]]` is ignored for WhatsApp (audio already ships as voice note).
+
+## Media limits + optimization
+
+- Default outbound cap: 5 MB (per media item).
+- Override: `agents.defaults.mediaMaxMb`.
+- Images are auto-optimized to JPEG under cap (resize + quality sweep).
+- Oversize media => error; media reply falls back to text warning.
+
+## Heartbeats
+
+- **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s).
+- **Agent heartbeat** can be configured per agent (`agents.list[].heartbeat`) or globally
+ via `agents.defaults.heartbeat` (fallback when no per-agent entries are set).
+ - Uses the configured heartbeat prompt (default: `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`) + `HEARTBEAT_OK` skip behavior.
+ - Delivery defaults to the last used channel (or configured target).
+
+## Reconnect behavior
+
+- Backoff policy: `web.reconnect`:
+ - `initialMs`, `maxMs`, `factor`, `jitter`, `maxAttempts`.
+- If maxAttempts reached, web monitoring stops (degraded).
+- Logged-out => stop and require re-link.
+
+## Config quick map
+
+- `channels.whatsapp.dmPolicy` (DM policy: pairing/allowlist/open/disabled).
+- `channels.whatsapp.selfChatMode` (same-phone setup; bot uses your personal WhatsApp number).
+- `channels.whatsapp.allowFrom` (DM allowlist). WhatsApp uses E.164 phone numbers (no usernames).
+- `channels.whatsapp.mediaMaxMb` (inbound media save cap).
+- `channels.whatsapp.ackReaction` (auto-reaction on message receipt: `{emoji, direct, group}`).
+- `channels.whatsapp.accounts..*` (per-account settings + optional `authDir`).
+- `channels.whatsapp.accounts..mediaMaxMb` (per-account inbound media cap).
+- `channels.whatsapp.accounts..ackReaction` (per-account ack reaction override).
+- `channels.whatsapp.groupAllowFrom` (group sender allowlist).
+- `channels.whatsapp.groupPolicy` (group policy).
+- `channels.whatsapp.historyLimit` / `channels.whatsapp.accounts..historyLimit` (group history context; `0` disables).
+- `channels.whatsapp.dmHistoryLimit` (DM history limit in user turns). Per-user overrides: `channels.whatsapp.dms[""].historyLimit`.
+- `channels.whatsapp.groups` (group allowlist + mention gating defaults; use `"*"` to allow all)
+- `channels.whatsapp.actions.reactions` (gate WhatsApp tool reactions).
+- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`)
+- `messages.groupChat.historyLimit`
+- `channels.whatsapp.messagePrefix` (inbound prefix; per-account: `channels.whatsapp.accounts..messagePrefix`; deprecated: `messages.messagePrefix`)
+- `messages.responsePrefix` (outbound prefix)
+- `agents.defaults.mediaMaxMb`
+- `agents.defaults.heartbeat.every`
+- `agents.defaults.heartbeat.model` (optional override)
+- `agents.defaults.heartbeat.target`
+- `agents.defaults.heartbeat.to`
+- `agents.defaults.heartbeat.session`
+- `agents.list[].heartbeat.*` (per-agent overrides)
+- `session.*` (scope, idle, store, mainKey)
+- `web.enabled` (disable channel startup when false)
+- `web.heartbeatSeconds`
+- `web.reconnect.*`
+
+## Logs + troubleshooting
+
+- Subsystems: `whatsapp/inbound`, `whatsapp/outbound`, `web-heartbeat`, `web-reconnect`.
+- Log file: `/tmp/openclaw/openclaw-YYYY-MM-DD.log` (configurable).
+- Troubleshooting guide: [Gateway troubleshooting](/gateway/troubleshooting).
+
+## Troubleshooting (quick)
+
+**Not linked / QR login required**
+
+- Symptom: `channels status` shows `linked: false` or warns “Not linked”.
+- Fix: run `openclaw channels login` on the gateway host and scan the QR (WhatsApp → Settings → Linked Devices).
+
+**Linked but disconnected / reconnect loop**
+
+- Symptom: `channels status` shows `running, disconnected` or warns “Linked but disconnected”.
+- Fix: `openclaw doctor` (or restart the gateway). If it persists, relink via `channels login` and inspect `openclaw logs --follow`.
+
+**Bun runtime**
+
+- Bun is **not recommended**. WhatsApp (Baileys) and Telegram are unreliable on Bun.
+ Run the gateway with **Node**. (See Getting Started runtime note.)
diff --git a/docs/channels/zalo.md b/docs/channels/zalo.md
new file mode 100644
index 0000000000000000000000000000000000000000..0f247190c36b749cb9c875cb7a0898d2bf6850ab
--- /dev/null
+++ b/docs/channels/zalo.md
@@ -0,0 +1,189 @@
+---
+summary: "Zalo bot support status, capabilities, and configuration"
+read_when:
+ - Working on Zalo features or webhooks
+title: "Zalo"
+---
+
+# Zalo (Bot API)
+
+Status: experimental. Direct messages only; groups coming soon per Zalo docs.
+
+## Plugin required
+
+Zalo ships as a plugin and is not bundled with the core install.
+
+- Install via CLI: `openclaw plugins install @openclaw/zalo`
+- Or select **Zalo** during onboarding and confirm the install prompt
+- Details: [Plugins](/plugin)
+
+## Quick setup (beginner)
+
+1. Install the Zalo plugin:
+ - From a source checkout: `openclaw plugins install ./extensions/zalo`
+ - From npm (if published): `openclaw plugins install @openclaw/zalo`
+ - Or pick **Zalo** in onboarding and confirm the install prompt
+2. Set the token:
+ - Env: `ZALO_BOT_TOKEN=...`
+ - Or config: `channels.zalo.botToken: "..."`.
+3. Restart the gateway (or finish onboarding).
+4. DM access is pairing by default; approve the pairing code on first contact.
+
+Minimal config:
+
+```json5
+{
+ channels: {
+ zalo: {
+ enabled: true,
+ botToken: "12345689:abc-xyz",
+ dmPolicy: "pairing",
+ },
+ },
+}
+```
+
+## What it is
+
+Zalo is a Vietnam-focused messaging app; its Bot API lets the Gateway run a bot for 1:1 conversations.
+It is a good fit for support or notifications where you want deterministic routing back to Zalo.
+
+- A Zalo Bot API channel owned by the Gateway.
+- Deterministic routing: replies go back to Zalo; the model never chooses channels.
+- DMs share the agent's main session.
+- Groups are not yet supported (Zalo docs state "coming soon").
+
+## Setup (fast path)
+
+### 1) Create a bot token (Zalo Bot Platform)
+
+1. Go to **https://bot.zaloplatforms.com** and sign in.
+2. Create a new bot and configure its settings.
+3. Copy the bot token (format: `12345689:abc-xyz`).
+
+### 2) Configure the token (env or config)
+
+Example:
+
+```json5
+{
+ channels: {
+ zalo: {
+ enabled: true,
+ botToken: "12345689:abc-xyz",
+ dmPolicy: "pairing",
+ },
+ },
+}
+```
+
+Env option: `ZALO_BOT_TOKEN=...` (works for the default account only).
+
+Multi-account support: use `channels.zalo.accounts` with per-account tokens and optional `name`.
+
+3. Restart the gateway. Zalo starts when a token is resolved (env or config).
+4. DM access defaults to pairing. Approve the code when the bot is first contacted.
+
+## How it works (behavior)
+
+- Inbound messages are normalized into the shared channel envelope with media placeholders.
+- Replies always route back to the same Zalo chat.
+- Long-polling by default; webhook mode available with `channels.zalo.webhookUrl`.
+
+## Limits
+
+- Outbound text is chunked to 2000 characters (Zalo API limit).
+- Media downloads/uploads are capped by `channels.zalo.mediaMaxMb` (default 5).
+- Streaming is blocked by default due to the 2000 char limit making streaming less useful.
+
+## Access control (DMs)
+
+### DM access
+
+- Default: `channels.zalo.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
+- Approve via:
+ - `openclaw pairing list zalo`
+ - `openclaw pairing approve zalo `
+- Pairing is the default token exchange. Details: [Pairing](/start/pairing)
+- `channels.zalo.allowFrom` accepts numeric user IDs (no username lookup available).
+
+## Long-polling vs webhook
+
+- Default: long-polling (no public URL required).
+- Webhook mode: set `channels.zalo.webhookUrl` and `channels.zalo.webhookSecret`.
+ - The webhook secret must be 8-256 characters.
+ - Webhook URL must use HTTPS.
+ - Zalo sends events with `X-Bot-Api-Secret-Token` header for verification.
+ - Gateway HTTP handles webhook requests at `channels.zalo.webhookPath` (defaults to the webhook URL path).
+
+**Note:** getUpdates (polling) and webhook are mutually exclusive per Zalo API docs.
+
+## Supported message types
+
+- **Text messages**: Full support with 2000 character chunking.
+- **Image messages**: Download and process inbound images; send images via `sendPhoto`.
+- **Stickers**: Logged but not fully processed (no agent response).
+- **Unsupported types**: Logged (e.g., messages from protected users).
+
+## Capabilities
+
+| Feature | Status |
+| --------------- | ------------------------------ |
+| Direct messages | ✅ Supported |
+| Groups | ❌ Coming soon (per Zalo docs) |
+| Media (images) | ✅ Supported |
+| Reactions | ❌ Not supported |
+| Threads | ❌ Not supported |
+| Polls | ❌ Not supported |
+| Native commands | ❌ Not supported |
+| Streaming | ⚠️ Blocked (2000 char limit) |
+
+## Delivery targets (CLI/cron)
+
+- Use a chat id as the target.
+- Example: `openclaw message send --channel zalo --target 123456789 --message "hi"`.
+
+## Troubleshooting
+
+**Bot doesn't respond:**
+
+- Check that the token is valid: `openclaw channels status --probe`
+- Verify the sender is approved (pairing or allowFrom)
+- Check gateway logs: `openclaw logs --follow`
+
+**Webhook not receiving events:**
+
+- Ensure webhook URL uses HTTPS
+- Verify secret token is 8-256 characters
+- Confirm the gateway HTTP endpoint is reachable on the configured path
+- Check that getUpdates polling is not running (they're mutually exclusive)
+
+## Configuration reference (Zalo)
+
+Full configuration: [Configuration](/gateway/configuration)
+
+Provider options:
+
+- `channels.zalo.enabled`: enable/disable channel startup.
+- `channels.zalo.botToken`: bot token from Zalo Bot Platform.
+- `channels.zalo.tokenFile`: read token from file path.
+- `channels.zalo.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
+- `channels.zalo.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. The wizard will ask for numeric IDs.
+- `channels.zalo.mediaMaxMb`: inbound/outbound media cap (MB, default 5).
+- `channels.zalo.webhookUrl`: enable webhook mode (HTTPS required).
+- `channels.zalo.webhookSecret`: webhook secret (8-256 chars).
+- `channels.zalo.webhookPath`: webhook path on the gateway HTTP server.
+- `channels.zalo.proxy`: proxy URL for API requests.
+
+Multi-account options:
+
+- `channels.zalo.accounts..botToken`: per-account token.
+- `channels.zalo.accounts..tokenFile`: per-account token file.
+- `channels.zalo.accounts..name`: display name.
+- `channels.zalo.accounts..enabled`: enable/disable account.
+- `channels.zalo.accounts..dmPolicy`: per-account DM policy.
+- `channels.zalo.accounts..allowFrom`: per-account allowlist.
+- `channels.zalo.accounts..webhookUrl`: per-account webhook URL.
+- `channels.zalo.accounts..webhookSecret`: per-account webhook secret.
+- `channels.zalo.accounts..webhookPath`: per-account webhook path.
+- `channels.zalo.accounts..proxy`: per-account proxy URL.
diff --git a/docs/channels/zalouser.md b/docs/channels/zalouser.md
new file mode 100644
index 0000000000000000000000000000000000000000..5a1b555b82f08bc1140e5eef0e0407096a3dbd98
--- /dev/null
+++ b/docs/channels/zalouser.md
@@ -0,0 +1,140 @@
+---
+summary: "Zalo personal account support via zca-cli (QR login), capabilities, and configuration"
+read_when:
+ - Setting up Zalo Personal for OpenClaw
+ - Debugging Zalo Personal login or message flow
+title: "Zalo Personal"
+---
+
+# Zalo Personal (unofficial)
+
+Status: experimental. This integration automates a **personal Zalo account** via `zca-cli`.
+
+> **Warning:** This is an unofficial integration and may result in account suspension/ban. Use at your own risk.
+
+## Plugin required
+
+Zalo Personal ships as a plugin and is not bundled with the core install.
+
+- Install via CLI: `openclaw plugins install @openclaw/zalouser`
+- Or from a source checkout: `openclaw plugins install ./extensions/zalouser`
+- Details: [Plugins](/plugin)
+
+## Prerequisite: zca-cli
+
+The Gateway machine must have the `zca` binary available in `PATH`.
+
+- Verify: `zca --version`
+- If missing, install zca-cli (see `extensions/zalouser/README.md` or the upstream zca-cli docs).
+
+## Quick setup (beginner)
+
+1. Install the plugin (see above).
+2. Login (QR, on the Gateway machine):
+ - `openclaw channels login --channel zalouser`
+ - Scan the QR code in the terminal with the Zalo mobile app.
+3. Enable the channel:
+
+```json5
+{
+ channels: {
+ zalouser: {
+ enabled: true,
+ dmPolicy: "pairing",
+ },
+ },
+}
+```
+
+4. Restart the Gateway (or finish onboarding).
+5. DM access defaults to pairing; approve the pairing code on first contact.
+
+## What it is
+
+- Uses `zca listen` to receive inbound messages.
+- Uses `zca msg ...` to send replies (text/media/link).
+- Designed for “personal account” use cases where Zalo Bot API is not available.
+
+## Naming
+
+Channel id is `zalouser` to make it explicit this automates a **personal Zalo user account** (unofficial). We keep `zalo` reserved for a potential future official Zalo API integration.
+
+## Finding IDs (directory)
+
+Use the directory CLI to discover peers/groups and their IDs:
+
+```bash
+openclaw directory self --channel zalouser
+openclaw directory peers list --channel zalouser --query "name"
+openclaw directory groups list --channel zalouser --query "work"
+```
+
+## Limits
+
+- Outbound text is chunked to ~2000 characters (Zalo client limits).
+- Streaming is blocked by default.
+
+## Access control (DMs)
+
+`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
+`channels.zalouser.allowFrom` accepts user IDs or names. The wizard resolves names to IDs via `zca friend find` when available.
+
+Approve via:
+
+- `openclaw pairing list zalouser`
+- `openclaw pairing approve zalouser `
+
+## Group access (optional)
+
+- Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to override the default when unset.
+- Restrict to an allowlist with:
+ - `channels.zalouser.groupPolicy = "allowlist"`
+ - `channels.zalouser.groups` (keys are group IDs or names)
+- Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
+- The configure wizard can prompt for group allowlists.
+- On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed.
+
+Example:
+
+```json5
+{
+ channels: {
+ zalouser: {
+ groupPolicy: "allowlist",
+ groups: {
+ "123456789": { allow: true },
+ "Work Chat": { allow: true },
+ },
+ },
+ },
+}
+```
+
+## Multi-account
+
+Accounts map to zca profiles. Example:
+
+```json5
+{
+ channels: {
+ zalouser: {
+ enabled: true,
+ defaultAccount: "default",
+ accounts: {
+ work: { enabled: true, profile: "work" },
+ },
+ },
+ },
+}
+```
+
+## Troubleshooting
+
+**`zca` not found:**
+
+- Install zca-cli and ensure it’s on `PATH` for the Gateway process.
+
+**Login doesn’t stick:**
+
+- `openclaw channels status --probe`
+- Re-login: `openclaw channels logout --channel zalouser && openclaw channels login --channel zalouser`
diff --git a/docs/cli/acp.md b/docs/cli/acp.md
new file mode 100644
index 0000000000000000000000000000000000000000..46b78cce6f51d654b6507061a0a09d05531a547d
--- /dev/null
+++ b/docs/cli/acp.md
@@ -0,0 +1,170 @@
+---
+summary: "Run the ACP bridge for IDE integrations"
+read_when:
+ - Setting up ACP-based IDE integrations
+ - Debugging ACP session routing to the Gateway
+title: "acp"
+---
+
+# acp
+
+Run the ACP (Agent Client Protocol) bridge that talks to a OpenClaw Gateway.
+
+This command speaks ACP over stdio for IDEs and forwards prompts to the Gateway
+over WebSocket. It keeps ACP sessions mapped to Gateway session keys.
+
+## Usage
+
+```bash
+openclaw acp
+
+# Remote Gateway
+openclaw acp --url wss://gateway-host:18789 --token
+
+# Attach to an existing session key
+openclaw acp --session agent:main:main
+
+# Attach by label (must already exist)
+openclaw acp --session-label "support inbox"
+
+# Reset the session key before the first prompt
+openclaw acp --session agent:main:main --reset-session
+```
+
+## ACP client (debug)
+
+Use the built-in ACP client to sanity-check the bridge without an IDE.
+It spawns the ACP bridge and lets you type prompts interactively.
+
+```bash
+openclaw acp client
+
+# Point the spawned bridge at a remote Gateway
+openclaw acp client --server-args --url wss://gateway-host:18789 --token
+
+# Override the server command (default: openclaw)
+openclaw acp client --server "node" --server-args openclaw.mjs acp --url ws://127.0.0.1:19001
+```
+
+## How to use this
+
+Use ACP when an IDE (or other client) speaks Agent Client Protocol and you want
+it to drive a OpenClaw Gateway session.
+
+1. Ensure the Gateway is running (local or remote).
+2. Configure the Gateway target (config or flags).
+3. Point your IDE to run `openclaw acp` over stdio.
+
+Example config (persisted):
+
+```bash
+openclaw config set gateway.remote.url wss://gateway-host:18789
+openclaw config set gateway.remote.token
+```
+
+Example direct run (no config write):
+
+```bash
+openclaw acp --url wss://gateway-host:18789 --token
+```
+
+## Selecting agents
+
+ACP does not pick agents directly. It routes by the Gateway session key.
+
+Use agent-scoped session keys to target a specific agent:
+
+```bash
+openclaw acp --session agent:main:main
+openclaw acp --session agent:design:main
+openclaw acp --session agent:qa:bug-123
+```
+
+Each ACP session maps to a single Gateway session key. One agent can have many
+sessions; ACP defaults to an isolated `acp:` session unless you override
+the key or label.
+
+## Zed editor setup
+
+Add a custom ACP agent in `~/.config/zed/settings.json` (or use Zed’s Settings UI):
+
+```json
+{
+ "agent_servers": {
+ "OpenClaw ACP": {
+ "type": "custom",
+ "command": "openclaw",
+ "args": ["acp"],
+ "env": {}
+ }
+ }
+}
+```
+
+To target a specific Gateway or agent:
+
+```json
+{
+ "agent_servers": {
+ "OpenClaw ACP": {
+ "type": "custom",
+ "command": "openclaw",
+ "args": [
+ "acp",
+ "--url",
+ "wss://gateway-host:18789",
+ "--token",
+ "