Scripting SlackCLI from shell scripts and AI agents
SlackCLI is built to be driven by scripts, cron jobs, and AI agents as much as by hand.
--json
Section titled “--json”Every read command supports --json: conversations list, conversations read,
conversations get, conversations unread, search messages, search channels,
search people, saved list, canvas list, canvas read, team info,
usergroups list, usergroups read, emoji list, emoji get, files info,
files read.
The writing commands support it too — messages send, messages edit,
messages draft, and the usergroups write verbs (create, update, add,
remove, enable, disable) — where it returns the identity of what was just
written instead of the human success line.
JSON goes to stdout. Progress spinners, warnings, error messages, and the update-available notice go to stderr, so a pipe normally carries only data:
slackcli conversations read C1234567890 --json | jq '.messages[].text'Shapes
Section titled “Shapes”# Conversations, with a resolved users array so DM ids are not opaqueslackcli conversations list --json | jq '.conversations[] | {id, name}'slackcli conversations list --json | jq '.next_cursor'
# Messages, with a resolved users array so IDs are not opaqueslackcli conversations read C123 --json | jq '.messages[] | {ts, user, text}'slackcli conversations read C123 --json | jq '.users[] | {id, real_name}'
# Just the thread replies to one messageslackcli conversations read --permalink="$LINK" --json | jq -r '.messages[].text'
# Search hits with their permalinksslackcli search messages "deploy failed" --json | jq -r '.matches[] | "\(.channel.name)\t\(.permalink)"'
# Unread channels that have mentionsslackcli conversations unread --json | jq '.unread_channels[] | select(.mention_count > 0)'
# A canvas as Markdownslackcli canvas read F123 --json | jq -r '.markdown' > canvas.md--json output for messages includes ts, thread_ts, user, text, type,
reply_count, reactions, bot_id, blocks, attachments, and file metadata
when a message has attachments.
Exit codes
Section titled “Exit codes”0 on success, 1 on failure. Failures print a message to stderr — check the
exit code rather than parsing that text.
if ! slackcli messages send --recipient-id="$CHANNEL" --message="$TEXT"; then echo "post failed" >&2 exit 1fiAn empty result is not a failure: a search with no hits, or an unread list with
nothing in it, exits 0. Test the data, not the exit code:
count=$(slackcli search messages "$Q" --json | jq '.total')[ "$count" -gt 0 ] || echo "nothing found"Patterns
Section titled “Patterns”Post and keep the timestamp, so you can edit or react later. --json gives
you the channel, the timestamp, and the permalink in one object:
sent=$(slackcli messages send --recipient-id=C123 --message="Working…" --json)ts=$(jq -r '.ts' <<<"$sent")
slackcli messages react --channel-id=C123 --timestamp="$ts" --emoji=eyesslackcli messages edit --channel-id=C123 --timestamp="$ts" --message="Done ✅"The permalink is handy for handing the message to a human, or for feeding it
straight back into any command that takes --permalink:
link=$(jq -r '.permalink // empty' <<<"$sent")slackcli messages send --permalink="$link" --message="…and here is the log"permalink is looked up after delivery and is omitted if that lookup fails, so
use // empty (or test the key) rather than assuming it is always there.
Send a long or multi-line body from a file, instead of fighting shell quoting — useful when the text is generated by an earlier step:
generate-release-notes > /tmp/notes.mdslackcli messages send --recipient-id=C123 --message-file=/tmp/notes.md --json--message and --message-file are mutually exclusive, and an empty or
unreadable file is rejected before anything is posted, so a failed generation
step cannot silently post an empty message.
Reply into a thread from a link — no ID juggling:
slackcli messages send --permalink="$SLACK_LINK" --message="On it"Pick an identity explicitly in unattended jobs, rather than depending on whichever workspace happens to be the default:
slackcli messages send --workspace=automation-bot --recipient-id=C123 --message="Nightly build green"Paginate a search:
page=1while :; do out=$(slackcli search messages "$Q" --page="$page" --limit=100 --json) echo "$out" | jq -r '.matches[].permalink' [ "$page" -lt "$(echo "$out" | jq '.pages')" ] || break page=$((page + 1))doneNotes for unattended use
Section titled “Notes for unattended use”- Token freshness. Browser tokens die with the browser session. Refresh them
non-interactively with
slackcli auth login-auto --headless, which works once the profile has been signed in once. - Throttling. slackcli keeps at most 2 Slack API calls in flight and leaves
at least 200ms between them, so it does not trip Slack’s
unexpected_api_call_volumeanomaly detection. Commands that resolve many users or channels (conversations unread,saved liston a long list) make one API call per entity, so budget wall-clock time for them and set generous timeouts in a scheduled job. - The update notice. SlackCLI may append a one-line “update available” notice
after a command. It goes to stderr and never contaminates
--jsonon stdout. usergroupswrites need--yes.create,update,add,remove,enable, anddisablerefuse to run with a non-zero exit when stdin is not a terminal and--yesis absent, so an unattended job must pass--yesexplicitly. See User groups.- Credentials.
~/.config/slackcli/workspaces.jsonholds live tokens at mode0600. Give a CI job its own bot-token profile rather than copying a personal browser session around.