openclaw/docs/cli/message.md
Felix Krause b48134acc8
fix(cli): retain every repeated message media attachment (#151043)
## What Problem This Solves

Fixes `openclaw message send --media first.png --media second.png` silently dropping the first attachment. The CLI previously overwrote each earlier `--media` value before the shared send action could see it.

## User Impact

Repeated media flags now reach the existing send pipeline in their original order. Single-file and text-only sends are unchanged. Telegram uses its existing photo-album support; existing duplicate removal and caption behavior remain unchanged. This does not add a channel capability, alter message receipts, or resolve the broader album feature requests #13620 and #135545.

## Why This Change Was Made

The command reuses the existing repeated-option collector and passes its ordered list to the shared `mediaUrls` contract. The contributor's production repair and commits are preserved. Regression coverage is consolidated in the existing actual-CLI process suite rather than a new test that only spies on argument forwarding.

## Evidence

- The actual CLI regression failed on pinned main: serialized dry-run output contained only `second.png` instead of both files. The repaired process case passes; all 64 focused CLI process/helper tests pass. Scoped lint, formatting, both line ratchets, and runtime builds pass.
- Five network-denied actual-CLI dry-run cases per phase covered two files, one file, text only, repeated duplicates, and three media flags mixed with the message option.
- Ran both phases through the actual CLI, an isolated Gateway, the registered Telegram adapter, Telegram Test Server, and a separately authenticated QA user under the maintained credential lease.
- Native recipient observations confirmed the baseline delivered only the last photo. The candidate delivered red then blue in one two-photo album, preserved the single red photo and text-only controls, kept red/blue order after duplicate removal, and delivered red/blue/green in one three-photo album.
- Inspected the actual recipient minithumbnail bytes and native album/message ordering, not reconstructed chat images. This is attachment-delivery evidence, not a Telegram client layout or image-quality claim.
- Both native recording runs completed and removed credential/Gateway scratch. Synthetic Test Server DM messages remain as test evidence; no deletion is claimed. Baseline had no model calls; the candidate had two synthetic-provider calls independently attributable to an autonomous heartbeat and its recap, not media sends. No paid inference was used.

All three final changed files match the tested current-main replay. The change is confined to CLI collection, its process regression, and CLI documentation; no schema, permission, dependency, provider, or UI-rendering changes are included.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-09-20 15:54:37 +05:30

19 KiB

summary read_when title
CLI reference for `openclaw message` (send + channel actions)
Adding or modifying message CLI actions
Changing outbound channel behavior
Message

openclaw message

Single outbound command for sending messages and channel actions across Discord, Google Chat, iMessage, Matrix, Mattermost (plugin), Microsoft Teams, Signal, Slack, Telegram, and WhatsApp.

openclaw message <subcommand> [flags]

Channel selection

  • --channel <name> is required if more than one channel is configured; with exactly one channel configured, that channel is the default.
  • Values: discord|googlechat|imessage|matrix|mattermost|msteams|signal|slack|telegram|whatsapp (Mattermost requires the plugin).
  • Channel-prefixed targets (for example discord:channel:123) resolve the owning plugin without an explicit --channel.

Agent ownership

openclaw message uses the configured System Agent as its agent owner, falling back to a retained legacy owner or the sole configured agent when the System Agent is unset.

In an explicit multi-agent configuration without an owner, the command stops before sending. Choose an existing agent ID from openclaw agents list, set it as the System Agent, then retry:

openclaw config set agents.defaults.systemAgent.agentId <id>

This setting also selects the owner for other ambient system work. The message command does not accept --agent; --channel and --account select the channel and channel account.

Target formats (-t, --target)

Channel Format
Discord channel:<id>, user:<id>, <@id> mention, or a bare numeric id (treated as a channel id)
Google Chat spaces/<spaceId> or users/<userId>
iMessage handle, chat_id:<id>, chat_guid:<guid>, or chat_identifier:<id>
Mattermost (plugin) channel:<id>, user:<id>, @username, or a bare id (treated as a channel)
Matrix @user:server, !room:server, or #alias:server
Microsoft Teams conversation:<id> (19:...@thread.tacv2), a bare conversation id, or user:<aad-object-id>
Signal +E.164, group:<id>, uuid:<id>, username:<name>/u:<name>, or any of these prefixed with signal:
Slack channel:<id> or user:<id> (a bare id is treated as a channel)
Telegram chat id, @username, or a forum topic target: <chatId>:topic:<topicId> (or --thread-id <topicId>)
WhatsApp E.164, group JID (...@g.us), or Channel/Newsletter JID (...@newsletter)

Channel name lookup: for providers with a directory (Discord/Slack/etc), names like Help or #help resolve via the directory cache, falling back to a live directory lookup on a cache miss where the provider supports it.

Common flags

Every action accepts: --channel <name>, --account <id>, --json, --dry-run, --verbose. Actions that take a destination also accept -t, --target <dest>.

An explicitly empty or whitespace-only --account value is rejected. Omit the option to use the existing default or bound account, including when a shell variable is empty. Nonblank account values keep their existing selection rules.

An explicitly empty or whitespace-only --channel value is also rejected. Omit the option to select the sole configured channel, or use a channel-prefixed target when supported.

Discord message bodies, captions, poll context, and component text retain leading indentation. Existing empty-message validation still applies. Ordinary message and caption delivery still trims trailing whitespace.

Local message actions run the loaded plugins' shutdown hooks before exiting, including after an action fails. Cleanup has a 2.5-second overall budget and does not change the action's exit status. message read skips these shutdown hooks.

SecretRef resolution

openclaw message resolves channel SecretRefs before running the action, scoped as narrowly as possible:

  • channel-scoped when --channel is set (or inferred from a prefixed target)
  • account-scoped when --account is also set
  • all configured channels when neither is set

Unresolved SecretRefs on unrelated channels never block a targeted action; an unresolved SecretRef on the selected channel/account fails the action closed.

Actions

Core

Action Channels Required Notes
send Discord, Google Chat, iMessage, Matrix, Mattermost (plugin), Microsoft Teams, Signal, Slack, Telegram, WhatsApp --target, plus one of --message/--media/--presentation See Send below.
poll Discord, Matrix, Microsoft Teams, Telegram, WhatsApp --target, --poll-question, --poll-option (repeat) See Poll below.
react Discord, Matrix, Nextcloud Talk, Signal, Slack, Telegram, WhatsApp --message-id, --target --emoji, --remove (needs --emoji; omit it to clear own reactions where supported, see Reactions). WhatsApp: --participant, --from-me. Signal group reactions require --target-author or --target-author-uuid. Nextcloud Talk only adds reactions; --remove errors.
reactions Discord, Matrix, Microsoft Teams, Slack --message-id, --target --limit.
read Discord, Matrix, Microsoft Teams, Slack --target --limit, --message-id, --before, --after. Discord: --around. Slack: --message-id reads a specific timestamp, combine with --thread-id for an exact thread reply.
edit Discord, Matrix, Microsoft Teams, Slack, Telegram --message-id, --message, --target Telegram forum threads use --thread-id.
delete Discord, Matrix, Microsoft Teams, Slack, Telegram --message-id, --target
pin / unpin Discord, Matrix, Microsoft Teams, Slack --message-id, --target unpin also accepts --pinned-message-id (Microsoft Teams: the pin/list-pins resource id, not the chat message id).
pins (list) Discord, Matrix, Microsoft Teams, Slack --target --limit.
permissions Discord, Matrix --target Matrix: available only when encryption is enabled and verification actions are allowed.
search Discord, Microsoft Teams --query --guild-id (Discord; resolved from --channel-id when omitted), --channel-id (required for Microsoft Teams as Graph <team-id>/<channel-id>), --channel-ids (repeat), --author-id, --author-ids (repeat), --limit.
member info Discord, Matrix, Microsoft Teams, Slack --user-id --channel-id (required for Matrix and Microsoft Teams), --guild-id (Discord).

Reaction listings show labels, counts, and available users as plain terminal text. Use --json for the complete channel result.

The legacy message read --include-thread spelling remains accepted for existing scripts but has no effect.

Member info

Use --channel-id to select a Matrix room or a Microsoft Teams standard channel. Teams requires the Graph <team-id>/<channel-id> form because the CLI has no current conversation. Provider access and membership checks still apply.

openclaw message member info --channel matrix \
  --channel-id '!room:example.org' --user-id '@member:example.org'

openclaw message member info --channel msteams \
  --channel-id '<team-id>/<channel-id>' --user-id '<aad-object-id>'

Send

openclaw message send --channel discord \
  --target channel:123 --message "hi" --reply-to 456
  • --media <path-or-url>: attach image/audio/video/document (local path or URL). Repeat to send multiple files in order; Telegram groups consecutive photos into albums.
  • --presentation <json>: shared payload with text, context, divider, chart, table, buttons, and select blocks, rendered per channel capability. See Message Presentation.
  • --delivery <json>: generic delivery preferences, for example {"pin": true}. --pin is shorthand for pinned delivery when the channel supports it.
  • --reply-to <id>, --thread-id <id> (Telegram forum topic; Slack thread timestamp, same field as --reply-to).
  • --force-document: preserve original image bytes on Slack, or send images/GIFs/videos as documents on Telegram and WhatsApp, to avoid channel compression.
  • --silent (Telegram, Discord): send without a notification.
  • --gif-playback (WhatsApp only): treat video media as GIF playback.

When a send is suppressed by a message hook, fails, or only partially succeeds, the command explains the outcome and exits nonzero. Partial delivery keeps any confirmed message ID. JSON failures include ok: false, deliveryStatus, and error; successful JSON responses retain their existing shape.

openclaw message send --channel discord \
  --target channel:123 --message "Choose:" \
  --presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Approve","value":"approve","style":"success"},{"label":"Decline","value":"decline","style":"danger"}]}]}'
openclaw message send --channel telegram --target @mychat --message "Choose:" \
  --presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Yes","value":"cmd:yes"},{"label":"No","value":"cmd:no"}]}]}'

Slack renders supported chart blocks natively; other channels receive the same data as readable text:

openclaw message send --channel slack --target channel:C123 \
  --presentation '{"blocks":[{"type":"chart","chartType":"bar","title":"Quarterly revenue","categories":["Q1","Q2"],"series":[{"name":"Revenue","values":[120,145]}],"xLabel":"Quarter"}]}'

Slack also renders explicit table blocks natively. Other channels receive the caption and every row as deterministic text:

openclaw message send --channel slack --target channel:C123 \
  --presentation '{"title":"Pipeline report","blocks":[{"type":"table","caption":"Open pipeline","headers":["Account","Stage","ARR"],"rows":[["Acme","Won",125000],["Globex","Review",82000]],"rowHeaderColumnIndex":0}]}'

Telegram Mini App buttons use webApp (web_app still parses for legacy JSON) and only render in private chats between a user and the bot:

openclaw message send --channel telegram --target 123456789 --message "Open app:" \
  --presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Launch","webApp":{"url":"https://example.com/app"}}]}]}'
openclaw message send --channel telegram --target @mychat \
  --media ./diagram.png --force-document
openclaw message send --channel telegram --target @mychat \
  --message "Trip photos" --media ./photo-1.jpg --media ./photo-2.jpg
openclaw message send --channel msteams \
  --target conversation:19:abc@thread.tacv2 \
  --presentation '{"title":"Status update","blocks":[{"type":"text","text":"Build completed"}]}'

Poll

openclaw message poll --channel discord \
  --target channel:123 \
  --poll-question "Snack?" \
  --poll-option Pizza --poll-option Sushi \
  --poll-multi --poll-duration-hours 48
  • --poll-option <choice>: repeat 2-12 times.
  • --poll-multi: allow multiple selections.
  • Discord: --poll-duration-hours, --silent, --message.
  • Telegram: --poll-duration-seconds <n> (5-604800; up to seven days), --silent, --poll-anonymous / --poll-public, --thread-id.
openclaw message poll --channel telegram \
  --target @mychat \
  --poll-question "Lunch?" \
  --poll-option Pizza --poll-option Sushi \
  --poll-duration-seconds 120 --silent
openclaw message poll --channel msteams \
  --target conversation:19:abc@thread.tacv2 \
  --poll-question "Lunch?" \
  --poll-option Pizza --poll-option Sushi

Threads

  • thread create: channels Discord. Required: --thread-name, --target (channel id). Optional: --message-id, --message, --auto-archive-min.
  • thread list: channels Discord. Required: --guild-id. Optional: --channel-id, --include-archived, --before, --limit.
  • thread reply: channels Discord. Required: --target (thread id), --message. Optional: --media, --reply-to.

Emojis

  • emoji list: Discord (--guild-id), Slack (no extra flags).
  • emoji upload: Discord. Required: --guild-id, --emoji-name, --media. Optional: --role-ids (repeat).

Stickers

  • sticker send: Discord. Required: --target, --sticker-id (repeat). Optional: --message.
  • sticker upload: Discord. Required: --guild-id, --sticker-name, --sticker-desc, --sticker-tags, --media.

Roles, channels, voice, events (Discord)

  • role info: --guild-id.
  • role add / role remove: --guild-id, --user-id, --role-id.
  • channel info: --target.
  • channel list: --guild-id.
  • voice status: --guild-id, --user-id.
  • event list: --guild-id.
  • event create: required --guild-id, --event-name, --start-time; optional --end-time, --desc, --channel-id, --location, --event-type, --image <url-or-path>.

Moderation (Discord)

  • timeout: --guild-id, --user-id; optional --duration-min or --until (omit both to clear the timeout), --reason.
  • kick: --guild-id, --user-id, --reason.
  • ban: --guild-id, --user-id, --delete-days, --reason.

Broadcast

openclaw message broadcast --targets <target...> [--channel all] [--message <text>] [--media <url>] [--dry-run]

Sends one payload to multiple targets. --targets takes a space-separated list. Use --channel all to target every configured provider.

If any target fails, is suppressed, or only partially delivers, the broadcast exits nonzero. Text output identifies failed targets; JSON reports ok: false and retains every target's result, including failures returned by channel plugins.