OpenClaw Message CLI Guide: openclaw message send, read, broadcast, and When to Skip the Agent
A scheduled alert rarely needs a model. Send it with openclaw message send and keep the agent out of it.
The pages that rank for openclaw message are reference material. The official CLI page runs past 3,000 words and walks every action from send to Discord moderation. A translated quick start covers three examples and a troubleshooting list. The agent --deliver docs explain how an agent turn can post its reply to a channel. All three agree on targets, --channel, and --dry-run. None of them tells you which of the two commands a cron script should call, and that choice decides whether your 7:15 alert costs a model call and can come back worded differently every morning. This guide covers the flags you will actually script against, then makes the case for keeping the agent out of plain notifications.
The send command and the flags that matter
The minimum is a target and a body:
openclaw message send --channel telegram --target @mychat --message "Backup finished"If only one channel is configured, --channel can be dropped. Once you add a second one, the docs say it becomes required. I set it every time anyway, because a script that worked with Telegram alone will start failing the day someone connects Slack, and nobody connects the two events in their head.
You can also fold the channel into the target. discord:channel:123 is a complete address on its own.
The flags I reach for, roughly in order of how often:
--dry-runprints the payload and skips sending. Run it once per new script.--jsonreturns the result as JSON, which is what you want in a log file.--media <path-or-url>attaches a file. It accepts local paths or URLs and can be repeated for several attachments.--messagebecomes optional when media is set.--silentskips the notification sound, on Telegram and Discord only.--account <id>picks one bot when a channel has several. An empty or whitespace-only value is rejected, so a blank shell variable fails loudly instead of falling back.--force-document, for the day Telegram or WhatsApp compresses a screenshot into mush.
Target formats per channel
Every channel addresses recipients differently, and --target takes them all. These come from the official reference:
Telegram
A chat id, @username, or a forum topic written as <chatId>:topic:<topicId>.
Slack
channel:<id> or user:<id>.
Discord
channel:<id>, user:<id>, a <@id> mention, or a bare numeric id.
WhatsApp and Signal
E.164 numbers like +15555550123. WhatsApp groups use a JID ending in @g.us; Signal groups use group:<id>.
iMessage
A handle, or chat_id:<id> when you are writing to a group thread.
You probably do not know your Slack channel id offhand. Ask the directory:
openclaw directory self --channel slack
openclaw directory peers list --channel slack --query "alice"
openclaw directory groups list --channel discordPaste the id into the script. Names drift when someone renames a channel. Ids stay put.
Reading messages back
openclaw message read pulls recent history with --limit, and you can page with --before, --after, or --around a message id. Add --json and pipe it into jq. I use this to check whether a human already replied to an alert before sending a reminder, which keeps a flaky job from nagging the same person four times.
openclaw message read --channel slack --target channel:C0123ABCD --limit 20 --jsonThe two meanings of --thread-id
This one is easy to miss.
On send, the CLI help describes --thread-id as a Telegram forum thread. On read, the same flag is a Slack thread timestamp. Same spelling, different platforms, different value shapes. If you copy a working read invocation, swap the verb to send, and expect your reply to land in the Slack thread, use --reply-to instead and check the result with --dry-run first. Telegram edits and deletes inside a forum also need --thread-id, per the docs.
Broadcast and its exit code
openclaw message broadcast --channel all \
--targets channel:C0123ABCD @opsroom \
--message "Gateway restarted after upgrade"--targets takes a space-separated list, and the help says it accepts names as well as ids when the directory is available. --channel all fans out across every configured provider. The detail worth building around: if any single target fails, is suppressed, or only partially delivers, the whole broadcast exits nonzero. One dead Signal number makes the run look failed even though Slack got the message, so log the --json output and read per-target results before you retry, or you will double-post to everyone who already received it.
message send vs agent --deliver
openclaw agent --message "..." --deliver runs a full agent turn and then posts the reply, optionally to a different place via --reply-channel and --reply-to. Its default timeout is 600 seconds. That is a model reading your prompt and writing a reply, with tool calls along the way if it decides it needs them.
openclaw message send posts exactly the string you gave it.
My rule: if the script already knows what to say, use message send. A disk-full warning qualifies. So does a failed backup. Fixed text means no inference spend and the same wording every run; when it breaks, look at the token or the target, because there is no prompt to blame. Reach for agent --deliver when the message itself needs judgment, like summarizing a log you have not read or triaging overnight email into three lines. Wrapping a fixed string in an agent turn buys you paraphrase and a bill. The cost optimization guide has more on trimming those calls.
A cron wrapper in that style, following the shape from the cron automation guide:
#!/usr/bin/env bash
set -euo pipefail
LOG="$HOME/openclaw-automation/logs/disk-alert.log"
USED=$(df -P / | awk 'NR==2 {gsub("%",""); print $5}')
if [ "$USED" -ge 90 ]; then
openclaw message send --channel telegram --target @opsroom \
--message "Root volume at ${USED}% on $(hostname)" --json >> "$LOG"
fiWho owns the message
The docs say openclaw message acts as the configured System Agent. On a single-agent install you will never notice. With several agents and no owner assigned, the command asks you to pick one:
openclaw agents list
openclaw config set agents.defaults.systemAgent.agentId <id>Channel secrets are resolved before anything sends. An unresolved SecretRef on the channel you selected stops the command, but a broken credential on some unrelated channel will not block it. If you are still wiring up bots, the Slack, Discord, and Telegram integration guide covers token setup, and the secrets guide covers where those tokens should live.
Common failure modes
Script worked yesterday, fails today
A second channel was added and the script never passed --channel. Add it, or use a channel-prefixed target.
Message lands in the wrong place after a rename
The script addressed a channel by name. Look up the id with openclaw directory groups list and use channel:<id>.
Broadcast exits nonzero but people got the message
One target failed or was suppressed. Read the per-target results in the --json output and retry only the failures.
Blank --account in a template
Empty values are rejected outright. Either set the variable or drop the flag.
Final verdict
Keep the model for messages that need thinking. Everything else is a shell string, an id you looked up once with directory, and a --dry-run before the first real send.
Ready to build?
Get the OpenClaw Starter Kit — config templates, 5 production-ready skills, deployment checklist. Go from zero to running in under an hour.
$14 $6.99
Get the Starter Kit →Also in the OpenClaw store
Get the free OpenClaw quickstart guide
Step-by-step setup. Plain English. No jargon.