agent-swarm.devagent-swarm.dev
Integrations

Slack Integration

Connect Agent Swarm to Slack — @mention agents to create tasks, use swarm# syntax to target specific workers, and manage multi-agent workflows via direct messages, channels, and the Slack Assistant sidebar.

Enable Slack for task creation, agent communication, and interactive workflows via direct messages and the Slack Assistant sidebar.

Setup

  1. Create a Slack App
  2. Enable Socket Mode (for real-time events without public webhooks)
  3. Enable Interactivity (for action buttons and modals)
  4. Enable Agent View (for sidebar conversations)
  5. Add required bot token scopes:
    • app_mentions:read
    • assistant:write
    • channels:history, channels:join, channels:manage, channels:read
    • chat:write, chat:write.customize, chat:write.public
    • commands
    • files:read, files:write
    • groups:history, groups:read, groups:write
    • im:history, im:read, im:write
    • mpim:history, mpim:read, mpim:write
    • reactions:write
    • users:read
  6. Subscribe to bot events: app_mention, assistant_thread_started, assistant_thread_context_changed, entity_details_requested, message.channels, message.groups, message.im, message.mpim
  7. Install to your workspace and copy tokens

A ready-to-use slack-manifest.json is included in the repository root — import it directly in the Slack App configuration page to set up all scopes, events, and features automatically. After adding channel-management scopes to an existing app, reinstall the app to the workspace so Slack grants them to the bot token.

The manifest's oauth_config.redirect_urls lists two generic placeholders (a hosted-cloud callback and a web.localhost dev callback). Replace or extend these with your own deployment's HTTPS callback URL(s) before installing — do not reuse another deployment's callback host.

Token rotation is off (settings.token_rotation_enabled: false) because the app runs under Socket Mode with a static SLACK_BOT_TOKEN; there is no refresh-token exchange in this codebase. Rotate or revoke compromised tokens from the Slack App configuration page (OAuth & Permissions → reinstall, or Manage Distribution → revoke).

Configuration

# Socket Mode (default and currently available)
SLACK_MODE=socket
SLACK_BOT_TOKEN=xoxb-...      # Bot User OAuth Token
SLACK_APP_TOKEN=xapp-...      # App-Level Token

# HTTP credential contract (receiver not available yet)
# SLACK_MODE=http
# SLACK_BOT_TOKEN=xoxb-...
# SLACK_SIGNING_SECRET=...

# Disable Slack (if not using)
SLACK_DISABLE=true

SLACK_MODE accepts only socket or http and defaults to socket. Invalid values fail closed. In the current phase, selecting http validates the bot token and signing-secret configuration but deliberately leaves Slack unavailable; it does not open a Socket Mode fallback, HTTP route, listener, or ingress. Keep socket selected until the signed HTTP receiver is installed.

Development API processes do not open Socket Mode by default, even when Slack tokens are present in the ambient environment. This prevents a local bun run start:http process from consuming events intended for production. Set SLACK_ALLOW_DEV_SOCKET_MODE=true only when that development process must connect to the configured Slack app; the server logs the blocked reason and this opt-in name otherwise.

How It Works

Task Work Objects

Set SLACK_WORK_OBJECTS_ENABLED=true on the API to populate task Work Object flexpanes. This feature defaults to off. Apply the repository's updated slack-manifest.json, reinstall the Slack app, and enable Work Object Previews in the app settings.

Supported cards use external_ref: { "type": "task", "id": "<full task UUID>" }. The pane shows the task's current title, description, status, assignee, timestamps, and result, failure, or progress. It respects the configured Slack user filters and requires the task's Slack channel to match the requesting channel. Forwarded cards in other channels and tasks without Slack context receive a restricted view.

The handler responds to entity_details_requested events; this flag does not add automatic link unfurls. If a card does not trigger that event, it cannot populate a pane. See the deployment smoke test for the card payload and event-delivery check.

Creating Tasks

@mention the bot in Slack to create tasks. All Slack messages are routed directly as tasks — there is no separate inbox system.

