Possibility to send Slack notifications about approvals actions

Description

Slack Notification Integration for Approval Path

Summary

Implement Slack notification delivery for approval actions, supporting direct messages (DM), channel notifications with @mentions, and action buttons (Approve / Reject / View issue) linking to Jira or the external approver page. The integration supports multiple connection modes and multiple Slack workspaces per Jira instance.

Background

Approval Path currently sends notifications via email (21 template types across Jira and Confluence). This feature adds Slack as a parallel notification channel. Action links embedded in messages route to existing authorization mechanisms -- no new auth logic is required.

Scope

In scope: Jira approvals (all step types: USER, GROUP, VOTE, EMAIL, dynamic ISSUE_*_FIELD steps) and Confluence approvals (APPROVAL_OUTDATED_NOTIFICATION, APPROVAL_RENEWAL_NOTIFICATION).


Connection Modes

Three connection modes. Each named Slack Connection has its own mode. Multiple connections of different modes can coexist within a single Jira instance.

Mode

Name (enum)

Webhook URL

Bot Token

DM

@mention

chat.update

A1

INCOMING_WEBHOOK

required

--

no

no (display name only)

no

A2

INCOMING_WEBHOOK_LOOKUP

required

lookup only

no

yes <@U...>

no

B

SLACK_APP

--

full

yes

yes <@U...>

yes

Mode Details

INCOMING_WEBHOOK -- Admin provides a Webhook URL. The webhook is permanently bound to a single channel (Slack limitation -- channel cannot be overridden in the payload). Approvers are identified by display name in message text. No push notifications -- approvers must monitor the configured channel.

INCOMING_WEBHOOK_LOOKUP -- Admin provides a Webhook URL and a Bot Token used exclusively for users.lookupByEmail. Messages are sent via webhook (still bound to one channel); the token is never used for posting. Required bot scopes: users:read, users:read.email.

SLACK_APP -- Full Slack App installed via OAuth 2.0 ("Add to Slack" flow). AP receives and stores the Bot Token per workspace automatically. Required bot scopes: chat:write, im:write, mpim:write, users:read, users:read.email, channels:read, groups:read.

Webhook Channel Limitation (Important)

Slack incoming webhooks are permanently bound to a single channel at creation time. The channel cannot be overridden in the payload. This means:

  • For INCOMING_WEBHOOK and INCOMING_WEBHOOK_LOOKUP modes, one webhook URL = one channel

  • To post to different channels, admins must create separate webhook URLs (and thus separate Slack Connections)

  • Channel configuration at definition/step level is only meaningful for SLACK_APP mode


Slack Connections -- Data Model

CREATE TYPE slack_connection_type AS ENUM (    'INCOMING_WEBHOOK',    'INCOMING_WEBHOOK_LOOKUP',    'SLACK_APP'); CREATE TABLE slack_connection (    id              BIGSERIAL PRIMARY KEY,    host_id         INTEGER NOT NULL REFERENCES atlassian_host(id),    name            VARCHAR(80) NOT NULL,    type            slack_connection_type NOT NULL,    is_default      BOOLEAN NOT NULL DEFAULT FALSE,     -- SLACK_APP fields    team_id         VARCHAR(32),    team_name       VARCHAR(80),    bot_token       BYTEA,                    -- encrypted with masterKeyCipher    bot_user_id     VARCHAR(32),     -- INCOMING_WEBHOOK / INCOMING_WEBHOOK_LOOKUP fields    webhook_url     BYTEA,                    -- encrypted with masterKeyCipher    lookup_bot_token BYTEA,                   -- encrypted with masterKeyCipher, INCOMING_WEBHOOK_LOOKUP only     created_at      TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),    updated_at      TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),     UNIQUE (host_id, name)); -- Enforce single default per instanceCREATE UNIQUE INDEX one_default_slack_connection_per_instance    ON slack_connection (host_id)    WHERE is_default = TRUE; CREATE INDEX idx_slack_connection_host ON slack_connection (host_id);

Notes:

  • Sensitive fields (bot_token, webhook_url, lookup_bot_token) are encrypted using masterKeyCipher (Google Cloud KMS envelope encryption), following the same pattern as webhook_signing_key.encrypted_private_key.

  • team_domain from the original plan is removed -- OAuth oauth.v2.access response does not include it. team_name is sufficient for display.

  • SLACK_APP connections are populated via OAuth flow. INCOMING_WEBHOOK connections require manual paste of webhook_url.


