Skip to content

Read Slack channels, threads and unreads

Edit on GitHub

slackcli conversations covers channels, DMs, and group DMs: listing them, reading history and threads, fetching one message, seeing what is unread, and marking a conversation as read.

Every subcommand accepts --workspace <id|name>. list, read, get and unread take --fields with --json to print only the fields you name, e.g. --json --fields ts,user,text.

Terminal window
slackcli conversations list # every conversation you can see
slackcli conversations list --types=public_channel
slackcli conversations list --types=im # DMs only
slackcli conversations list --limit=200 --exclude-archived
# Machine-readable
slackcli conversations list --json
Option Default Purpose
--types <types> public_channel,private_channel,mpim,im Comma-separated conversation types
--limit <number> 100 How many to return
--exclude-archived off Skip archived conversations
--cursor <cursor> — Fetch the next page
--json off JSON output with a resolved users array
--fields <list> all With --json, only these comma-separated fields (dot paths allowed)

DM entries are resolved to the other person’s name. When more results exist, the exact --cursor command for the next page is printed (human output) or set on next_cursor (--json; null on the last page).

Read channel history, or one thread.

Terminal window
# Recent messages in a channel (oldest first)
slackcli conversations read C1234567890
# One thread
slackcli conversations read C1234567890 --thread-ts=1234567890.123456
# The thread a message link points at
slackcli conversations read --permalink="https://myteam.slack.com/archives/C1234567890/p1234567890123456"
# Top-level messages only
slackcli conversations read C1234567890 --exclude-replies
# A time window
slackcli conversations read C1234567890 --oldest=1735689600 --latest=1738368000
# Machine-readable
slackcli conversations read C1234567890 --json
# Only what is new since the last poll, minus your own messages
slackcli conversations read C1234567890 --oldest="$CURSOR" --exclude-self --json
Option Default Purpose
--thread-ts <ts> — Read a specific thread instead of the channel
--permalink <url> — Replaces the channel argument and --thread-ts
--exclude-replies off Drop threaded replies from channel history
--exclude-self off Drop messages sent by the authenticated user (or, for a bot token, its bot)
--limit <number> 100 How many messages
--oldest / --latest — Time range bounds (1234567890.123456, p1234567890123456, or epoch seconds); only messages strictly newer than --oldest are shown
--json off JSON output, including ts and thread_ts
--fields <list> all With --json, only these comma-separated fields (dot paths allowed)

Channel history comes back newest-first from Slack and is reversed so you read top to bottom. Thread replies are already chronological. --json also includes reactions, blocks, attachments, file metadata, and a resolved users array.

--json also carries a polling cursor:

  • next_oldest — the newest ts of the page Slack returned, taken before --exclude-self / --exclude-replies filter it. Pass it back as --oldest on the next call. On an empty page it is the --oldest you passed in (or null without one), and it never moves backwards.
  • has_more — Slack has more messages in the range than --limit returned.

Slack’s thread API always returns the thread’s parent message, whatever --oldest says, so read drops every message with ts <= --oldest itself: a thread poll does not see the parent again. --exclude-self uses the user ID saved at login; a workspace saved before that, or a bot token (whose bot_id is not stored), costs one auth.test call. See Poll a channel or thread safely.

Fetch one message by channel and timestamp.

Terminal window
slackcli conversations get C1234567890 1234567890.123456
slackcli conversations get --permalink="https://myteam.slack.com/archives/C1234567890/p1234567890123456"
slackcli conversations get C1234567890 p1234567890123456 --json

Auth-type caveat. With browser auth this resolves both top-level messages and thread replies. With a standard token it can only resolve top-level messages — looking up an arbitrary reply needs its parent’s thread_ts, and no public Slack API returns that from a reply timestamp alone. Read the thread instead: conversations read <channel> --thread-ts=<parent>.

Terminal window
slackcli conversations unread
slackcli conversations unread --types=dms # channels, dms, groups
slackcli conversations unread --json

Conversations with mentions sort first, then alphabetically. --json gives { unread_channels: [...] }. When no conversation and no thread is unread the command prints “All caught up!” on stderr and exits 0. The text output then writes nothing to stdout; --json still prints one object with an empty list, { "unread_channels": [] } (plus threads with browser auth), so jq '.unread_channels | length' works in every case.

Unread threads. With browser auth the command also says whether the threads you follow have unread replies or mentions, which Slack counts separately from their channel:

🧵 Threads: unread replies, 1 mention
{ "unread_channels": [], "threads": { "has_unreads": true, "mention_count": 1 } }