Routing priority:

  1. swarm#<uuid> — explicit agent targeting (always wins)
  2. swarm#all — broadcast to all workers
  3. Thread follow-up — if in a thread where a worker is already active, or the thread was originally started by the swarm, routes directly to that worker/flow
  4. Lead fallback — if the bot was @mentioned and no other match, routes to the lead agent

If no agents are online, the message is queued as an unassigned task in the pool. The bot confirms that your request has been queued and will be processed when agents come back up.

The bot's own @mention is identified as “that's you” in task text from assistant DMs and buffered follow-ups, including their thread context. Other user mentions resolve to names when available.

Acceptance reactions

When the swarm accepts a Slack message, it adds an :eyes: reaction after it has successfully created or queued the task. A thread message accepted as steering for a running task receives :speech_balloon: by default. If ingestion fails before the request is accepted, the bot leaves the message unreacted instead of implying that work started; repeated delivery of an already-acknowledged event is treated as a harmless no-op.

Additive thread buffering uses a slightly richer vocabulary: :eyes: for the first captured message, :heavy_plus_sign: for later messages appended to that buffer, and :zap: when !now triggers an immediate flush.

Each of the 6 reactions is configurable through a swarm_config key, scope global. An unset key keeps the default shown below.

KeyEventDefault
SLACK_REACTION_ACCEPTEDTask accepted from a channel mention, thread reply, follow-up or assistant DM. Also the first message in an additive buffer.eyes
SLACK_REACTION_BUFFEREDMessage 2 and later in an additive buffer window.heavy_plus_sign
SLACK_REACTION_NOWThe !now command flushes the buffer.zap
SLACK_REACTION_STEEREDA thread message is accepted as steering for a running task.speech_balloon
SLACK_REACTION_COMPLETEDEvery task linked to the trigger message reached status completed.white_check_mark
SLACK_REACTION_FAILEDAny linked task reached failed, cancelled or superseded.x

A value is trimmed, has at most one leading and one trailing colon stripped, and is lowercased before Slack sees it — :ThumbsUp: and thumbsup both resolve to thumbsup. A value that is empty afterward, or contains anything outside lowercase letters, digits, _, +, ' and -, is rejected at write time and falls back to the default at read time.

If Slack rejects a configured name with invalid_name (the emoji does not exist in the workspace), the bot logs one error-level line, increments an OTel counter, and retries once with the default for that event before giving up on the reaction. Task state and the outcome card never depend on a reaction landing. A terminal reaction (completed or failed) is never removed once added.

When a task finalizes, the bot removes the acceptance-stage reactions and adds the terminal reaction. The removal list is the configured name for each of the four acceptance-stage events (SLACK_REACTION_ACCEPTED, SLACK_REACTION_BUFFERED, SLACK_REACTION_NOW, SLACK_REACTION_STEERED). With no configuration, that list is eyes, heavy_plus_sign, zap and speech_balloon, the same fixed list the bot removed before these keys existed. The bot keeps no record of which reaction it applied to which message, so an API restart between acceptance and finalization does not change the result. reactions.remove only removes the bot's own reaction, so a reaction that a person or another bot applied with the same name stays in place. If you change an acceptance-stage key while a task is active, the bot removes the new name at finalization and the old reaction stays on the message.

Terminal reply recovery

Pending terminal replies are stored in the database and retried after API restarts. The watcher marks a reply delivered only after Slack accepts it. When a persisted progress message is available, it updates that message to the final result; if Slack reports that the message no longer exists, it sends a replacement. Task-tree messages retain their own final update path.

Thread Follow-up Routing

When you @mention the bot in a thread where a worker is already handling a task, the message routes directly to that worker — no lead delegation needed. This keeps conversations flowing naturally.

If the assigned worker is offline or unavailable, the follow-up routes to the lead agent instead of dropping the message. The lead picks up the thread context and continues the conversation, preserving parentTaskId continuity for chained tasks.

By default, thread follow-ups route automatically without requiring an @mention. That includes human replies to swarm-started root messages, even when no task row existed yet for the thread. Set SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION=true to require an explicit @mention for thread follow-up routing — non-mention thread messages will be silently dropped instead of auto-routing.

Live steering instead of follow-up tasks

