Skip to main content

Basic Operations

The SlackGateway[F] trait provides basic messaging operations. All methods return values wrapped in your effect type F.

Sending Messages

msgId <- slack.send(channel, "Deployment started")

send returns a MessageId (channel + timestamp) that you can use to reply, update, or delete the message later.

Replying in Threads

_     <- slack.reply(msgId, "Step 1 complete")

reply posts a message as a thread reply under the message identified by MessageId.

Updating Messages

_     <- slack.update(msgId, "Deployment finished")

update replaces the text (and optionally the buttons) of an existing message. This is commonly used to replace a message's buttons with a status after a user clicks one.

Deleting Messages

_     <- slack.delete(msgId)

Reactions

_ <- slack.addReaction(msgId, "hourglass_flowing_sand")
_ <- slack.removeReaction(msgId, "hourglass_flowing_sand")
_ <- slack.addReaction(msgId, "white_check_mark")

Reactions are a lightweight way to show status on a message without updating its text.

Ephemeral Messages

slack.sendEphemeral(channel, userId, "Only you can see this")

Ephemeral messages are visible only to the specified user. They're useful for command acknowledgments or error messages that shouldn't clutter the channel.

Buttons on Messages

All message-sending methods (send, reply, update) accept an optional buttons parameter:

slack.send(
channel,
"Approve deployment?",
Seq(
approveBtn.render("Approve", "v1.2.3"),
rejectBtn.render("Reject", "v1.2.3"),
),
)

See Buttons for how to register button handlers.

User Info Cache

getUserInfo fetches user profile data from Slack and caches it in memory (15-minute TTL, 1000 entries by default):

info <- slack.getUserInfo(userId)
// info.profile.flatMap(_.email) — user's email
// info.profile.flatMap(_.real_name) — display name

You can provide a custom cache implementation:

customCache <- UserInfoCache.inMemory[IO](ttl = Duration.ofMinutes(30), maxEntries = 5000)
_ <- slack.withUserInfoCache(customCache)

Or disable caching entirely with UserInfoCache.noCache.

Idempotent Sending

send and reply accept an optional idempotencyKey parameter. When provided, the gateway checks for a recently sent message with the same key before posting a new one. If a match is found, the existing MessageId is returned and no duplicate message is sent.

msgId <- slack.send(channel, "Deployment started", idempotencyKey = Some(IdempotencyKey("deploy-v1.2.3")))

This also works for thread replies:

_ <- slack.reply(msgId, "Step 1 complete", idempotencyKey = Some(IdempotencyKey("step-1")))

The key is embedded in the message's metadata field and is used for duplicate detection.

How it works

By default, the gateway uses a Slack scan strategy: before posting, it calls conversations.history (or conversations.replies for threads) and looks for a message whose metadata contains the matching key. This works across restarts since the key is persisted on the message itself.

Customizing the check

You can swap the idempotency strategy using withIdempotencyCheck:

check <- IdempotencyCheck.inMemory[IO](ttl = Duration.ofMinutes(30), maxEntries = 5000)
_ <- slack.withIdempotencyCheck(check)

Available implementations:

  • IdempotencyCheck.slackScan (default) — scans recent Slack messages via the API. Survives restarts and works across multiple app instances, but makes an API call per send.
  • IdempotencyCheck.inMemory — fast in-memory cache with configurable TTL and max entries. No extra API calls, but state is lost on restart and not shared across multiple app instances — each instance maintains its own cache, so duplicates can still occur if the same key is sent from different instances.
  • IdempotencyCheck.noCheck — disables idempotency checks entirely. Messages are always sent.

The IdempotencyCheck trait is simple to implement, so you can provide your own backed by Redis, a database, or any shared store if you need cross-instance deduplication without the per-send Slack API cost.

Caveats

  • Race condition: If two processes send with the same key simultaneously, both may send before either's message appears in the history scan. The inMemory check has the same limitation across instances.
  • Rate limiting: The default slackScan makes a conversations.history/conversations.replies call for each send with a key. For high-volume use, prefer inMemory or a custom implementation.
  • update and delete are not affected — they are already naturally idempotent by MessageId.

Error Handling

By default, exceptions thrown in interaction handlers (buttons, commands, forms) are logged and swallowed so the WebSocket connection stays alive. You can replace the default handler with onError:

slack.onError { error =>
IO.println(s"Handler error: ${error.getMessage}")
}

This is useful for sending errors to an alerting system or logging them in a structured format. The handler replaces any previously set handler.