Skip to content

slick message

Send, edit, and delete Slack messages. Markdown input is converted to Block Kit by default; --blocks accepts raw Block Kit JSON. Slack-side message operations require chat:write (and matching channel scopes); user-token profiles can edit and delete only their own messages.

For thread replies, see reply. For reactions, see react.

message send

Send to a channel by ID, name, or alias; --file - reads the body from stdin, and --dry-run previews without sending:

slick message send --channel C1234567890 --message "Deploy complete"
echo "Deploy complete" | slick message send --channel C1234567890 --file -
slick message send --channel C1234567890 --message "Deploy complete" --dry-run

Send to one user, a group DM, or a Slack-profile email:

slick message send --user U1234567890 --message "Build artifact ready"
slick message send --user dev@example.com,ops@example.com --message "Review please"

Send a raw Block Kit payload from a file:

slick message send --channel C1234567890 --blocks --file blocks.json

Schedule a future send with RFC3339, a Go duration, or Unix seconds (no natural-language times):

slick message send --channel C1234567890 --message "Deploy tomorrow" --schedule 2026-06-01T15:00:00-04:00
slick message send --channel C1234567890 --message "Deploy in 90 minutes" --schedule 90m
slick message send --user dev@example.com --message "DM follow-up" --schedule 90m

Flags

Flags
Flag Value Description
-c, --channel <CHANNEL> Channel ID, name, or alias
-u, --user <USER>… User ID or alias; repeat or comma-separate for group DM
-m, --message <TEXT> Message body
-f, --file <FILE> Read message body from file or - for stdin
-N, --filename <NAME> Filename metadata for stdin sources
-b, --blocks Treat message source as raw Block Kit JSON
-s, --schedule <WHEN> Schedule for future send: RFC3339, Go duration, or Unix seconds
-n, --dry-run Preview without sending
Attribution flags

Accepted by the four mutating commands (message send, message edit, reply, file upload):

Flag Value Description
--[no-]attribution Toggle attribution for this command
--attribution-label <LABEL> Override attribution label
--attribution-emoji <EMOJI> Override attribution emoji
--attribution-message <TEXT> Override attribution message

If default_channel is set in config, both --channel and --user can be omitted for immediate sends. Scheduled sends require an explicit --channel or --user target.

--user accepts Slack user IDs, configured aliases, and Slack-profile email addresses resolved via users.lookupByEmail. Repeat the flag or comma-separate values to open a group DM. If Slack returns users_not_found for an email, that address did not match a Slack user in the active workspace.

Accepted --schedule formats

--schedule accepts only RFC3339 timestamps, Go durations such as 90m or 2h30m, and Unix seconds. It rejects every other format, including natural-language strings such as tomorrow at 9am.

Scheduled sends accept either --channel or --user; scheduled DMs resolve/open the DM before calling the Slack scheduled-message API. Real scheduled --user sends return the raw DM or MPIM conversation ID in data.channel. Scheduled sends return scheduled_message_id, not ts, because the Slack API does not assign a message timestamp until the message fires.

Markdown handling

Markdown converts to Block Kit before posting:

  • Paragraphs and headings render as section blocks with mrkdwn text. (Headings do not become Slack header blocks — slick treats them as section text so Slack renders them inline with the rest of the message.)
  • Tables render as native Block Kit table blocks. Slack rejects messages containing more than one table block, so slick errors out rather than truncating.
  • Unsupported block-level constructs — lists, blockquotes, fenced code, raw HTML — are preserved as readable section blocks rather than dropped.
  • User content is escaped. Section-block text passes through slack-go/slackutilsx.EscapeMessage before posting: &&amp;, <&lt;, >&gt;. Slack sentinels like <!channel>, <@U…>, <#C…>, <!subteam^…>, and the <url|label> link pattern in piped content render as literal text instead of firing mentions or forging links. Markdown blockquotes (> quoted) and bare URLs (https://example.com) still render correctly — Slack decodes the escapes before mrkdwn parsing.

Sending intentional mentions / links

The escape applies to every < > & in Markdown source, including sentinels you typed on purpose. To deliberately ping a user, channel, or user-group, bypass the Markdown converter with --blocks and use Slack's wire syntax inside your JSON:

slick lookup user --filter john.doe -o compact | jq -r '.users[0].id'
# → U123ABC
printf '%s' '[{"type":"section","text":{"type":"mrkdwn","text":"Deploy failed — <@U123ABC>"}}]' \
  | slick message send --channel C1234567890 --blocks --file -