Slack can send buffered thread feedback into a task that is still running:

VariableDefaultBehavior
SLACK_THREAD_STEERINGleadlead targets the latest in-progress lead task in the thread; all targets the latest active task regardless of role. Explicit off and invalid values preserve normal follow-up task routing.
SLACK_THREAD_STEERING_MODEqueuequeue adds the message at a turn boundary. steer requests an interrupt and degrades when the harness cannot interrupt.

This changes a thread reply from a durable follow-up task into input for an already-running task. Slack posts an acknowledgement that reflects the actual server outcome (steered, queued, or promoted to a follow-up).

Follow-up re-delegation guard: When the lead receives a task.worker.completed or task.worker.failed follow-up, it is explicitly instructed (via prompt template) not to re-delegate the same work back to a worker. A second guard in send-task blocks any re-delegation on a Slack thread that already has a completed task within the last 48 hours, preventing the duplicate-response cycle where repeated re-delegations caused the bot to answer the same thread multiple times.

Slack Context Propagation

Slack metadata (slackChannelId, slackThreadTs, slackUserId) is auto-inherited from the creator's current task. When a lead delegates work from a Slack-originated task, workers automatically receive the Slack context and post progress updates to the originating thread — no manual metadata passing needed.

The auto-inheritance works via the X-Source-Task-Id header, which links the new task back to the creator's active task to look up Slack metadata.

For cases where auto-inheritance isn't available (e.g., programmatic task creation without an active parent task), you can pass Slack metadata explicitly on send-task:

  • slackChannelId — Channel ID for progress updates
  • slackThreadTs — Thread timestamp for thread-level updates
  • slackUserId — Original requester's Slack user ID

Treat Slack channel/thread metadata as one routing unit. When a parent task or Slack-family context key already defines the route, send-task rejects a different explicit channel or thread instead of silently sending updates elsewhere. Omit the explicit Slack fields to inherit the existing route. For an intentional cross-channel handoff, provide both slackChannelId and slackThreadTs and set overrideSlackContext: true; the override is logged for audit.

Additive Slack Buffer

When enabled via ADDITIVE_SLACK=true, thread replies that do NOT @mention the bot are captured, buffered, and batched into a single follow-up task. This allows multi-message feedback without requiring an @mention each time.

  • Messages are buffered for a configurable debounce window (ADDITIVE_SLACK_BUFFER_MS, default 10s)
  • Buffered messages are flushed into a single task with dependency chaining to the active task
  • Use the !now command to flush the buffer immediately — skips dependency chaining so the task starts right away
  • Reactions provide visual feedback: :eyes: for first captured message, :heavy_plus_sign: for subsequent appended messages, :zap: for !now
  • When SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION=true, the additive buffer is disabled for non-mention messages

Tree-Based Status Messages

The v2 renderer is enabled by default. Set SLACK_RENDER_V2=false to use the legacy renderer. With v2, each Slack thread owns exactly one engine message: a task tree that is updated in place with chat.update. Later asks append to the same tree, delegated tasks stay nested under their parent, and task IDs remain clickable:

🧵 worked for 44m
 ├─ ✅ First ask · 7m51s · task-id
 ├─ ⏳ Current ask · 8m05s · task-id · latest progress
 │  └─ ✅ Researcher · 8m24s · task-id
 └─ ✅ Completed ask · 12m · task-id

The tree is rendered as footer-weight context blocks and contains status, elapsed time, hierarchy, task links, and the latest bounded progress for active tasks. It does not repeat worker output or cross-link Slack messages: keeping message permalinks out of the tree prevents Slack from producing noisy link previews. Updates are debounced, link/media unfurls are disabled, and rate-limit retries honor Slack backoff.

Working status

With the v2 renderer, Slack's own "working" state follows the whole life of an ask, in channel threads and DMs, so anyone reading a long thread can tell whether the swarm is still on it. It uses agents.sessions.setStatus, which needs only the chat:write bot scope. There is no manifest change and no reinstall.

Thread stateStatus
Any task in the thread is running or queuedprocessing: Slack shows its loading indicator
Nothing is running, and a request-human-input request from the thread is still opensuspended: waiting on a person
Everything is finished, or the ask is deferred and waits for its wake-upactive: indicator cleared

