slackcli messages sends, edits, and reacts to messages, and creates, lists,
sends, and deletes drafts.
Every subcommand accepts --workspace <id|name>.
Previewing a write (--dry-run)
Section titled “Previewing a write (--dry-run)”send, edit, react, draft, send-draft and delete-draft take
--dry-run. The message is resolved and checked as for a real send — the text
from --message or --message-file, parsed --blocks, the --file to
upload, the target from --recipient-id, --thread-ts or --permalink — and
printed instead of sent. send, edit, react and draft have no
confirmation prompt, so this is the way to check one first:
slackcli messages send --recipient-id C0123456789 --message-file ./note.md --dry-runslackcli messages send-draft Dr0123456789 --dry-run --jsonA dry run to a user ID does not open the DM; the preview names the user. The
draft commands still need browser session tokens, and fail on an app token as
the real command does. See --dry-run for the
preview format.
messages send
Section titled “messages send”# To a channelslackcli messages send --recipient-id=C1234567890 --message="Hello team!"
# To a person — the DM is opened for youslackcli messages send --recipient-id=U9876543210 --message="Hey there!"
# As a thread replyslackcli messages send --recipient-id=C1234567890 --thread-ts=1234567890.123456 --message="Great idea!"
# Reply in the thread a link points atslackcli messages send --permalink="https://myteam.slack.com/archives/C1234567890/p1234567890123456" --message="Great idea!"
# With a file attachedslackcli messages send --recipient-id=C1234567890 --message="Here is the file" --file=./report.pdf
# Message text from a fileslackcli messages send --recipient-id=C1234567890 --message-file=./release-notes.md
# Message text from standard inputprintf '%s' "$REPORT" | slackcli messages send --recipient-id=C1234567890 --message-file -| Option | Purpose |
|---|---|
--recipient-id <id> |
Channel ID, user ID, Slack URL, "#channel", @handle or email (names) |
--message <text> |
Message text. Required unless --message-file is given |
--message-file <path> |
Read the message text from a UTF-8 file, or from standard input with -; cannot be combined with --message |
--thread-ts <ts> |
Post as a reply in this thread |
--permalink <url> |
Replaces --recipient-id and --thread-ts |
--file <path> |
Attach a file; the message text becomes the comment |
--blocks <json|@file> |
Block Kit JSON array; cannot be combined with --file |
--json |
Print the delivered message as JSON instead of the human line |
A --recipient-id starting with U opens a DM first. File uploads need upload
permission in the workspace — files:write for standard tokens. Uploads go
through Slack’s external-upload flow; empty files, directories, and missing
paths are rejected before anything is sent.
On success the message timestamp is printed — capture it if you want to edit or
react to the message later, or use --json to get it as structured data.
Message text from a file (--message-file)
Section titled “Message text from a file (--message-file)”--message-file reads the message body from a UTF-8 file instead of an
argument, which avoids quoting and shell-escaping a long or multi-line message:
slackcli messages send --recipient-id=C1234567890 --message-file=./release-notes.mdThe file’s contents are sent exactly as --message would send them, mrkdwn
included. --message and --message-file are mutually exclusive, and exactly
one of them is required. A missing path, or a file that is empty or only
whitespace, is an error raised before anything is sent.
Message text from standard input (--message-file -)
Section titled “Message text from standard input (--message-file -)”Give - as the path and the text is read from standard input. Nothing has to
be quoted for the shell and no temporary file is written, so this is the
safest way to send multi-line text, code, or anything containing quotes,
backticks or $:
printf '%s' "$REPORT" | slackcli messages send --recipient-id=C1234567890 --message-file -
slackcli messages send --recipient-id=C1234567890 --message-file - <<'MSG'Build `main` failed: "tests" step, see $LOGMSG- The text is sent as it arrives. Only one trailing newline is dropped,
the one
echoor a heredoc adds; every other character, including further blank lines, is kept. - Standard input must be piped or redirected. If it is a terminal, the command fails at once and tells you to pipe the text in. It never waits for typing.
- Empty or whitespace-only input is rejected like an empty file, and nothing is sent.
- At most 1 MB is read (a Slack message holds about 40 KB). Larger input is an error and nothing is sent.
- The command waits for the producer to finish, for up to 5 minutes. A pipe that is still open after that is an error and nothing is sent, so a stuck producer cannot hang an unattended run. For a step that takes longer, write its output to a file first and pass the path.
- Only
-itself means standard input. To read a file that is really named-, write./-. --messagecannot be combined with--message-file -, as with a path.
The same works on messages edit and messages draft.
JSON output (--json)
Section titled “JSON output (--json)”--json replaces the human success line with a single object on stdout, so a
script can keep the message’s identity for a follow-up call:
slackcli messages send --recipient-id=C1234567890 --message="Deploying…" --json{ "channel_id": "C1234567890", "ts": "1234567890.123456", "permalink": "https://myteam.slack.com/archives/C1234567890/p1234567890123456"}permalink is looked up separately with chat.getPermalink after the message
is delivered. If that lookup fails — a token without the scope, say — the key
is omitted rather than emitted as null; the send itself still succeeded.
Test for the key rather than assuming it.
With --file, the upload flow returns the attached file rather than a message
timestamp, so that branch emits channel_id and file_id only.
A failure with --json writes nothing to stdout and exits 1; the last line of
stderr is a JSON error object with a stable code. See
errors under --json.
Text formatting
Section titled “Text formatting”Message text is sent with Slack’s parse=none, so Slack mrkdwn works as you
write it: *bold*, _italic_, ~strike~, `code`, triple-backtick
blocks, and links as <https://example.com|label>. Slack has no [text](https://github.com/shaharia-lab/slackcli/blob/main/docs/user-guide/url)
link syntax and no # headings in plain message text — use --blocks with a
markdown block if you want those.
Block Kit (--blocks)
Section titled “Block Kit (--blocks)”--blocks takes a JSON array of
Block Kit blocks, inline or
loaded from a file with --blocks=@blocks.json. It works with both auth types.
The required --message text is still used as the notification and
accessibility fallback.
Native markdown blocks
take standard Markdown rather than Slack mrkdwn, which buys you headings, task
lists, syntax-highlighted code fences, and Markdown tables:
slackcli messages send \ --recipient-id=C1234567890 \ --message="Release notes" \ --blocks='[{"type":"markdown","text":"# Release notes\n\n- [x] Build\n- [ ] Deploy\n\nSee the [runbook](https://example.com/runbook)."}]'Native table blocks
render a real table instead of a code block:
slackcli messages send \ --recipient-id=C1234567890 \ --message="Project status table" \ --blocks='[ { "type": "table", "column_settings": [{"is_wrapped": true}, {"align": "right"}], "rows": [ [ {"type": "rich_text", "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "Project", "style": {"bold": true}}]}]}, {"type": "rich_text", "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "Status", "style": {"bold": true}}]}]} ], [ {"type": "rich_text", "elements": [{"type": "rich_text_section", "elements": [{"type": "link", "text": "SlackCLI", "url": "https://github.com/shaharia-lab/slackcli"}]}]}, {"type": "raw_text", "text": "Ready"} ] ] } ]'The JSON is validated before the request: it must be an array, and every element
must be an object with a non-empty string type. Errors name the offending
index.
messages edit
Section titled “messages edit”slackcli messages edit --channel-id=C1234567890 --timestamp=1234567890.123456 --message="Corrected message"slackcli messages edit --permalink="https://myteam.slack.com/archives/C1234567890/p1234567890123456" --message="Corrected message"Only messages posted by the authenticated user or app can be edited; ephemeral
messages cannot. Links survive an edit — SlackCLI sends parse=none explicitly,
because chat.update would otherwise default to client and escape
<url|label> markup.
--json prints the edited message’s identity instead of the human line. There
is no permalink lookup here: the caller already had the message’s location in
order to edit it.
{ "channel_id": "C1234567890", "ts": "1234567890.123456"}--message-file works here exactly as it does on messages send — the new
body comes from a UTF-8 file, or from standard input with -, mutually
exclusive with --message.
messages react
Section titled “messages react”slackcli messages react --channel-id=C1234567890 --timestamp=1234567890.123456 --emoji=+1slackcli messages react --permalink="https://myteam.slack.com/archives/C1234567890/p1234567890123456" --emoji=heart--emoji takes the name without colons: +1/thumbsup 👍, heart ❤️,
fire 🔥, eyes 👀, tada 🎉, rocket 🚀. Custom workspace emoji work too.
messages draft
Section titled “messages draft”slackcli messages draft --recipient-id=C1234567890 --message="Hello team!"Creates an unsent draft in the Slack client — useful when you want a human to review and press send.
To send a reviewed draft or discard it:
slackcli messages list-drafts --jsonslackcli messages send-draft Dr1234567890 --workspace=rafael --yes --jsonslackcli messages delete-draft Dr1234567890 --workspace=rafael --yes --jsonsend-draft reads the active draft, posts its original rich-text blocks to the saved
channel and thread_ts (if present), then deletes the draft. --json returns
{channel_id, ts, permalink?} like messages send. A failed permalink lookup
omits that field. Scheduled drafts, drafts with files, empty drafts, and drafts
with multiple destinations are refused before posting. delete-draft removes the
specified draft without posting; with --json it returns
{draft_id, deleted: true}.
Both actions ask for confirmation in a terminal. In a script or other non-TTY
session, pass --yes explicitly. send-draft reads the draft (drafts.list) before
it asks, so a refused send still makes that one read call; it posts nothing. If posting succeeds but draft deletion fails,
send-draft --json emits the posted
message identity plus cleanup_error, exits nonzero, and leaves the draft. Check
the posted message before retrying; another send could duplicate it.
Sending and deleting require browser auth, just like creation and listing.
They use Slack’s undocumented web-client drafts.delete endpoint, which may
change without notice. The CLI supplies a current client_last_updated_ts for
deletion; the draft’s stored update timestamp causes draft_has_conflict.
Browser auth only. Slack apps cannot manage drafts; there is no public API
for it. The command uses an undocumented Slack web-client endpoint that may
change without notice. With a standard token the command fails with
Draft creation requires browser authentication.
The text is converted from Slack mrkdwn into rich_text blocks so the draft
opens in the composer already formatted. User, user group, channel and
broadcast tokens (<@U…>, <!subteam^S…>, <#C…>, <!here>) and links
(<https://…|label>) become real mentions and links; any other <…> text stays
literal. --message-file works here exactly as
it does on messages send, including - for standard input.
--json prints the draft’s identity. A draft is unsent, so it has no message
timestamp and no permalink — the draft id is what a follow-up has to work with.
thread_ts is present only when the draft is a threaded reply.
{ "channel_id": "C1234567890", "draft_id": "1234567890.123456"}messages list-drafts
Section titled “messages list-drafts”# Human-readable listslackcli messages list-drafts
# Stable structured output, capped at 25 active draftsslackcli messages list-drafts --limit=25 --jsonLists the authenticated user’s active drafts. The default --limit is 100;
it must be a positive integer. Human output shows each draft’s destination,
age, message preview, draft id, and any thread, file, or scheduled-send metadata.
Browser auth only. Slack apps cannot list drafts because Slack exposes no
public API for it. This command uses the undocumented web-client drafts.list
endpoint, which may change without notice. With a standard token it fails with
Draft listing requires browser authentication.
--json deliberately exposes a small, stable projection rather than Slack’s
raw internal draft objects (--fields
trims each draft further, e.g. --fields draft_id,text):
{ "draft_count": 1, "drafts": [ { "draft_id": "Dr1234567890", "channel_id": "C1234567890", "text": "Draft for later", "date_created": 1789734977, "file_ids": [] } ]}thread_ts and date_scheduled are included only when present. Deleted and
already-sent entries are excluded, and an empty result is successful with
{"draft_count":0,"drafts":[]}.
Related
Section titled “Related”- Slack links and timestamps — what
--permalinkaccepts - Scripting and JSON output — capturing timestamps in a script