Multiple Workspaces

Multiple Slack Connections can be registered per Jira instance. Connections are listed in Global Settings with one marked as default.

Slack Connections  Name                      Type                        Workspace / Channel       Default  ────────────────────────────────────────────────────────────────────────────────────────  Company Workspace          SLACK_APP                  company (team)            [default]  Germany Office             SLACK_APP                  company-de (team)  Finance Webhook            INCOMING_WEBHOOK_LOOKUP    #finance-approvals  Partner Legal Webhook      INCOMING_WEBHOOK           #partner-legal  [+ Add Slack App]  [+ Add Webhook]

Notification Events -- Complete Coverage

All 21 notification template types are covered (19 Jira + 2 Confluence):

Step Activation Notifications

Event

EmailTemplateType

Recipients

Slack Message Type

User step activated (APPROVAL/CONSENT)

CALL_FOR_ACTION

Approver

Action message (Approve/Reject buttons)

User step activated (NOTIFICATION)

NOTIFICATION

User

Info message (View issue button only)

Group step activated (APPROVAL/CONSENT)

CALL_FOR_ACTION

Group members

Action message with @mentions

Group step activated (NOTIFICATION)

NOTIFICATION

Group members

Info message

Email step activated (APPROVAL/CONSENT)

EXTERNAL_APPROVER_CALL_FOR_ACTION

External approver

Action message (links to AP external page)

Email step activated (NOTIFICATION)

EXTERNAL_APPROVER_NOTIFICATION

External approver

Info message

Vote step activated

CALL_FOR_VOTE

Voters (group members)

Vote message (View issue button)

Vote step - moderator notified

VOTING_MODERATOR_NOTIFICATION

Moderator

Info message about voting started

Delegate Notifications

Event

EmailTemplateType

Recipients

Delegate call for action

CALL_DELEGATES_FOR_ACTION

Delegates of the approver

Delegate vote call

CALL_FOR_VOTE_DELEGATED

Delegates of voters

Delegate moderator notification

VOTING_MODERATOR_NOTIFICATION_DELEGATED

Delegates of moderator

Decision and Completion Notifications

Event

EmailTemplateType

Recipients

Voting ended - moderator action needed

VOTING_ENDED_CALL_FOR_ACTION

Moderator

Voting ended - delegate action needed

VOTING_ENDED_DELEGATED_CALL_FOR_ACTION

Moderator's delegates

Voting expired - moderator action needed

VOTING_EXPIRED_CALL_FOR_ACTION

Moderator

Voting expired - delegate action needed

VOTING_EXPIRED_DELEGATED_CALL_FOR_ACTION

Moderator's delegates

Watcher notification (step completed)

WATCHERS_NOTIFICATION

Issue watchers

Reminder Notifications

Event

EmailTemplateType

Recipients

Automatic reminder

AUTOMATIC_REMINDER

Approvers who have not yet decided

Manual reminder (from Jira UI)

CUSTOM_REMINDER

Selected approvers

Dynamic Step and Expiration Notifications

Event

EmailTemplateType

Recipients

User dynamically added to step

DYNAMIC_STEP_USER_ADDED

Added user

User dynamically removed from step

DYNAMIC_STEP_USER_REMOVED

Removed user

Validity expiration

VALIDITY_EXPIRATION_NOTIFICATION

Requester

Decision deadline expired

DEADLINE_EXPIRATION_NOTIFICATION

Requester

Confluence-Specific Notifications

Event

EmailTemplateType

Recipients

Slack Message Type

Approval outdated (page changed)

APPROVAL_OUTDATED_NOTIFICATION

Approvers

Info message

Approval renewal required

APPROVAL_RENEWAL_NOTIFICATION

Approvers

Info message

ISSUE_*_FIELD Step Types

These steps resolve at initialization time into USER, GROUP, or EMAIL steps. After resolution, they follow the exact same notification flow as their resolved type. No additional Slack logic needed.


Approver Types and Delivery Strategies

Jira User (USER step, ISSUE_USER_FIELD)

Email source: jira.getUserEmails(Set<String> userIds) via Jira REST API, cached via UserCacheService.

Mode

Delivery

INCOMING_WEBHOOK