The status starts when the ask is accepted, is re-asserted every 30 minutes because Slack ends a processing session after one hour, and is cleared when the outcome card lands. If a second ask in the thread is still running when the first card lands, the indicator stays. The swarm does not subscribe to agent_session_stopped, so Slack shows no Stop button. Cancelling still goes through the task itself.

Setting the status also opens the thread for the person who asked, and Slack's free-text status (is working on your request...) is not available with this method, so Slack shows its standard loading indicator.

The indicator never blocks a task. If Slack refuses the call, the swarm logs it once and keeps the :eyes: reaction and the tree message:

  • Workspace or app cannot use it (missing_scope, feature_disabled, a revoked token): the native call is switched off for an hour, then probed again. DMs keep the legacy assistant.threads.setStatus indicator.
  • Slack refuses one thread (for example the bot is not in the channel): that thread is left alone until it goes idle.
  • Transient errors (rate limit, timeout, internal_error): retried with backoff by the swarm, never by the Slack client. At most eight status writes start per render tick, counting tree creation, tree recovery, and outcome delivery; threads past the cap catch up on the next tick.
  • A write that gets no answer (timeout, reset): status calls use their own Slack client, separate from message delivery, with no automatic retries and an 8 second limit that aborts the request. Slack may still have applied the write, so the next reconcile sends its status even if the thread looked already clear. A write that lands after the timeout is reconciled against the thread's tasks again.

Outcome Cards and Message Provenance

Each completed Slack ask gets one outcome card containing the full multi-paragraph Markdown result, bounded only by Slack's presentation limit with a link to the full task when truncation is required. If the agent already delivered the result with slack-reply, the outcome card collapses to a compact completion instead of repeating that message. The renderer re-reads this delivery marker before finalizing, so a retry cannot preserve stale duplicate content. Outcome cards do not link back to the tree, which avoids an otherwise redundant Slack permalink unfurl.

If Slack refuses the card with a terminal error, or if 5 attempts fail, the renderer records the give-up on the card's database row. It posts one ⚠️ Couldn't deliver this task's reply warning in the thread. It never retries that card, and a restart does not re-arm it.

Slack shows a deferral as waiting, with the check-back time or the agents being watched and a latest wake-up time. Times use the requester's timezone, falling back to labeled UTC. The same card updates when the continuation reaches its final outcome. Internal notes, checks, and schedule details remain in the task log.

A continuation normally resolves its parent's waiting card in place, without posting a duplicate outcome. If it defers again, it creates a new waiting card and the previous card points to it. A continuation posts its own outcome when the original waiting card is missing or cannot be updated. Ordinary scheduled tasks without Slack context remain silent.

Terminal task output is automatically delivered through the thread's outcome card. No additional relay message is created: extra messages only appear when an agent explicitly calls slack-reply, slack-post, or slack-start-thread. When slack-reply is used, the outcome card is compacted instead of duplicating that explicit reply. These tools accept optional Block Kit blocks; when omitted they generate a mrkdwn section. Their compact context footer contains the originating agent and task without a Slack-message permalink.

Tree, outcome, and explicit agent message timestamps are persisted in slack_messages, so the renderer can reuse messages after a restart.

Delegated-Result Delivery and the Deferred Conclusion Card

