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.