threads is one summary for the whole workspace, not a list: it does not say which threads. It comes from the same client.counts response, so it costs no extra call, and --types does not filter it. Unread threads alone are enough to print a result, so a script should test both keys before reporting “nothing unread”. A thread mention counts as unread even when has_unreads is false. With a standard token the threads key is left out, because conversations.list has no equivalent.

--messages: the unread messages themselves

Section titled “--messages: the unread messages themselves”
Terminal window
slackcli conversations unread --messages
slackcli conversations unread --messages --max-conversations 5 --limit 10 --json

With --messages the same command also returns what is unread, so catching up is one call instead of one conversations read per channel:

  • For each unread conversation, the messages newer than that conversation’s read cursor (last_read, which Slack reports with the unread counts), oldest first. These are top-level messages; replies belong to the thread list below.
  • The threads you follow that have unread replies: the root message and its unread replies.

It needs browser auth. With a standard token --messages fails with unsupported_auth_type before any Slack call. Nothing is marked as read.

Option Default Meaning
--max-conversations <n> 10 How many conversations are read. Mentions first, then by name, after --types. The rest are listed without messages.
--limit <n> 20 Most messages per conversation, and most replies per thread (1 to 999). When more are unread, the earliest are kept.

Both options only apply with --messages. Without it, or with a value that is not a positive integer, the command fails with invalid_input.

{
"unread_channels": [
{
"id": "C1234567890", "name": "general", "mention_count": 2, "has_unreads": true,
"last_read": "1700000001.000000", "latest": "1700000003.000000",
"messages": [{ "ts": "1700000002.000000", "user": "U1234567890", "text": "…", "type": "message" }],
"has_more": true
},
{ "id": "C2345678901", "name": "random", "mention_count": 0, "has_unreads": true, "last_read": "1700000000.000000" }
],
"threads": {
"has_unreads": true,
"mention_count": 1,
"items": [
{
"channel_id": "C1234567890",
"thread_ts": "1700000100.000000",
"root": { "ts": "1700000100.000000", "user": "U1234567890", "text": "…", "type": "message" },
"unread_replies": [{ "ts": "1700000101.000000", "user": "U2345678901", "text": "…", "type": "message" }]
}
]
},
"users": [{ "id": "U1234567890", "name": "alice", "real_name": "Alice", "email": "alice@acme.com" }]
}
  • Each item of unread_channels gains last_read and latest when Slack sent them. A conversation that was read has messages (possibly empty, when only thread replies are new), with the same fields as conversations read --json. A conversation past --max-conversations has no messages key: read it with conversations read <id> --oldest <last_read>.
  • has_more: true on a conversation or a thread means --limit left unread messages out. Raise --limit, or continue with conversations read <id> --oldest <ts of the last message shown>.
  • threads.items lists the unread threads, and threads.has_more: true says the list is incomplete. items is [] when no thread is unread. --types does not filter it, and --fields still projects unread_channels only (for example --fields id,name,messages.ts,messages.text).
  • users lists the authors of every message printed, as in conversations read.

The text output prints each conversation’s row followed by its messages, then an Unread Threads section.

Cost. Every call is paced one at a time by the rate limiter: one conversations.history per conversation read, one users.info per author, and up to five calls for the threads (none when no thread is unread). That is why --max-conversations exists; a run over many conversations is slower than plain unread.

Unread threads come from an undocumented method. The thread list is read from subscriptions.thread.getView, the method behind Slack’s own Threads view, up to 5 pages of 10 threads. If Slack changes or refuses it, the command still succeeds: it warns on stderr and threads keeps only has_unreads and mention_count, with no items key. A script should treat a missing items as “not known”, not as “none”.

A conversation never read has an all-zero last_read, which gives no starting point. The newest --limit messages are returned for it instead.

Auth-type caveat. With browser auth this reads Slack’s own unread state (client.counts), then makes one or two API calls per unread conversation to resolve its name (a DM also looks up the user), so a workspace with many unread channels may hit Slack rate limits. With a standard token it reads the first 1000 conversations from conversations.list and keeps those you are a member of that carry an unread count; Slack often omits those counts for app tokens, so the result can be incomplete.

Terminal window
slackcli conversations mark-read C1234567890 --ts 1712345678.123456
slackcli conversations mark-read --permalink https://acme.slack.com/archives/C1234567890/p1712345678123456 --yes --json
slackcli conversations mark-read "#general" --ts 1712345678.123456 --dry-run

Marks a channel, DM or group DM as read up to one message: it moves the conversation’s read cursor to that message’s timestamp. It is the second half of catching up, after conversations unread and conversations read.