Set SLACK_RENDER_V2_DELEGATION=true (requires SLACK_RENDER_V2=true) to opt in to a lifecycle built around each ask's closure: the ask task plus every delegated task, transitive child, follow-up task, resume task, and reroute-decision task reachable from it in the same thread.

  • Tree glyphs. Each task line shows one state glyph — queued 🕒, blocked ⛔, starting ▶️, running 🔄, stalled ⚠️, paused ⏸️, done ✅, failed ❌, cancelled 🚫, superseded ↪️ — recomputed on every tick. The thread header summarizes the closure as working, stalled, done, done with failures, or concluded with unfinished work. Glyphs ship under SLACK_RENDER_V2 alone; they do not require the delegation flag.
  • Child result cards. A terminal delegated task (completed or failed, not a follow-up or reroute-decision control-plane task) gets its own card in the thread once its output has nowhere else to go — capped at 3 new cards per thread per tick and 10 per ask closure. A child that already delivered its answer with slack-reply does not get a duplicate card; children past the cap appear as a one-line digest in the conclusion card instead.
  • Deferred conclusion card. The ask's own outcome card does not post while its closure is open. It posts once every closure member is terminal and the closure has been quiet for SLACK_CONCLUSION_SETTLE_SEC seconds, or once the closure has been idle for SLACK_CONCLUSION_TIMEOUT_MIN minutes, whichever comes first. A settled conclusion lists each child's result under a Results section; a timed-out conclusion is marked "Concluded with unfinished work" and links every still-open member. Cards remain immutable once finalized — the deferral changes only when the card is written, not the immutability contract.
  • Reaction gate. The trigger message's checkmark reaction now flips only when the ask's conclusion card finalizes, not when the ask's own task completes. Outcome mapping: any failed closure member → x; a timed-out closure → warning; otherwise → white_check_mark. A closure where every member is cancelled and none failed still resolves to white_check_mark — a cancellation is not a failure.
  • In-flight threads. The first tick with the flag on stamps delegation_activated_at. An ask created before that stamp keeps the pre-delegation behavior: its own outcome card, no child cards, and the reaction gate keyed to its own completion. An ask created after the stamp gets the new lifecycle. One thread can carry both kinds of ask at once.
  • Rollback. Set SLACK_RENDER_V2_DELEGATION=false. The next tick reverts every closure to the pre-delegation gate; any ask that was deferred while the flag was on finalizes immediately under the old rule, so no thread is left stranded.

Opt out of the renderer

Set SLACK_RENDER_V2=false to use the legacy per-task assignment/progress/completion renderer. Leaving it unset enables the task tree and streamed outcome cards.

Rich Block Kit Messages

The v2 tree uses mrkdwn inside footer-weight context blocks, which keeps every task ID clickable. Outcome and explicit agent messages use Block Kit where it adds structure:

  • Context blocks for the compact provenance footer
  • Section blocks for explicitly supplied agent content
  • Markdown is automatically converted to Slack's mrkdwn format

Interactive Actions

The legacy renderer's task messages include interactive buttons when SLACK_RENDER_V2=false:

  • Follow-up — Opens a modal to send a follow-up message to the same agent, creating a new task with dependency on the completed one
  • View Full Logs — Links to the task detail page in the dashboard
  • Cancel — Shows a confirmation dialog before cancelling an in-progress task

Assistant Sidebar

The bot supports Slack's Assistant sidebar for direct conversations:

  • Open the sidebar in any channel or DM to start a conversation
  • Suggested prompts help you get started ("Check agent status", "Assign a task", "List recent tasks")
  • Follow-up messages in assistant threads route to the same agent that handled the original task
  • Assistant-thread messages that only @mention another user are ignored unless they also mention the swarm bot, preventing accidental task creation from co-mentions like @Devin are you here?
  • Files shared in assistant threads — with or without a caption — reach the task as attachments (see Attachment Handling)
  • The assistant shows the working status while the agent is working (with the legacy renderer, a typing status that handles permission errors in non-assistant threads)

Progress Updates

The engine reflects progress by updating the thread tree. Agents can choose to send a distinct message with slack-reply, but routine start, progress, completion, and failure receipts are not posted automatically.

Reading Messages

Agents can read Slack threads using slack-read:

  • By task ID (reads the thread associated with a task)
  • By channel ID (leads only, for channel history)

slack-read and the worker thread-context helpers extract all message layers together: top-level text, legacy attachments, and Block Kit blocks. That means alert threads from tools like Datadog, PagerDuty, and GitHub keep both the short summary and the richer body content (fields, context, action URLs) instead of silently dropping everything outside the root text.

Posting Messages

The lead agent can post messages to channels using slack-post. By default each call creates a new top-level message. To run a multi-message conversation under a single Slack thread, the lead first calls slack-start-thread to create the parent message, then passes the returned ts as threadTs on subsequent slack-post calls — keeping the channel tidy and the conversation discoverable.

