Sitemap
Business4s Blog

Space for the articles related to business4s.org community

ChatOps4s — You Might Actually Build That Slack Integration Now

6 min readMar 2, 2026

--

Do you know that feeling? Someone on the team says “it would be nice if we could deploy from Slack” and you think — yeah, that would be nice. Let’s do a /deploy command and put an approval flow on top. How hard can it be?

Then you open the Slack API docs.

Seven types of tokens, dozens of OAuth scopes with subtle interplay between bot scopes and user scopes. Do you want HTTP mode (you’ll need an internet-facing URL, SSL certificates, and webhook endpoints) or Socket Mode (WebSocket, but a different token type and a different setup flow)? How should you manage your app: manually (and swallow the pain) or through rather arcane manifests, even more complicated app management API, and overpowered config tokens (that create security issue)?

By this point, you haven’t written a single line of application logic and you’re done. The perceived hassle wins — you close the browser tab and add it to the backlog, where it quietly dies.

I kept running into this pattern across projects. The Slack API is powerful, but the mental overhead of getting started is wildly disproportionate to what most teams actually need — a simple command + a few buttons + (maybe) a form. So I built ChatOps4s.

One scenario, start to finish

Here’s the deploy approval flow — the thing that felt like too much work few lines above. A /deploy command posts a message with Approve and Reject buttons. Clicking either updates the message in place.

for {
slack <- SlackGateway.create(backend)
approveBtn <- slack.registerButton[Version] { click =>
slack.update(click.messageId, s"Approved by ${click.userId.mention}").void
}
rejectBtn <- slack.registerButton[Version] { click =>
slack.update(click.messageId, s"Rejected by ${click.userId.mention}").void
}
_ <- slack.registerCommand[DeployInputs]("deploy", "Deploy to production") { cmd =>
slack
.send(
channel,
s"Deploy ${cmd.args.version}?",
Seq(
approveBtn.render("Approve", cmd.args.version),
rejectBtn.render("Reject", cmd.args.version),
),
)
.as(CommandResponse.Silent)
}
_ <- slack.validateSetup("MyApp", "slack-manifest.yml")
_ <- slack.start(botToken, Some(appToken))
} yield ()

That’s the entire app. A for-comprehension that reads top to bottom: create a gateway, register two buttons with typed payloads, register a command that sends a message with those buttons, validate the setup, start.

The [Version] type parameter on registerButton is a typed payload. When a user clicks Approve, your handler receives a ButtonClick[Version] with the "Version(v1.2.3)" value already decoded. No parsing raw JSON payloads or matching on action IDs. The type system carries the context through for you.

Similarly for commands: registerCommand[DeployInputs] parses the command text into a DeployInputs, -- case class DeployInputs(service: ServiceName, version: Version) derives CommandParser gives you typed argument parsing from a single line.

Press enter or click to view image in full size
Video created from this example.

You never configure scopes manually again

That validateSetup call does something slightly unusual: it inspects every handler you've registered (buttons, commands, forms), figures out which Slack scopes and event subscriptions your app actually needs, and generates a complete app manifest. It writes the manifest file and prints setup instructions with a direct link to Slack's app configuration page.

On your first run, the app won’t start — instead, it prints a setup tutorial:

Slack manifest written to slack-manifest.ymlThis looks like a first-time setup. To create your Slack app:1. Open this URL to create the app from the manifest:
https://api.slack.com/apps?new_app=1&manifest_json=...
2. Once created, install the app to your workspace. (App settings > Install App)3. Use the following tokens for SlackGateway.start:
bot token: xoxb-... (from OAuth & Permissions > Bot User OAuth Token)
app token: xapp-... (from Basic Information > App-Level Tokens, with connections:write scope)

And if you later add a new handler that requires different scopes, it detects the drift and shows you exactly what changed:

Slack manifest has changed. Updated slack-manifest.ymlPlease update your Slack app manifest to match:
App settings > App Manifest > paste the contents of slack-manifest.yml

You don’t have to guess which scopes to add, and you don’t end up in the “nothing happens” situation because you missed an event subscription.

Is it annoyingly manual still? Yeah, a bit, but going for full automation here is a real pandora box. And you can still do it if you wish. But this strikes a sweet balance between effort and control.

Forms from types

ChatOps4s can also derive full Slack modals from case classes:

case class DeployForm(service: String, version: String, dryRun: Boolean) derives FormDef

That one-liner gives you a Slack modal with a text input for service, a text input for version, and a checkbox for dryRun — all with correct Slack block types. We support 15+ field types out of the box.

Press enter or click to view image in full size
Video showing the workings of this example.

More than a client

Once you move beyond “hello world”, operational issues start showing up. Your bot restarts mid-deploy and posts the approval twice. You’re resolving user profiles on every interaction and hitting rate limits. A token expires and the socket quietly drops.

ChatOps4s handles the common edge cases out of the box: idempotent message sending, a lightweight user profile cache, automatic socket reconnection with retry, token rotation, and pluggable error handling. It’s still a Slack integration — but you don’t have to rebuild the same safety rails every time.

What you might build

The deploy approval flow is just one example. The same primitives — commands, forms, buttons, message updates — compose into most chatops workflows.

  • An /incident command that opens a severity form and posts to an incidents channel with an Acknowledge button.
  • A /grant-access flow where security approves via buttons and the requester gets an ephemeral confirmation.
  • A /scale api 5 command where typed arguments flow through to a Revert button carrying the previous state.

The pattern repeats: capture intent, collect data, capture decisions, update status. Compose the pieces however you need.

Where this fits

I should be honest about what ChatOps4s is and isn’t. Bolt is probably much more complete, stable, and bullet-proof. On the other hand, ChatOps4s is native to Scala, integrates seamlessly with the ecosystem and patterns, and is much more type-safe with all its codecs and derivation.

ChatOps4s is early stage. It only supports Socket Mode and it’s not a full Slack framework — it’s an opinionated layer for the most common chatops patterns. If you need granular control over every Slack API surface, Bolt might be a better choice. If you want to wire up a deploy command in twenty lines of Scala and move on with your day, ChatOps4s is your guy.

If you need something that is not exposed directly, you can always drop down to the underlying Slack client. The raw client covers a large portion of the Web API, and since it’s built on top of sttp, adding a missing endpoint is rather trivial.

One more word about Slack Workflows: they are great for quick automation, but they’re not a serious place to encode business logic. They’re hard to version, hard to test, and tightly coupled to Slack itself. My bias is that logic belongs in code — reviewed, versioned, and testable. Slack should be a UI surface over that logic, not the place where it lives. ChatOps4s reflects that bias.

Try it

ChatOps4s is part of Business4s, a collection of pragmatic Scala libraries for real business problems — alongside Workflows4s for business processes and Decisions4s for decision logic. They’re not tightly integrated because there is no integration needed. In their raw state they fit together naturally, and combining them lets you describe surprisingly complex behaviors with fairly small amounts of code.

Add the dependency and build something:

"org.business4s" %% "chatops4s-slack" % "0.1.0"

--

--

Voytek Pituła
Voytek Pituła

Written by Voytek Pituła

Generalist. An absolute expert in faking expertise. Claimant to the title of The Laziest Person in Existence. Staff Engineer @ SwissBorg.