The target message is always explicit. Give <channel> and --ts, or --permalink alone; combining them, or giving neither --ts nor --permalink, fails with invalid_input before anything is sent. There is no “mark everything read” form. <channel> accepts an ID, a Slack link or a channel name, and --ts accepts 1712345678.123456 and p1712345678123456. A link to a thread reply marks the conversation up to that reply’s own timestamp, not its parent’s.

Like the other mutating commands it confirms first (or takes --yes when non-interactive), and --dry-run prints the conversation, the timestamp and the current cursor without changing anything. --json gives:

{ "channel_id": "C1234567890", "ts": "1712345678.123456", "previous_last_read": "1712345600.000200" }

Undoing it. previous_last_read is the read cursor before the change, read with one conversations.info call just before the write. Run the command again with --ts set to that value to put the cursor back. It is null when Slack does not report a cursor or that read fails (the write still happens), and then there is nothing to mark back to. Without --json the previous cursor is part of the success line on stderr.

What it does not do.

  • It does not check that the timestamp names an existing message. A timestamp older than the current cursor moves the cursor back, so later messages count as unread again.
  • It does not change thread read state. Slack tracks the threads you follow with a separate cursor, so the threads summary of conversations unread is unaffected.

Auth-type caveat. The cursor belongs to the authenticated user, so the command is meant for a browser session or a user token (xoxp), where it is a person’s inbox. A standard token needs the write scope for the conversation’s kind (channels:write, groups:write, im:write or mpim:write); without it Slack answers missing_scope, reported as permission_denied. With a bot token (xoxb) the cursor is the bot’s own, not a person’s inbox; what marking it changes has not been verified beyond the scope requirement.

Terminal window
slackcli conversations members list C1234567890
slackcli conversations members list C1234567890 --limit=50
slackcli conversations members list C1234567890 --cursor="$NEXT_CURSOR" # next page
slackcli conversations members list C1234567890 --json

Lists the member IDs of a channel or conversation. --limit reflects members returned (it pages until it has that many or the conversation is exhausted), and when more remain the output prints the --cursor value to fetch the next page. --json gives { channel_id, member_count, members: [...], next_cursor? }.

Enterprise-grid caveat. On an Enterprise Grid org this endpoint can be blocked by org policy — Slack returns enterprise_is_restricted, the command reports it clearly and exits non-zero (scoping to a team does not lift it). Member management (add/remove) and self ops (join/leave) are documented below; this command is read-only.

members add, members remove, join, leave and mark-read take --dry-run: the channel and user IDs are resolved and checked, and the command prints what it would do without doing it and without prompting. The preview lists the users that would be added or removed; for mark-read it shows the timestamp and the current read cursor.

Terminal window
slackcli conversations members add C1234567890 U1 U2 --dry-run
slackcli conversations leave C1234567890 --dry-run --json

See --dry-run for the preview format.

Terminal window
slackcli conversations members add C1234567890 U1 U2 U3
slackcli conversations members add C1234567890 U1,U2 --yes # non-interactive
slackcli conversations members add C1234567890 U1 --json

Adds one or more users (or agents/apps) to a channel. IDs are comma- or space-separated. This is a batch, all-or-nothing operation: Slack’s conversations.invite adds the whole set or, if any ID cannot be invited, adds none and reports the error — a success message never over-reports.

Mutating commands confirm before acting: in a terminal you get a [y/N] prompt; with no terminal the command refuses unless you pass --yes, so a script cannot change membership by accident. --json gives { channel_id, added: [...] }.

Terminal window
slackcli conversations members remove C1234567890 U1 U2
slackcli conversations members remove C1234567890 U1 --yes --json

Removes one or more users from a channel. Slack’s conversations.kick removes one user per call, so this is best-effort per ID: it attempts every ID and reports which came out and which failed, rather than stopping at the first error. It exits non-zero if any removal failed, and --json gives { channel_id, removed: [...], failed: [{ user, error }] }. Confirmation works exactly as for add (prompt, or --yes when non-interactive).

Terminal window
slackcli conversations join C1234567890
slackcli conversations join C1234567890 --json

Joins a public channel as yourself. This is a self-op with no confirmation prompt — it is idempotent (joining a channel you are already in is a no-op) and only changes your own membership. --json gives { channel_id, channel }.

Terminal window
slackcli conversations leave C1234567890
slackcli conversations leave C1234567890 --yes --json

Leaves a channel or conversation as yourself. Like the other mutating commands it confirms first (or takes --yes when non-interactive). Leaving a channel you are already out of is reported as a no-op, not an error. --json gives { channel_id, left, not_in_channel }.

Cookies