Skip to main content

App Management

ChatOps4s automatically generates a Slack app manifest from your registered handlers — the right OAuth scopes, event subscriptions, and slash commands are derived for you.

There are two ways to keep your Slack app in sync with your code:

ApproachMethodWhat it does
Semi-automatedvalidateSetup / checkSetupWrites a manifest file to disk; you copy-paste it into Slack's UI
Fully automatedSlackConfigApi (raw client)Creates and updates the app via Slack's manifest API

Most apps should use the semi-automated approach. The fully automated path is significantly more complex due to Slack's configuration token model.

Semi-automated: Setup Verification

validateSetup

The simplest option. Call it before start — it fails if the manifest is new or has changed, printing instructions to update your Slack app:

// Fails if manifest is new or changed — recommended for development.
slack.validateSetup("MyApp", "slack-manifest.yml")

On each run:

  1. First run — writes the manifest file and fails with a setup guide containing a URL that opens api.slack.com/apps with the manifest pre-filled.
  2. Manifest unchanged — succeeds silently.
  3. Manifest changed (e.g. you added a command) — overwrites the file, fails with a diff and instructions to update your Slack app settings.

This fail-fast behavior is intentional: it surfaces configuration drift immediately during development.

checkSetup

Returns a SetupVerification value instead of failing, so you can handle the result programmatically:

for {
result <- slack.checkSetup("MyApp", "slack-manifest.yml")
_ <- result match {
case SetupVerification.UpToDate =>
IO.println("Manifest is up to date.")
case v: SetupVerification.Created =>
IO.println(s"Manifest created at ${v.path}") *>
IO.println(s"Create your app: ${v.createAppUrl}")
case v: SetupVerification.Changed =>
IO.println(s"Manifest changed at ${v.path}") *>
IO.println(v.diff)
}
} yield ()

The three cases:

CaseFieldsMeaning
UpToDateManifest file matches; nothing to do
Createdpath, createAppUrl, messageFirst run; file written, URL ready for app creation
Changedpath, diff, messageFile updated; diff shows what changed

This is useful when you want to log drift without aborting, integrate with CI checks, or build a custom setup flow.

Custom Manifest Modification

Both methods accept a modifier to tweak the generated manifest before it is written:

_     <- slack.validateSetup(
appName = "MyApp",
manifestPath = "slack-manifest.yml",
modifier = (m: SlackAppManifest) => m.addOutgoingDomains("api.example.com"),
)

Use this when your app needs settings that are not inferred from registered handlers (e.g. outgoing domains, extra scopes).

Fully Automated: Manifest API

For fully automated app management — creating, updating, and deleting Slack apps from code — use SlackConfigApi from the Raw Client. This is the right choice for multi-tenant platforms or CI/CD pipelines that provision Slack apps programmatically.

Configuration Tokens

Slack's manifest API uses configuration tokens (xoxe.xoxp-), which are fundamentally different from bot tokens:

  • They expire after 12 hours.
  • Rotation requires a refresh token (xoxe-), which is single-use — each rotation invalidates the previous refresh token and returns a new one.
  • There are no per-app configuration tokens. Configuration tokens are tied to a workspace admin, not to a specific app.

Token Rotation

RefreshingSlackConfigApi handles automatic token rotation:

val initial = ConfigTokenStore.TokenPair(configToken, refreshToken)
for {
store <- ConfigTokenStore.inMemory[IO](initial)
refreshing <- RefreshingSlackConfigApi.create[IO](backend, store)
// Use withApi to get a SlackConfigApi with a fresh token:
resp <- refreshing.withApi { api =>
api.apps.manifest.create(
apps.manifest.CreateRequest(
manifest = SlackAppManifest(
display_information = DisplayInformation(name = "MyApp"),
).addBotScopes("chat:write"),
),
)
}
} yield ()

withApi checks the token expiry before each call and rotates if needed. It tracks the exp claim from tooling.tokens.rotate and rotates when the token is within 5 minutes of expiry (configurable via refreshMargin).

The ConfigTokenStore trait abstracts token persistence. An in-memory implementation is provided; for production use you'll typically back it with a database or secret manager.

HA Deployment Challenges

Running the manifest API from multiple instances is non-trivial:

  • Single-use refresh tokens: If two instances race to rotate, the loser's refresh token is already invalid. The store implementation must use atomic compare-and-swap (e.g. Redis WATCH/MULTI, database row versioning) and the losing instance must re-read the store to pick up the winner's tokens.
  • No per-app tokens: Configuration tokens are tied to a workspace admin, not scoped to a single app. This means all apps managed by the same admin share one token rotation chain.
  • In-memory store is single-process only: ConfigTokenStore.inMemory does not synchronize across instances.

For these reasons, the semi-automated approach (validateSetup / checkSetup) is simpler and more robust for most deployments.

API Operations

SlackConfigApi exposes the full manifest API:

MethodPurpose
apps.manifest.createCreate a new Slack app from a manifest
apps.manifest.updateUpdate an existing app's manifest
apps.manifest.validateValidate a manifest without creating an app
apps.manifest.exportExport an existing app's current manifest
apps.manifest.deleteDelete a Slack app

See the Raw Client page for more on the four client classes and their token types.