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 |
|
required |
-- |
no |
no (display name only) |
no |
|
A2 |
|
required |
lookup only |
no |
yes |
no |
|
B |
|
-- |
full |
yes |
yes |
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 fieldsteam_id VARCHAR(32),team_name VARCHAR(80),bot_token BYTEA, -- encrypted with masterKeyCipherbot_user_id VARCHAR(32),-- INCOMING_WEBHOOK / INCOMING_WEBHOOK_LOOKUP fieldswebhook_url BYTEA, -- encrypted with masterKeyCipherlookup_bot_token BYTEA, -- encrypted with masterKeyCipher, INCOMING_WEBHOOK_LOOKUP onlycreated_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_instanceON 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 usingmasterKeyCipher(Google Cloud KMS envelope encryption), following the same pattern aswebhook_signing_key.encrypted_private_key. -
team_domainfrom the original plan is removed -- OAuthoauth.v2.accessresponse does not include it.team_nameis 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 ConnectionsName Type Workspace / Channel Default────────────────────────────────────────────────────────────────────────────────────────Company Workspace SLACK_APP company (team) [default]Germany Office SLACK_APP company-de (team)Finance Webhook INCOMING_WEBHOOK_LOOKUP #finance-approvalsPartner 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) |
|
Approver |
Action message (Approve/Reject buttons) |
|
User step activated (NOTIFICATION) |
|
User |
Info message (View issue button only) |
|
Group step activated (APPROVAL/CONSENT) |
|
Group members |
Action message with @mentions |
|
Group step activated (NOTIFICATION) |
|
Group members |
Info message |
|
Email step activated (APPROVAL/CONSENT) |
|
External approver |
Action message (links to AP external page) |
|
Email step activated (NOTIFICATION) |
|
External approver |
Info message |
|
Vote step activated |
|
Voters (group members) |
Vote message (View issue button) |
|
Vote step - moderator notified |
|
Moderator |
Info message about voting started |
Delegate Notifications
|
Event |
EmailTemplateType |
Recipients |
|---|---|---|
|
Delegate call for action |
|
Delegates of the approver |
|
Delegate vote call |
|
Delegates of voters |
|
Delegate moderator notification |
|
Delegates of moderator |
Decision and Completion Notifications
|
Event |
EmailTemplateType |
Recipients |
|---|---|---|
|
Voting ended - moderator action needed |
|
Moderator |
|
Voting ended - delegate action needed |
|
Moderator's delegates |
|
Voting expired - moderator action needed |
|
Moderator |
|
Voting expired - delegate action needed |
|
Moderator's delegates |
|
Watcher notification (step completed) |
|
Issue watchers |
Reminder Notifications
|
Event |
EmailTemplateType |
Recipients |
|---|---|---|
|
Automatic reminder |
|
Approvers who have not yet decided |
|
Manual reminder (from Jira UI) |
|
Selected approvers |
Dynamic Step and Expiration Notifications
|
Event |
EmailTemplateType |
Recipients |
|---|---|---|
|
User dynamically added to step |
|
Added user |
|
User dynamically removed from step |
|
Removed user |
|
Validity expiration |
|
Requester |
|
Decision deadline expired |
|
Requester |
Confluence-Specific Notifications
|
Event |
EmailTemplateType |
Recipients |
Slack Message Type |
|---|---|---|---|
|
Approval outdated (page changed) |
|
Approvers |
Info message |
|
Approval renewal required |
|
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 |
|
|
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_ONLYApproval 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 channelif (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 approversapprover_email VARCHAR(255),connection_id BIGINT NOT NULL REFERENCES slack_connection(id),slack_channel VARCHAR(32) NOT NULL, -- DM channel ID or group channelmessage_ts VARCHAR(32) NOT NULL, -- Slack message timestamp (ID for chat.update)status VARCHAR(16) NOT NULL, -- SENT | UPDATED | FAILEDsent_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),updated_at TIMESTAMP WITH TIME ZONE);CREATE INDEX idx_slack_notification_stepON slack_notification (approval_step_id);CREATE INDEX idx_slack_notification_channel_tsON 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 |
|---|---|---|---|
|
|
WARN |
User {email} not found in Slack workspace "{name}", fallback to email |
Automatic fallback |
|
|
ERROR |
Bot not in channel {channelId} for connection "{name}" -- run |
Admin intervention |
|
|
ERROR |
Channel {channelId} not found -- check connection config "{name}" |
Admin intervention |
|
|
WARN |
Slack rate limit hit for connection "{name}", retrying after {n}s |
Automatic retry |
|
|
ERROR |
Bot Token revoked for connection "{name}" -- reconnect required |
Admin intervention, connection disabled, fallback to email |
|
|
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_APPSlackDeliveryMode.java -- enum: DM, CHANNEL, BOTHSlackFallbackBehavior.java -- enum: EMAIL, SKIP, LOG_ONLYSlackConnection.java -- record (id, hostId, name, type, isDefault, teamId, teamName, ...)SlackConnectionDao.java -- jOOQ DAO for slack_connection tableSlackConnectionService.java -- CRUD + default management + encryption/decryptionSlackConnectionRestController.java -- REST endpoints for connection managementSlackOAuthController.java -- OAuth callback + "Add to Slack" URL generationSlackInteractionController.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, recordsSlackNotificationDao.java -- jOOQ DAO for slack_notification tableSlackNotificationCleanupService.java -- 30-day retention cleanupSlackMessageBuilder.java -- builds Block Kit JSON for each notification typeSlackNotificationRecord.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.tsxSlackSettingsContent.tsxuseSlackSettings.tscomponents/SlackConnectionList.tsx -- table of connections with add/edit/delete/set-defaultSlackConnectionForm.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:
-
Liquibase migrations:
slack_connection_typeenum,slack_connectiontable -
SlackConnectionrecord,SlackConnectionDao,SlackConnectionService(CRUD + encryption viamasterKeyCipher) -
SlackConnectionRestController(CRUD endpoints, test send on save) -
SlackApiClient--postWebhookMessage(webhookUrl, payload)method -
SlackMessageBuilder-- Block Kit JSON for all notification types (action messages, info messages, reminders, delegate messages) -
SlackNotificationService-- connection resolution, message building, delivery orchestration -
Integration into
JiraNotification-- injectSlackNotificationService, add Slack call insendNotification()and in ~4 inline delegate/reminder send points that bypasssendNotification() -
Integration into
StepInitializerorMailNotification-- Slack delivery for EMAIL steps (external approver emails bypass JiraNotification entirely viaMailNotification.sendExternalApproverEmail()) -
Integration into
NotificationService-- Slack delivery for automatic/custom reminders -
InstanceErrorType.SLACK_NOTIFICATION+ DB migration -
Fallback to email on webhook errors
-
Global settings: default connection, fallback behavior
Frontend:
-
Slack settings page: connection list, add webhook form, test button
-
Settings types update
-
Navigation route
Covered notifications: All 19 Jira notification types via webhook (display names, no @mentions).
Phase 2 -- INCOMING_WEBHOOK_LOOKUP (A2)
Backend:
-
SlackUserResolver--users.lookupByEmail+ Caffeine cache (TTL 24h, key: email + connectionId) -
SlackApiClient--lookupByEmail(botToken, email)method -
SlackMessageBuilder-- switch to<@U...>when Slack User IDs are available -
@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)
-
users_not_foundhandling -- graceful degradation to display name per user
Frontend:
-
Webhook form: add optional Bot Token field for lookup mode
Phase 3 -- SLACK_APP (B), DM + chat.update
Backend:
-
Register AP Slack App on api.slack.com (with Interactivity Request URL)
-
SlackOAuthController--/slack/oauth/callbackwith CSRF state validation -
SlackInteractionController--POST /slack/interactionsreturning 200 OK -
SlackApiClient--postMessage,conversationsOpen,chatUpdate,conversationsInfo -
1:1 DM:
chat.postMessage(channel: userId)-> savets(no conversations.open needed) -
Group DM for <= 8 members via
conversations.open(requiresmpim:write) -
Parallel delivery via
CompletableFuturefor multiple recipients -
slack_notificationtable migration +SlackNotificationDao -
chat.updateafter decision (rule ANY: cancel remaining, rule ALL: progress update) -
SlackNotificationCleanupService(30-day retention) -
Channel access validation via
conversations.infoon config save -
Delivery mode configuration (DM / CHANNEL / BOTH) per step
Frontend:
-
"Add to Slack" button in connection form
-
Connected workspaces list with default selector
-
Definition/step forms: Slack connection override, delivery mode, channel selector
Phase 4 -- External User + Per-step Connection Override
Backend:
-
External user support in
SlackUserResolver(email from step definition, no accountId resolution) -
Definition/step level connection and channel override (DB columns + mapping)
Frontend:
-
Step form: connection selector (inherit or override), especially for external approvers who may use a different workspace
Phase 5 -- UX Enhancements
-
Custom Slack message templates per project/step (variables:
{issueKey},{stepName},{deadline},{requesterName},{votingRule}) -
Audit log: every sent/updated Slack message visible in AP Activity
-
token_revokedhandling with admin notification banner in UI -
Delivery mode CHANNEL and BOTH (DM is default from Phase 3; CHANNEL posts to a configured channel with @mentions; BOTH sends DM + channel message)
-
Group DM for <= 8 members via
conversations.open(alternative to individual DMs)