The --blocks path is the deliberate-action escape hatch; <@U…>, <#C…>, <!subteam^S…>, and <url|label> all work there. Look up IDs with lookup user / lookup channel.

Output

Real send:

Message sent channel=C7N2Q8L4P ts=1746284582.123456 permalink=p1746284582123456

When Slack metadata is available in human mode, channel may render as a friendly clickable label such as #deployments or @johndoe. Use JSON output for the raw conversation ID plus channel_name, channel_hr, and channel_url metadata.

--dry-runts is the literal "dry-run", no permalink is emitted, and dry_run=true joins the event:

Message sent channel=C7N2Q8L4P ts=dry-run dry_run=true

Real send:

{
  "meta": {"command": "message.send", "workspace": "default", "timestamp": "2026-05-26T03:00:56Z", "request_id": "337f5bd1-a5f2-4bb8-8da5-510cb801f62d"},
  "data": {
    "message": {
      "type": "message",
      "text": "Deploy complete",
      "ts": "1746284582.123456",
      "channel": "C7N2Q8L4P",
      "channel_name": "deployments",
      "channel_hr": "#deployments",
      "channel_url": "https://app.slack.com/client/T8KQ42P9D/C7N2Q8L4P"
    },
    "permalink": "https://example.slack.com/archives/C7N2Q8L4P/p1746284582123456",
    "attribution": true
  },
  "errors": []
}

--dry-run — no Slack call is made, so ts is the literal string "dry-run", permalink is absent, and dry_run: true joins the data record:

{
  "data": {
    "message": {
      "type": "message",
      "text": "Deploy complete",
      "ts": "dry-run",
      "channel": "C7N2Q8L4P",
      "channel_url": "https://app.slack.com/client/T8KQ42P9D/C7N2Q8L4P"
    },
    "dry_run": true,
    "attribution": true
  }
}

--schedule — scheduled messages have no ts until Slack posts them, so scheduled_message_id is the reference for list/delete:

{
  "data": {
    "channel": "C1234567890",
    "channel_name": "deployments",
    "channel_hr": "#deployments",
    "channel_url": "https://app.slack.com/client/T8KQ42P9D/C1234567890",
    "scheduled_message_id": "Q123",
    "post_at": 1780335600,
    "post_at_iso": "2026-06-01T19:00:00Z",
    "text": "Deploy tomorrow",
    "attribution": {"enabled": true, "label": "slick"}
  }
}

permalink is Slack's chat.getPermalink HTTP URL in JSON. Human mode renders the short form (p1746284582123456) as an OSC 8 terminal hyperlink on supporting terminals.

message edit

Edit your own message. The Slack API permits edits only to messages your token identity sent.

slick message edit --channel C1234567890 --timestamp 1746284582.123456 --message "Corrected text"
slick message edit --channel C1234567890 --timestamp 1746284582.123456 --message "Updated" --dry-run

Flags

Flags
Flag Value Description
-c, --channel <CHANNEL> Channel ID, name, or alias
-t, --timestamp <TS> Message timestamp
-m, --message <TEXT> Message body
-f, --file <FILE> Read message body from file or - for stdin
-b, --blocks Treat message source as raw Block Kit JSON
-n, --dry-run Preview without mutating
Attribution flags

Accepted by the four mutating commands (message send, message edit, reply, file upload):

Flag Value Description
--[no-]attribution Toggle attribution for this command
--attribution-label <LABEL> Override attribution label
--attribution-emoji <EMOJI> Override attribution emoji
--attribution-message <TEXT> Override attribution message

Output

--dry-run:

Message edited channel=C7N2Q8L4P ts=1746284582.123456 dry_run=true

Real edit. Slack's chat.update does not return a permalink, and slick suppresses data.message.blocks even when the edit re-renders Block Kit — verify rendered output via history list or the Slack UI when needed. The attribution field is still present:

{
  "data": {
    "message": {
      "type": "message",
      "text": "Corrected text",
      "ts": "1746284582.123456",
      "channel": "C1234567890"
    },
    "attribution": true
  }
}

message delete

Delete your own message. Requires --force unless you also pass --dry-run.

slick message delete --channel C1234567890 --timestamp 1746284582.123456 --dry-run
slick message delete --channel C1234567890 --timestamp 1746284582.123456 --force