Display name in message text, posted to webhook's fixed channel

INCOMING_WEBHOOK_LOOKUP

users.lookupByEmail -> <@U...> mention in webhook's fixed channel

SLACK_APP

1:1 DM (default), or channel + @mention, or both

Jira Group (GROUP step, ISSUE_GROUP_FIELD)

Member source: jira.getAllUsersFromGroup(groupId) -> filtered to active atlassian accounts, cached via ApprovalDataCacheService.

SLACK_APP routing by group size:

resolvedUsers = expandGroup() -> lookupByEmail each memberif resolvedUsers.size <= 8 AND mode == GROUP_DM:    conversations.open(users: [...])             -> group DM (mpim)else if mode == DM:    for each user: chat.postMessage(channel: userId)  -> individual 1:1 DMs (parallel)else:    chat.postMessage(channel, "<@U001> <@U002> ...")  -> channel + @mentions

Note on 1:1 DMs: chat.postMessage accepts a user ID (U...) directly as the channel parameter and auto-opens a DM conversation. No need to call conversations.open first. conversations.open is only needed for group DMs (2-8 members, requires mpim:write scope).

@mention batching for large groups (SLACK_APP only): For groups > 50 members, AP sends the main message with the first 50 @mentions via chat.postMessage (which returns ts), then additional batches as thread replies (thread_ts = ts of the main message). This only works for SLACK_APP mode because chat.postMessage returns ts. Incoming webhooks do NOT return tsin the response -- they just return "ok". For webhook modes, all mentions must go in a single message (Slack's Block Kit text limit is 3000 chars, enough for ~150 <@U...> mentions).

INCOMING_WEBHOOK / INCOMING_WEBHOOK_LOOKUP: Always posted to the webhook's fixed channel. No channel override possible.

Individual DMs (SLACK_APP): Sent in parallel using CompletableFuture -- each DM is a separate Slack channel, so the 1 req/s per-channel rate limit does not apply across different recipients. Workspace-wide limit is several hundred messages per minute.

External User (EMAIL step, ISSUE_EMAIL_FIELD)

Email source: Provided directly in the step definition.

AP attempts users.lookupByEmail on the configured connection; on users_not_found it falls back to email notification. External approvers may require a different workspace -- admin can select a different connection per step.

Vote Step (VOTE step)

Voters: Same as Group -- resolved from groupId, delivered to group members. Moderator: Single user, delivered like a User step.Delegates: Both voter delegates and moderator delegates receive separate notifications.

Delegates

When a step is activated, the delegation system (DelegateService.find()) resolves active delegates for the approver. Delegates receive their own Slack notification with CALL_DELEGATES_FOR_ACTION or CALL_FOR_VOTE_DELEGATED template. The message includes the delegator's name for context. Delegate action links embed the delegateConfigId for authorization.


Configuration Hierarchy

Global Settings  +-- list of Slack Connections (one marked as default)  +-- default channel_id (SLACK_APP only) -- optional  +-- fallback behavior: EMAIL | SKIP | LOG_ONLY Approval Definition (overrides global)  +-- Slack Connection ID (select from list or inherit)  +-- channel_id (SLACK_APP only) Step Definition (overrides definition)  +-- Slack Connection ID (select from list or inherit)  +-- channel_id (SLACK_APP only)  +-- delivery mode: DM | CHANNEL | BOTH (SLACK_APP only)

Connection Resolution at Send Time

SlackConnection resolve(ApprovalProcessRequest ctx, DefinitionStep step) {    return firstPresent(        step.getSlackConnectionId(),        ctx.definition().getSlackConnectionId(),        getDefaultSlackConnection(ctx.host())    );} // Channel is only relevant for SLACK_APP modeString channelId = firstPresent(    step.getSlackChannelId(),    ctx.definition().getSlackChannelId(),    getDefaultSlackChannelId(ctx.host())); DeliveryMode mode = connection.type() == SLACK_APP    ? step.getDeliveryMode()    : DeliveryMode.CHANNEL;  // webhooks always post to their fixed channel if (mode.requiresChannel() && connection.type() == SLACK_APP && channelId == null) {    throw new MissingSlackChannelConfigException(step);}

Step Definition UI -- Field Visibility

Field

INCOMING_WEBHOOK

INCOMING_WEBHOOK_LOOKUP

SLACK_APP

Slack Connection

select (inherit)

select (inherit)

select (inherit)

Delivery mode

hidden (always CHANNEL)

hidden (always CHANNEL)

DM / CHANNEL / BOTH

Channel

hidden (fixed in webhook)

hidden (fixed in webhook)

shown when CHANNEL or BOTH

Fallback to email

shown

shown

shown


Email -> Slack User ID Resolution

Applies to INCOMING_WEBHOOK_LOOKUP and SLACK_APP modes.

GET https://slack.com/api/users.lookupByEmail?email={email}Authorization: Bearer {botToken for given connection} -> { ok: true,  user: { id: "U0ABC123" } }  -> cache + use-> { ok: false, error: "users_not_found" }   -> fallback to email

Rate limit: Tier 3 (50+ per minute). For large groups, batch lookups sequentially with respect to this limit.

Cache: email + connection_id -> Slack User ID (Caffeine, TTL 24h). Cache key includes connection_id because the same email may resolve to different User IDs in different workspaces. Cache invalidated on users_not_found error at send time.


Message Structure (Block Kit)

Action Message (APPROVAL/CONSENT steps)

{  "blocks": [    {      "type": "section",      "text": {        "type": "mrkdwn",        "text": "{approversMention}\n*Approval required* -- <{issueUrl}|{issueKey}: {issueSummary}>\n*Step:* {stepName}  |  *Requested by:* {requesterName}  |  *Deadline:* {deadline}"      }    },    {      "type": "context",      "elements": [        { "type": "mrkdwn", "text": "Project: *{projectName}*  |  Rule: *{votingRule}*" }      ]    },    {      "type": "actions",      "block_id": "approval_actions",      "elements": [        {          "type": "button",          "text": { "type": "plain_text", "text": "Approve" },          "style": "primary",          "url": "{approveUrl}",          "action_id": "approve_link"        },        {          "type": "button",          "text": { "type": "plain_text", "text": "Reject" },          "style": "danger",          "url": "{rejectUrl}",          "action_id": "reject_link"        },        {          "type": "button",          "text": { "type": "plain_text", "text": "View issue" },          "url": "{issueUrl}",          "action_id": "view_issue_link"        }      ]    }  ]}

Notification Message (NOTIFICATION action, info-only steps)

Same structure but without Approve/Reject buttons -- only "View issue".

Vote Message

Same structure but buttons are "Vote" (links to Jira issue) and "View issue". Voting happens in Jira UI, not in Slack.

Delegate Message

Same as the approver's message but with additional context line: "Delegating for: *{delegatorName}*".

Reminder Message

Similar to action message but header says "*Reminder:* Approval required" instead of just "*Approval required*".

Updated Message After Decision (SLACK_APP only, via chat.update)

{  "channel": "{savedChannelId}",  "ts": "{savedMessageTs}",  "blocks": [    {      "type": "section",      "text": {        "type": "mrkdwn",        "text": "~*Approval required*~ -- <{issueUrl}|{issueKey}: {issueSummary}>\n{icon} *{action}* by {deciderName}  |  {timestamp}"      }    }  ]}

Rule ANY: After one approver acts, AP calls chat.update on all remaining approvers' messages: "No action required -- {action} by {deciderName}". Rule ALL: AP updates pending approvers' messages with progress: "Approved: 2 of 3 -- waiting for you".

Approvers Mention -- Building Text

String buildApproversMention(    List<ResolvedApprover> approvers,    SlackConnectionType connectionType) {    return approvers.stream()        .map(a -> a.slackUserId() != null            ? "<@" + a.slackUserId() + ">"            : a.displayName())        .collect(Collectors.joining(", "));}
  • INCOMING_WEBHOOK: Jan Kowalski, Anna Nowak

  • INCOMING_WEBHOOK_LOOKUP / SLACK_APP: <@U0ABC1>, <@U0DEF2>


Interaction Endpoint (Required for URL Buttons)

Slack sends an interaction payload when users click URL buttons, even though the URL opens in the browser. The app must acknowledge this with HTTP 200 or users see an error dialog.

POST /slack/interactionsContent-Type: application/x-www-form-urlencodedBody: payload={JSON} Response: 200 OK (empty body)

This endpoint must be registered as the Interactivity Request URL in the Slack App manifest (api.slack.com). It only needs to return 200 -- no business logic required since authorization happens at the target URL.


Slack OAuth Endpoint

GET /slack/oauth/callback?code={code}&state={hostId}

Exchange code via oauth.v2.access with client_id + client_secret. Response includes:

  • access_token (bot token, xoxb-...)

  • bot_user_id

  • team.id

  • team.name

Store encrypted bot_token in slack_connection with type SLACK_APP.

Security: The state parameter must include a CSRF token (validated against the user's session) in addition to hostId, following the same pattern as existing OAuth flows (Gmail/Outlook carriers in OAuth2TokenRepository).


Sent Notifications Store (SLACK_APP only)

Required for chat.update after decisions.

CREATE TABLE slack_notification (    id                BIGSERIAL PRIMARY KEY,    approval_step_id  UUID NOT NULL,    jira_account_id   VARCHAR(64),          -- null for external approvers    approver_email    VARCHAR(255),    connection_id     BIGINT NOT NULL REFERENCES slack_connection(id),    slack_channel     VARCHAR(32) NOT NULL,  -- DM channel ID or group channel    message_ts        VARCHAR(32) NOT NULL,  -- Slack message timestamp (ID for chat.update)    status            VARCHAR(16) NOT NULL,  -- SENT | UPDATED | FAILED    sent_at           TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),    updated_at        TIMESTAMP WITH TIME ZONE); CREATE INDEX idx_slack_notification_step    ON slack_notification (approval_step_id);CREATE INDEX idx_slack_notification_channel_ts    ON slack_notification (connection_id, slack_channel, message_ts);

Cleanup: SlackNotificationCleanupService deletes records older than 30 days, following the same pattern as EmailCleanupService and WebhookCallHistoryCleanupService.


Action Links

Action links are identical for all recipients of a given step -- not personalized per user. Authorization is enforced at click time.

Jira action link:

https://{baseUrl}/browse/{issueKey}?approvalId={id}&action=approve&refId={refId}&collectionId={collectionId}

Verification: Atlassian SSO. AP checks the logged-in user is an approver on the step.

External approver page link:

https://app.approval-path.com/step-action?token={JWT}

Verification: AP account link mechanism. The JWT embeds the step reference; verification occurs on the AP backend at click time.

Delegate action link: Same as above but the JWT includes delegateConfigId for delegate authorization.


Fallback and Error Handling

Fallback Hierarchy Per Recipient

1. Slack DM / channel + mention    <- always attempt first2. Email (existing AP logic)       <- on users_not_found or Slack error3. Instance error entry            <- always, regardless of outcome

Error Reporting

All Slack errors are reported via the existing InstanceErrorService mechanism. Two options for the error type:

Option A: Reuse existing NOTIFICATION type (no DB migration needed). Option B: Add new SLACK_NOTIFICATION value to error_type enum (requires ALTER TYPE error_type ADD VALUE 'SLACK_NOTIFICATION').

Recommendation: Option B -- allows filtering Slack errors separately from email errors in the admin panel.

Error Level Mapping

Using existing InstanceErrorLevel values (ERROR, WARN, INFO). The original plan proposed adding CRITICAL -- this is unnecessary since ERROR already conveys severity and avoids a DB enum migration.

Slack Error

Level

message

Action

users_not_found

WARN

User {email} not found in Slack workspace "{name}", fallback to email

Automatic fallback

not_in_channel

ERROR

Bot not in channel {channelId} for connection "{name}" -- run /invite @approval-path

Admin intervention

channel_not_found

ERROR

Channel {channelId} not found -- check connection config "{name}"

Admin intervention

ratelimited

WARN

Slack rate limit hit for connection "{name}", retrying after {n}s

Automatic retry

token_revoked

ERROR

Bot Token revoked for connection "{name}" -- reconnect required

Admin intervention, connection disabled, fallback to email

invalid_auth

ERROR

Invalid auth for connection "{name}" -- reconfigure required

Admin intervention

Channel Access Validation on Config Save

For SLACK_APP connections: AP calls conversations.info(channel) -- if the bot gets channel_not_found or no_permission, it displays: "Bot does not have access to this channel. Run /invite @approval-path in the channel." This prevents silent failures at notification send time.


Architecture -- Backend Classes

New Packages

src/main/java/ovh/atlasinc/ap/slack/    SlackConnectionType.java           -- enum: INCOMING_WEBHOOK, INCOMING_WEBHOOK_LOOKUP, SLACK_APP    SlackDeliveryMode.java             -- enum: DM, CHANNEL, BOTH    SlackFallbackBehavior.java         -- enum: EMAIL, SKIP, LOG_ONLY    SlackConnection.java               -- record (id, hostId, name, type, isDefault, teamId, teamName, ...)    SlackConnectionDao.java            -- jOOQ DAO for slack_connection table    SlackConnectionService.java        -- CRUD + default management + encryption/decryption    SlackConnectionRestController.java -- REST endpoints for connection management    SlackOAuthController.java          -- OAuth callback + "Add to Slack" URL generation    SlackInteractionController.java    -- POST /slack/interactions (returns 200 OK)    SlackUserResolver.java             -- email -> Slack User ID (lookupByEmail + Caffeine cache)    SlackApiClient.java                -- Slack API wrapper (postMessage, update, lookupByEmail, conversationsOpen, conversationsInfo, oauthAccess)    SlackNotificationService.java      -- orchestrator: resolves connection, builds message, sends, records    SlackNotificationDao.java          -- jOOQ DAO for slack_notification table    SlackNotificationCleanupService.java -- 30-day retention cleanup    SlackMessageBuilder.java           -- builds Block Kit JSON for each notification type    SlackNotificationRecord.java       -- record for slack_notification table row

Modified Classes

-- Notification dispatch (add Slack as parallel channel alongside email)-- There are 3 injection points in the notification flow:--   1. JiraNotification.sendNotification() (line 1293) -- covers most notifications--   2. ~4 inline mailNotification.sendNotifications() calls in JiraNotification--      (delegate and reminder methods that bypass sendNotification())--   3. StepInitializer -> MailNotification.sendExternalApproverEmail() for EMAIL steps--      (bypasses JiraNotification entirely)JiraNotification.java                  -- inject SlackNotificationService, add Slack call in sendNotification()                                       -- and in the ~4 inline delegate/reminder send pointsStepInitializer.java                   -- inject SlackNotificationService for EMAIL step Slack delivery                                       -- (MailNotification.sendExternalApproverEmail bypasses JiraNotification)MailNotification.java                  -- alternative: inject Slack here instead of StepInitializer                                       -- (sends external approver Slack notification alongside email)ConfluenceNotification.java            -- inject SlackNotificationService, add Slack dispatch for all Confluence notification typesApprovalProcess.java                   -- no changes needed (delegates to JiraNotification)NotificationService.java              -- inject SlackNotificationService for reminders -- ConfigurationGlobalSettings.java                    -- add slack fields (defaultConnectionId, defaultChannelId, fallbackBehavior)GlobalSettingsDto.java                 -- add SlackSettingsDtoGlobalSettingsMapper.java              -- map slack fieldsGlobalSettingsRestController.java      -- no changes (generic DTO mapping handles it)GlobalSettingsService.java             -- no changes (generic save handles it) -- Definition / Step configurationApprovalDefinitionDto.java             -- add slackConnectionId, slackChannelIdDefinitionStep (and subtypes)          -- add slackConnectionId, slackChannelId, slackDeliveryModeDefinitionMapper                       -- map new fieldsdefinition_step table                  -- add columns via Liquibase migration -- Error handlingInstanceErrorType.java                 -- add SLACK_NOTIFICATION valuechangelog.xml                          -- ALTER TYPE error_type ADD VALUE 'SLACK_NOTIFICATION' -- Databasechangelog.xml                          -- new changesets for slack_connection, slack_notification tables, enumsjOOQ regeneration needed after migrations

Frontend Changes

-- New settings sub-modulesrc/main/frontend/src/modules/settings/slack/    SlackSettingsPage.tsx    SlackSettingsContent.tsx    useSlackSettings.ts    components/        SlackConnectionList.tsx        -- table of connections with add/edit/delete/set-default        SlackConnectionForm.tsx        -- add/edit form (webhook URL or "Add to Slack" button)        SlackConnectionTestButton.tsx  -- test message send -- Modified filessettings/types.ts                      -- add SlackSettings, SlackConnection typessettings/notifications/               -- add link/reference to Slack settingsdefinitions/common/types.ts           -- add slack fields to step typesdefinitions/form/components/          -- add slack config fields to step formsnavigation                            -- add Slack settings routeservices/api/clients/backend.client.ts -- add Slack API methods

Implementation Phases

Phase 1 -- INCOMING_WEBHOOK (A1) + Infrastructure

Goal: End-to-end Slack notification via webhook for the simplest case.

Backend:

  1. Liquibase migrations: slack_connection_type enum, slack_connection table

  2. SlackConnection record, SlackConnectionDao, SlackConnectionService (CRUD + encryption via masterKeyCipher)

  3. SlackConnectionRestController (CRUD endpoints, test send on save)

  4. SlackApiClient -- postWebhookMessage(webhookUrl, payload) method

  5. SlackMessageBuilder -- Block Kit JSON for all notification types (action messages, info messages, reminders, delegate messages)

  6. SlackNotificationService -- connection resolution, message building, delivery orchestration

  7. Integration into JiraNotification -- inject SlackNotificationService, add Slack call in sendNotification() and in ~4 inline delegate/reminder send points that bypass sendNotification()

  8. Integration into StepInitializer or MailNotification -- Slack delivery for EMAIL steps (external approver emails bypass JiraNotification entirely via MailNotification.sendExternalApproverEmail())

  9. Integration into NotificationService -- Slack delivery for automatic/custom reminders

  10. InstanceErrorType.SLACK_NOTIFICATION + DB migration

  11. Fallback to email on webhook errors

  12. Global settings: default connection, fallback behavior

Frontend:

  1. Slack settings page: connection list, add webhook form, test button

  2. Settings types update

  3. Navigation route

Covered notifications: All 19 Jira notification types via webhook (display names, no @mentions).

Phase 2 -- INCOMING_WEBHOOK_LOOKUP (A2)

Backend:

  1. SlackUserResolver -- users.lookupByEmail + Caffeine cache (TTL 24h, key: email + connectionId)

  2. SlackApiClient -- lookupByEmail(botToken, email) method

  3. SlackMessageBuilder -- switch to <@U...> when Slack User IDs are available

  4. @mention batching for groups > 50 members -- single message for webhooks (no thread_ts available), thread replies for SLACK_APP (via Phase 3's chat.postMessage which returns ts)

  5. users_not_found handling -- graceful degradation to display name per user

Frontend:

  1. Webhook form: add optional Bot Token field for lookup mode

Phase 3 -- SLACK_APP (B), DM + chat.update

Backend:

  1. Register AP Slack App on api.slack.com (with Interactivity Request URL)

  2. SlackOAuthController -- /slack/oauth/callback with CSRF state validation

  3. SlackInteractionController -- POST /slack/interactions returning 200 OK

  4. SlackApiClient -- postMessage, conversationsOpen, chatUpdate, conversationsInfo

  5. 1:1 DM: chat.postMessage(channel: userId) -> save ts (no conversations.open needed)

  6. Group DM for <= 8 members via conversations.open (requires mpim:write)

  7. Parallel delivery via CompletableFuture for multiple recipients

  8. slack_notification table migration + SlackNotificationDao

  9. chat.update after decision (rule ANY: cancel remaining, rule ALL: progress update)

  10. SlackNotificationCleanupService (30-day retention)

  11. Channel access validation via conversations.info on config save

  12. Delivery mode configuration (DM / CHANNEL / BOTH) per step

Frontend:

  1. "Add to Slack" button in connection form

  2. Connected workspaces list with default selector

  3. Definition/step forms: Slack connection override, delivery mode, channel selector

Phase 4 -- External User + Per-step Connection Override

Backend:

  1. External user support in SlackUserResolver (email from step definition, no accountId resolution)

  2. Definition/step level connection and channel override (DB columns + mapping)

Frontend:

  1. Step form: connection selector (inherit or override), especially for external approvers who may use a different workspace

Phase 5 -- UX Enhancements

  1. Custom Slack message templates per project/step (variables: {issueKey}, {stepName}, {deadline}, {requesterName}, {votingRule})

  2. Audit log: every sent/updated Slack message visible in AP Activity

  3. token_revoked handling with admin notification banner in UI

  4. Delivery mode CHANNEL and BOTH (DM is default from Phase 3; CHANNEL posts to a configured channel with @mentions; BOTH sends DM + channel message)

  5. Group DM for <= 8 members via conversations.open (alternative to individual DMs)