Slack Integration
Status: Production-ready · Since: v6.0.0
EDDI's Slack integration enables conversational AI agents — including multi-agent group discussions — to operate natively in Slack channels and direct messages. It supports 1:1 agent conversations, live-streamed panel discussions with multiple agents, trigger-keyword routing, and context-aware threaded follow-ups.
Quick Setup
1. Create a Slack App
Go to api.slack.com/apps → Create New App
Choose From a manifest or From scratch
2. Configure OAuth & Permissions
Add these Bot Token Scopes:
chat:write
Post messages to channels and DMs
app_mentions:read
Respond to @mentions in channels
channels:read
Read channel metadata
channels:history
Read message events in channels
im:read
Read direct message metadata
im:history
Receive DM events
im:write
Send DM responses
3. Install to Workspace
Go to Install App → Install to Workspace
Copy the Bot User OAuth Token (starts with
xoxb-)Copy the Signing Secret from Basic Information
4. Store Credentials in Vault
Store your Slack credentials in EDDI's Secrets Vault:
5. Configure Channel Integration
There are two configuration methods. The recommended approach uses ChannelIntegrationConfiguration (new-style); the legacy ChannelConnector on agents is supported for backward compatibility.
Recommended: ChannelIntegrationConfiguration
Create a channel integration with trigger-keyword routing:
With this configuration:
@EDDI hello→ routes to the default agent@EDDI panel: Should we use microservices?→ triggers the group discussion@EDDI debate: REST vs GraphQL→ triggers the debate group
Legacy: ChannelConnector on Agent
Add a ChannelConnector to your agent configuration:
Note: When both a
ChannelIntegrationConfigurationand a legacyChannelConnectorcover the samechannelId, the new-style config always wins.
6. Enable Direct Messages (App Home)
For the bot to accept DMs, you must enable the Messages Tab:
Go to App Home → Show Tabs
Enable Messages Tab (toggle on)
✅ Check "Allow users to send Slash commands and messages from the messages tab"
⚠️ If this checkbox is unchecked, users will see "Sending messages to this app has been turned off" and cannot DM the bot.
7. Enable Event Subscriptions in Slack
⚠️ This step must come last. When you set the Request URL, Slack immediately sends a signed
url_verificationchallenge. EDDI verifies this using the signing secrets from step 4. If no agent is configured yet, verification fails and Slack rejects the URL.
Go to Event Subscriptions → Enable
Set the Request URL to:
https://<your-eddi-host>/integrations/slack/eventsSlack will verify the URL (you should see a green checkmark)
Subscribe to Bot Events:
app_mention— triggers when the bot is @mentioned in a channelmessage.im— triggers on direct messages to the botmessage.channels— enables thread-reply continuity without @mention
Click Save Changes
Architecture
Key Components
RestSlackWebhook
JAX-RS endpoint, multi-secret signature verification, URL challenge, event dispatching
SlackSignatureVerifier
HMAC-SHA256 verification with multi-secret support and 5-minute replay protection
SlackEventHandler
Core event logic: DM/channel routing, trigger keywords, group triggers, follow-up detection
ChannelTargetRouter
Maps Slack channels → agents/groups with trigger-keyword matching and credential resolution
SlackGroupDiscussionListener
Streams multi-agent discussions into Slack with header+thread UX
SlackWebApiClient
HTTP client for chat.postMessage with Markdown→mrkdwn conversion
Credential Flow
Features
1:1 Agent Conversations
@mention the bot in a channel:
The bot responds in a thread under the user's message.
Direct Messages (DMs)
Send a message directly to the bot — no @mention needed:
DMs are automatically routed to the default agent from any configured Slack integration. Since DM channel IDs are dynamic (unique per user-bot pair), they don't need explicit channel configuration — EDDI resolves to the first available Slack integration's default target.
Note: DMs use
message.imevents (Slack does not fireapp_mentionin DMs). Make suremessage.imis subscribed in your Slack app's event settings.
Trigger Keywords
Use colon-delimited trigger keywords to route to specific targets:
Triggers are case-insensitive. The text after the colon becomes the message sent to the target agent/group. Messages without a trigger keyword route to the default target.
Type @EDDI help to see available trigger keywords for the channel.
Multi-Agent Group Discussions
When a trigger keyword routes to a GROUP target, a multi-agent panel discussion starts. All configured agents in the group participate in a live discussion streamed to Slack.
UX Pattern: Header + Thread
All discussion styles use the same UX pattern — header at channel level, full content in thread:
This pattern keeps the channel scannable while preserving full discussion detail in threads.
Discussion Styles in Slack
Each style produces a distinct phase flow, but all use the same header+thread UX:
ROUND TABLE
Opinion → Synthesis
Each agent posts a channel header; moderator synthesizes
PEER REVIEW
Opinion → Critique → Revision → Synthesis
Peer feedback threads under the target agent's header
DEVIL'S ADVOCATE
Opinion → Challenge → Defense → Synthesis
Challenger threads under the original agent's header
DEBATE
Pro Arguments → Con Arguments → Rebuttals → Judge
PRO and CON agents post separate headers; rebuttals thread under opponents
DELPHI
Anonymous Round 1 → Round 2 (convergence) → Synthesis
Each round's opinions post as headers; convergence visible across rounds
TASK FORCE
Plan → Execute → Verify → Synthesis
Moderator posts plan; agents post task results; verifiers thread under targets; synthesis
TASK_FORCE Events in Slack
The SlackGroupDiscussionListener handles TASK_FORCE-specific events:
onTaskPlanCreated
Posts "📝 Task plan created" (or "pre-configured") with numbered task list and assignments
onSpeakerComplete (EXECUTE phase)
Each agent's task result posts as a channel-level header + thread reply
onTaskVerified
Posts ✅/❌ with task subject, pass/fail status, and moderator feedback
onGroupComplete
Posts "📋 Panel Synthesis" with preview + full content in thread
Peer Feedback Threading
In styles with agent-to-agent feedback (PEER_REVIEW, DEVIL_ADVOCATE, DEBATE), feedback is posted as a thread reply under the target agent's channel header. This creates a natural conversation flow:
Context-Aware Follow-ups
After a discussion, users can reply in an agent's thread to ask follow-up questions:
The follow-up system:
Detects the thread reply is under an agent's message
Retrieves the agent's discussion context (contribution + feedback received)
Injects that context into the prompt
Routes to the correct agent for a contextual response
Markdown Conversion
Agent responses often contain standard Markdown. The SlackWebApiClient automatically converts to Slack's mrkdwn format at the egress point:
**bold**
*bold*
# Heading
*Heading* (bold)
~~strike~~
~strike~
---
─────────── (Unicode line)
Tables (| col |)
Wrapped in ``` code blocks
Code blocks
Preserved unchanged
Enterprise & Clustering
Multi-Workspace Support
Each ChannelIntegrationConfiguration can use different bot tokens and signing secrets, allowing a single EDDI instance to serve multiple Slack workspaces. The ChannelTargetRouter caches all credentials and the SlackSignatureVerifier tries all known signing secrets during webhook verification.
Retry Logic
All Slack API calls use exponential backoff (3 attempts, 500ms/1s/2s base). Failed messages are logged but don't crash the event handler.
Event Deduplication
Slack retries webhook deliveries on timeout. EDDI uses an in-memory cache (ICache) to deduplicate events by event_id, preventing duplicate processing.
Follow-up Memory Management
Active group discussion contexts use EDDI's ICache infrastructure with TTL-based expiration (2 hours for group listeners, 10 minutes for event dedup). This prevents unbounded memory growth from long-lived discussions.
Thread Safety
ChannelTargetRouteruses volatile reference swaps with anAtomicBooleanrefresh gate — no thundering herd on cache expiryEvent processing runs on virtual threads — non-blocking, scales to thousands of concurrent events
The
CountDownLatchinSlackGroupDiscussionListenersignals completion cleanly without polling
Cluster Considerations
When running EDDI as a multi-instance cluster behind a load balancer:
Webhook Delivery: Slack sends each event to ONE URL. The load balancer routes to one EDDI instance. Event dedup is per-instance (ICache), which is fine — Slack only delivers to one endpoint.
Conversation State: Conversations are stored in MongoDB, so any instance can handle follow-up messages. The
IConversationServiceload-balances naturally.Group Discussion Affinity: A group discussion runs on the instance that received the trigger. Since the
SlackGroupDiscussionListenerstreams directly to Slack API, this is instance-local and correct. Follow-up context is cached per-instance in ICache — if a follow-up routes to a different instance, it gracefully falls back to a standard conversation (no context injection, but no error).NATS Integration: When
eddi.messaging.type=nats, conversation processing is ordered via NATS JetStream subjects. The Slack webhook handler still handles event dispatch locally (Slack only talks to one instance), but conversation execution benefits from NATS-backed ordering, retry (3 attempts), and dead-letter queuing.
Configuration Reference
ChannelIntegrationConfiguration (Recommended)
channelType
✅
Must be "slack"
platformConfig.channelId
✅
Slack channel ID (e.g., C0123ABCDEF)
platformConfig.botToken
✅
Bot User OAuth Token. Use vault reference.
platformConfig.signingSecret
✅
Slack Signing Secret. Use vault reference.
defaultTargetName
✅
Name of the target used when no trigger keyword matches
targets[].name
✅
Target name (must match defaultTargetName for the default)
targets[].type
✅
AGENT or GROUP
targets[].targetId
✅
Agent ID or Group Config ID
targets[].triggers
❌
List of trigger keywords (case-insensitive)
Legacy ChannelConnector (on Agent)
Retry & Error Handling
Retry Policy
All outgoing Slack API calls (chat.postMessage) use exponential backoff:
1
0ms (immediate)
0ms
2
500ms
500ms
3
1000ms
1500ms
Only retryable failures trigger retry:
HTTP 429 (Rate Limited)
HTTP 500, 502, 503, 504 (Server Error)
Network errors (connection refused, timeout, DNS failure)
Non-retryable failures (HTTP 200 + ok:false) are logged and skipped:
channel_not_found— bot not in channelinvalid_auth— bad tokennot_in_channel— bot not invited
What Happens After Retry Exhaustion
After 3 failed attempts, the message is permanently lost from the user's perspective. The system:
Logs a structured error for operator alerting:
The agent's response still exists in conversation memory (MongoDB). Operators can manually retrieve it via the conversation API.
The user sees no response in Slack — they can try sending the message again.
Recommended monitoring: Set up a log alert for SLACK_DELIVERY_FAILED in your observability stack (Grafana, Datadog, etc.) to catch delivery failures.
Group Discussion Resilience
During a multi-agent group discussion, individual Slack post failures do not abort the discussion. The SlackGroupDiscussionListener uses a fire-and-forget wrapper (postSafe) that catches delivery exceptions and continues. Users may see a missing agent contribution, but the discussion completes and synthesis is delivered.
Troubleshooting
Bot doesn't respond to @mentions
Integration configured?
Create a ChannelIntegrationConfiguration with the channel's channelId
Bot token configured?
platformConfig.botToken should reference a vault key
Bot in channel?
Invite the bot to the channel in Slack
Event subscription active?
Check Event Subscriptions in Slack app settings
Request URL verified?
Slack must have verified https://<host>/integrations/slack/events
Signing secret set?
Without a signing secret, webhook verification fails (HTTP 403)
Bot doesn't respond to DMs
"Sending messages has been turned off"?
App Home → Messages Tab → ✅ check "Allow users to send Slash commands and messages"
message.im subscribed?
Add message.im to Bot Events in Slack app settings
im:history scope?
Add im:history to Bot Token Scopes and reinstall the app
im:write scope?
Add im:write to Bot Token Scopes and reinstall the app
Any Slack integration configured?
DMs fall back to the first available Slack integration's default target
Signature verification fails (HTTP 403)
Signing secret correct?
Copy from Basic Information in Slack app settings, store in vault
Clock drift?
Timestamp validation uses 5-minute window — sync clocks
Reverse proxy stripping body?
The raw body must reach EDDI unchanged for HMAC verification
No agents configured?
At least one deployed agent must have a Slack integration with signingSecret
Messages appear duplicated
Slack retries events up to 3 times if it doesn't receive HTTP 200 within 3 seconds. EDDI deduplicates by event_id using an in-memory cache (TTL: 10 minutes). If you see duplicates:
Check EDDI response time — if pipeline processing blocks the webhook endpoint, Slack will retry
The webhook endpoint responds immediately (async processing) — if you see slow responses, check network/proxy latency
Group discussion times out
The registerAgentThreadMappings task waits up to 300 seconds. If the group discussion takes longer:
Check agent LLM response times
Consider using fewer agents or simpler discussion styles
Building Custom Channel Integrations
This section is a guide for developers building integrations for other platforms (Teams, Discord, Telegram, etc.) based on lessons learned from the Slack implementation.
Architecture Pattern
Every channel integration follows the same layered pattern:
Key Lessons from the Slack Implementation
Never catch-and-swallow in the API client
The retry wrapper in the handler needs to see failures. Throw for retryable, return null for non-retryable.
Always use TTL caches, not just size-based
Size-based caches keep stale entries indefinitely in low-traffic systems. Use ICacheFactory.getCache(name, Duration).
Use Jackson, not string manipulation
Manual JSON escaping misses control characters. Manual JSON parsing is fragile. Jackson handles both correctly.
Gate cache refresh with AtomicBoolean
Under load, many threads hit the refresh simultaneously. CAS-gate ensures only one thread refreshes.
Use CountDownLatch, not polling
Polling wastes CPU and has latency. CountDownLatch signals instantly.
Fire-and-forget in listeners
A failed Slack post should not crash the entire multi-agent discussion. Wrap in try/catch.
Structured exhaustion logs
After retry exhaustion, log enough context (channel, thread, text length, error) for operator recovery.
Never leak internal IDs to users
Error messages should be generic. Log the details server-side.
All credentials in config
Per-channel credentials via vault references. No server-level secrets.
Convert formatting at the egress point
Markdown→mrkdwn conversion in the API client ensures consistent rendering across all code paths.
Last updated
Was this helpful?