Skip to content

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.

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:

Terminal window
slackcli conversations read C1234567890 --json | jq '.messages[].text'
Terminal window
# Conversations, with a resolved users array so DM ids are not opaque
slackcli conversations list --json | jq '.conversations[] | {id, name}'
slackcli conversations list --json | jq '.next_cursor'
# Messages, with a resolved users array so IDs are not opaque
slackcli 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 message
slackcli conversations read --permalink="$LINK" --json | jq -r '.messages[].text'
# Search hits with their permalinks
slackcli search messages "deploy failed" --json | jq -r '.matches[] | "\(.channel.name)\t\(.permalink)"'
# Unread channels that have mentions
slackcli conversations unread --json | jq '.unread_channels[] | select(.mention_count > 0)'
# A canvas as Markdown
slackcli 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.

0 on success, 1 on failure. Failures print a message to stderr — check the exit code rather than parsing that text.

Terminal window
if ! slackcli messages send --recipient-id="$CHANNEL" --message="$TEXT"; then
echo "post failed" >&2
exit 1
fi

An 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:

Terminal window
count=$(slackcli search messages "$Q" --json | jq '.total')
[ "$count" -gt 0 ] || echo "nothing found"

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:

Terminal window
sent=$(slackcli messages send --recipient-id=C123 --message="Working…" --json)
ts=$(jq -r '.ts' <<<"$sent")
slackcli messages react --channel-id=C123 --timestamp="$ts" --emoji=eyes
slackcli 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:

Terminal window
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:

Terminal window
generate-release-notes > /tmp/notes.md
slackcli 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:

Terminal window
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:

Terminal window
slackcli messages send --workspace=automation-bot --recipient-id=C123 --message="Nightly build green"

Paginate a search:

Terminal window
page=1
while :; 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))
done
  • 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_volume anomaly detection. Commands that resolve many users or channels (conversations unread, saved list on 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 --json on stdout.
  • usergroups writes need --yes. create, update, add, remove, enable, and disable refuse to run with a non-zero exit when stdin is not a terminal and --yes is absent, so an unattended job must pass --yes explicitly. See User groups.
  • Credentials. ~/.config/slackcli/workspaces.json holds live tokens at mode 0600. Give a CI job its own bot-token profile rather than copying a personal browser session around.

Cookies