Flags

Flag Value Description
-c, --channel <CHANNEL> Channel ID, name, or alias
-t, --timestamp <TS> Message timestamp
-n, --dry-run Preview without mutating
-F, --force Confirm deletion

Output

Message deleted channel=C7N2Q8L4P ts=1746284582.123456 dry_run=true

The envelope keeps ts (formerly timestamp before v0.4.0):

{
  "data": {"channel": "C7N2Q8L4P", "ts": "1746284582.123456", "dry_run": true}
}

The deleted field was removed in v0.4.0; the action label conveys success and errors[] conveys failure.

message scheduled list

List pending scheduled messages. Pass --channel to narrow the result to one conversation, or omit it to ask the Slack API for the workspace-visible scheduled messages for the active token.

slick message scheduled list --channel C1234567890 --limit 20 --output=json
slick message scheduled list --cursor <next-cursor> --output=json

Flags

Flag Value Description
-c, --channel <CHANNEL> Channel ID, name, or alias
-C, --cursor <CURSOR> Pagination cursor
-L, --limit <N> Maximum scheduled messages to return

Output

Output in a TTY or with --output=human renders a table. CHANNEL uses a readable #channel or @user name when Slack metadata is readable, and DM marks direct-message conversations:

ID           CHANNEL       DM     POST_AT               TEXT
Q123         #deployments  false  2026-06-01T19:00:00Z  Deploy tomorrow
Q124         @johndoe      true   2026-06-01T20:00:00Z  Hello from slick — sched…

When there are no pending messages, human output keeps the same action label and adds count=0:

Scheduled messages retrieved count=0

JSON output uses data.scheduled_messages[] and cursor metadata:

{
  "meta": {
    "command": "message.scheduled.list",
    "pagination": {"next_cursor": "cur-2", "has_more": true, "max_items": 20, "items_returned": 1}
  },
  "data": {
    "scheduled_messages": [
      {
        "id": "Q123",
        "channel": "C1234567890",
        "channel_name": "deployments",
        "channel_hr": "#deployments",
        "channel_url": "https://app.slack.com/client/T8KQ42P9D/C1234567890",
        "channel_type": "channel",
        "is_dm": false,
        "post_at": 1780335600,
        "post_at_iso": "2026-06-01T19:00:00Z",
        "text_preview": "Deploy tomorrow"
      }
    ]
  }
}

For automation, use JSON. The human CHANNEL value is display-only; pass the raw data.scheduled_messages[].channel value back to message scheduled delete or to future scheduled sends. JSON also adds optional channel_name, channel_hr, channel_url, channel_type, channel_user, and is_dm fields when metadata can be resolved.

text_preview is capped at 200 Unicode characters and ends with when truncated.

message scheduled delete

Delete a pending scheduled message by exact channel and scheduled-message ID. No --force flag is required.

slick message scheduled delete --channel C1234567890 --scheduled-id Q123 --dry-run
slick message scheduled delete --channel C1234567890 --scheduled-id Q123 --output=json

Flags

Flag Value Description
-c, --channel <CHANNEL> Channel ID, name, or alias
--scheduled-id <QID> Scheduled message ID returned by scheduled send/list
-n, --dry-run Preview without mutating

JSON output:

{
  "data": {"channel": "C1234567890", "scheduled_message_id": "Q123", "deleted": true}
}

Common errors

Error Cause Fix
validation_error: --channel or --user is required No destination given and default_channel isn't set in config. Provide --channel/--user, or set default_channel.
not_in_channel (not_found, exit 2) App or user is not a member of the channel. Add the bot to the channel, or use a user token that is a member.
channel_not_found Bad channel ID or alias not configured. Verify with lookup channel.
user_not_found Email or alias did not resolve. Use a known Slack user ID or verify via lookup user.
cant_update_message / cant_delete_message (validation_error, exit 4) Slack disallows the edit/delete for this token identity. Use the user token that originally sent the message.
Schedule validation_error --schedule was in the past, beyond 120 days, or not RFC3339/duration/Unix seconds. Use a future RFC3339 timestamp, Go duration, or Unix seconds.
Raw Block Kit validation_error with no stdout data Malformed JSON, unsupported block types, missing required fields, or Slack limits. Inspect the --blocks JSON; the error envelope describes the rule that failed.

See also