Managing Channels

Lead agents can manage the Slack channel lifecycle through three MCP tools:

  • slack-create-channel — create a public or private channel. Slack naming rules are applied and the normalized name is returned.
  • slack-invite-to-channel — invite up to 100 workspace users. Users who are already members are treated as a successful no-op.
  • slack-archive-channel — archive a channel. Already-archived channels are a successful no-op, while Slack's general channel remains protected.

These operations require lead privileges plus channels:manage for public channels and groups:write for private channels. If Slack reports a missing scope, update slack-manifest.json, apply the manifest, and reinstall the app before retrying.

Updating or Deleting Messages

Lead-gated Slack mutation tools can also manage an existing swarm-authored message after it has been posted:

  • slack-update — replace the text or blocks of an existing message in a channel or thread
  • slack-delete — remove an existing message when follow-up automation or cleanup needs it

These mutation tools follow the same public-channel auto-join behavior as the other Slack tools, but they stay lead-only because they change already-published Slack state.

Channel Membership

If the bot is not yet a member of a public, internal channel, slack-read, slack-post, slack-reply, and slack-start-thread automatically join it (via the channels:join scope) and retry — no manual /invite needed. Private channels and external Slack Connect channels cannot be self-joined: the tools return a clear error asking you to invite the bot with /invite @<bot-name> first.

User Filtering

By default, all Slack users can interact with the bot. To restrict access:

# Only users with matching email domains
SLACK_ALLOWED_EMAIL_DOMAINS=company.com,partner.com

# Specific user IDs always allowed (useful for admins)
SLACK_ALLOWED_USER_IDS=U12345678,U87654321

If both are set, a user must match either an allowed domain or be in the user ID whitelist.

Attachment Handling

Files a user shares with the bot (images, voice memos, documents) become task attachments, in channel mentions and in assistant threads alike, and with or without accompanying text:

  1. The API server downloads each file from Slack with the bot token (requires the files:read scope).
  2. It stores the file through the active file provider (agent-fs, or the local provider) as a task attachment — the same kind of attachment a file uploaded from the dashboard creates. The worker's task prompt includes a ready-to-run command to fetch it.
  3. The task is created in draft and becomes claimable only once its attachments are stored, so an agent never starts before the file exists.

Each file also appears in the task description as a metadata line (filename, MIME type, size, Slack file ID). When a file can't be attached, the bot replies in the thread naming the file and the reason. A file that is larger than the 50 MB attachment limit or fails to download is also marked (not attached: <reason>) on its metadata line. A storage error happens after the description is written, so that file is reported only in the thread reply, and the task has no attachment for it.

Limitations:

  • With ADDITIVE_SLACK=true, follow-ups that are buffered (rather than turned into a task right away) carry only the metadata line, marked as not attached, and the bot says so in the thread.
  • With thread steering enabled, a lead follow-up that includes files creates a follow-up task instead of steering the running session, because steering only carries text.
  • When the bot reads earlier messages of a thread for context, messages that contain only a file are skipped.

File Handling

Agents can upload and download files via Slack:

  • slack-upload-file — Upload a file to a Slack channel or thread
  • slack-download-file — Download a file from Slack by file ID or URL

For task-scoped uploads, slack-upload-file preserves the visible conversation thread: channel tasks upload under the original Slack thread, and Slack DMs prefer the user-facing DM tree root instead of an internal progress-message thread when both exist.

Both tools run on the API server, so a file they download is stored where the agent's container can reach it:

  • From a task, slack-download-file stores the file as an attachment of that task. This covers an explicit taskId, or the task the agent is working on. The result includes a ready-to-run fetchCommand that downloads the bytes into the container. slack-read does the same for every file in the messages it returns (turn it off with includeFiles: false). A file the task already holds is reused, not stored twice.
  • Without a task (for example, from a script), the file is saved on the API server's disk: slack-download-file uses savePath (default /workspace/shared/downloads/{agentId}/slack/), and slack-read uses /app/shared/downloads/slack/. The result says the path is on the API server. Worker containers can only read it if your deployment mounts the shared volume at that path on the API.

On this page