Skip to content
AGH RuntimeBridges

Slack Setup

Create a signed Events API integration, route Slack channels safely, and understand how the /agh prompt command is delivered.

Audience
Operators running durable agent work
Focus
Bridges guidance shaped for scanability, day-two clarity, and operator context.

This guide connects one Slack app to one route-specific AGH workspace bridge. The AGH-generated manifest is the recommended path: it derives request URLs, bot events, OAuth scopes, interactivity, and the /agh command from the same runtime contract that verifies the installed app.

How the Slack bridge behaves

SurfaceAGH behavior
Inbound transportReceives Events API JSON plus Slack command and interaction form posts over public HTTPS. Socket Mode is not used.
AuthenticationVerifies X-Slack-Signature over the exact request body and rejects request timestamps outside a five-minute window.
Routed eventsHandles messages, app mentions, reactions, /agh prompt commands, and interactive actions after DM policy and deduplication.
Outbound deliveryCreates, updates, and deletes Slack messages. Normal message events can preserve a real Slack thread_ts; the safe /agh setup below posts the agent reply at channel level.
Long responsesFormats Slack mrkdwn, then splits terminal text at 40,000 UTF-16 code units with ordered, numbered, fence-balanced continuations.
Tool progressDefaults to new + accumulate with status edits and reactions enabled. The final answer is a separate bot message at the resolved route target.

One provider subprocess can own multiple Slack bridge instances. They share a listener address and must use distinct webhook paths.

Choose one route shape per bridge

AGH requires every routing dimension selected on a bridge to exist on each inbound event. A Slack channel event has a group target, while a direct message has a peer target. Do not require both on one bridge.

The safe route shapes for a Slack app that exposes /agh are:

Conversation typeRouting flagSession ownership
Channels--include-groupOne route per Slack channel.
Direct messages--include-peerOne route per direct conversation.

Both omit --include-thread because a slash-command payload has no parent message timestamp. The current provider cannot deliver a threaded /agh answer safely.

A Slack app has one Events API request URL. To run both route shapes at the same time, use separate Slack apps or an operator-owned request dispatcher that forwards each payload to the correct AGH webhook.

Before you start

Collect or decide these values:

ValueRequiredWhere it comes from
AGH workspace IDyesagh workspace list -o json; this workspace's default agent receives new routed messages.
Public callback URLyesStable HTTPS URL that forwards to the Slack provider listener and path.
Local listener addressyesprovider_config.webhook.listen_addr or AGH_BRIDGE_SLACK_LISTEN_ADDR in the daemon environment.
Bot User OAuth tokenyesSlack app OAuth & Permissions after installation; use the bot token, not a user token.
App signing secretyesSlack app Basic Information → App Credentials.
Slack app administrationyesPermission to create and install an app in the target Slack workspace.

The provider needs no Slack app-level token. Keep Socket Mode disabled.

Before configuring Slack, prove that the locally built provider is installed and visible in the daemon catalog:

PROVIDER=slack
agh extension status "$PROVIDER" -o json
curl -sS http://127.0.0.1:2123/api/bridges/providers | \
  jq --arg provider "$PROVIDER" '.providers[] | select(.extension_name == $provider)'

Continue only when the extension is enabled and the catalog row is not disabled or unhealthy. If either command has no provider, follow Install an in-tree provider first.

Step 1: Map the public and local endpoints

Choose a stable callback before generating the manifest:

Slack → https://bridge.example.com/slack/support
      → reverse proxy or tunnel
      → http://127.0.0.1:18081/slack/support

The proxy must preserve the raw request body, content type, method, path, and Slack signature headers. Body transformation breaks signature verification.

Step 2: Create the bridge disabled

Channel route

agh bridge create \
  --scope workspace \
  --workspace-id ws_8f33a913d23c4fd1 \
  --platform slack \
  --extension slack \
  --display-name "Slack support" \
  --enabled=false \
  --include-group \
  --provider-config '{
    "webhook": {
      "public_url": "https://bridge.example.com/slack/support",
      "listen_addr": "127.0.0.1:18081",
      "path": "/slack/support"
    }
  }' \
  -o json

Copy the returned ID:

BRIDGE_ID=brg_123

This route creates one session per Slack channel without splitting ownership by sender. Replies, including /agh answers, are top-level bot messages in that channel.

Direct-message route

Create a separate peer-only bridge when direct messages must have independent sessions:

agh bridge create \
  --scope workspace \
  --workspace-id ws_8f33a913d23c4fd1 \
  --platform slack \
  --extension slack \
  --display-name "Slack direct messages" \
  --enabled=false \
  --include-peer \
  --provider-config '{
    "webhook": {
      "public_url": "https://bridge.example.com/slack/direct",
      "listen_addr": "127.0.0.1:18081",
      "path": "/slack/direct"
    }
  }' \
  -o json

Complete the remaining steps with the bridge ID and Slack app that match the selected route shape. See Message routing before changing these dimensions.

Step 3: Generate the Slack app manifest

agh bridge manifest slack \
  --instance "$BRIDGE_ID" \
  --write \
  --out ./slack-manifest.json

Without --write, the command prints the manifest to stdout. With --write and no --out, it writes to $AGH_HOME/slack-manifest.json.

The manifest contains the persisted bridge instance's public callback. If you change that URL, update the bridge first and generate a new manifest. Do not hand-patch an old copy while AGH retains a different configuration.

Step 4: Create the Slack app from the manifest

  1. Open the Slack app dashboard.
  2. Select Create New App → From an app manifest.
  3. Choose the target Slack workspace.
  4. Paste or upload slack-manifest.json.
  5. Review the request URLs, bot events, OAuth scopes, interactivity, and slash command.
  6. Create the app.

The manifest subscribes to these bot events:

  • app_mention
  • message.channels
  • message.groups
  • message.im
  • message.mpim
  • reaction_added
  • reaction_removed

It requests these bot scopes:

  • app_mentions:read
  • assistant:write
  • channels:history
  • chat:write
  • commands
  • groups:history
  • im:history
  • mpim:history
  • reactions:read
  • reactions:write

The generated manifest also enables interactivity and configures /agh against the same public callback. Keep Socket Mode disabled because the provider is an HTTP webhook receiver.

What /agh does

The generated slash command is a prompt entry point:

/agh <text to send to the agent>

AGH preserves /agh as the command name and passes all following text to the routed agent as arguments. It does not parse administrative subcommands. Names such as /agh status, /agh new, /agh reset, and /agh help have no built-in behavior; their trailing words are ordinary prompt text.

The first accepted event for a canonical route creates a session with the workspace's default agent. Later /agh invocations on the same route reuse that session. In the channel configuration above, every member invoking /agh in the same channel reaches the same route because Slack user_id is not part of the route key.

Access and response visibility

Slack request signing authenticates the app request. For channel invocations, AGH applies no additional sender or channel allowlist after signature verification. Any workspace member who can invoke the installed command can submit a prompt to that channel route. The dm_policy described below applies only to direct messages.

The provider returns HTTP 200 with a plain OK body after synchronously dispatching the inbound envelope. It does not use Slack's response_url for the agent output. The completed answer is sent later through the bot API:

  • a group-only bridge posts a top-level bot message visible in the channel;
  • a peer-only bridge posts a bot message in the direct conversation;
  • there is no native ephemeral /agh answer mode.

Current /agh runtime constraints

Slack does not allow a slash command to originate inside a message thread, so its payload contains no parent message ts. The current provider derives a routing thread value from the channel ID; when --include-thread is enabled, that value reaches chat.postMessage as thread_ts even though Slack requires a message timestamp. Keep --include-thread off any bridge that accepts /agh. Normal message-event bridges can preserve threads when the inbound event supplies a real thread_ts.

Slack requires slash-command endpoints to acknowledge within three seconds. AGH currently dispatches the envelope before returning OK, and session-busy retries can outlast that deadline. Slack can therefore show a timeout even when AGH subsequently accepts the prompt. Inspect agh bridge routes and the target session before sending the same prompt again.

See Slack's slash-command contract and chat.postMessage thread contract for the platform constraints behind this configuration.

Step 5: Install the app and copy its credentials

  1. Open OAuth & Permissions and select Install to Workspace.
  2. Approve the requested bot scopes.
  3. Copy the Bot User OAuth Token. Use the bot credential Slack issued for this install.
  4. Open Basic Information → App Credentials and copy the Signing Secret.

Reinstall the app after any scope change. Updating scopes in the dashboard does not retroactively grant them to an existing installation.

Step 6: Bind the credentials

printf '%s' "$SLACK_BOT_TOKEN" | agh bridge secret-bindings put "$BRIDGE_ID" bot_token \
  --secret-ref "vault:bridges/$BRIDGE_ID/bot_token" \
  --kind token \
  --secret-value-stdin

printf '%s' "$SLACK_SIGNING_SECRET" | agh bridge secret-bindings put "$BRIDGE_ID" signing_secret \
  --secret-ref "vault:bridges/$BRIDGE_ID/signing_secret" \
  --kind secret \
  --secret-value-stdin

agh bridge secret-bindings list "$BRIDGE_ID" -o json

Bridge reads return only the secret references and redacted metadata.

Step 7: Verify the disabled bridge

agh bridge verify "$BRIDGE_ID" --json

Expected evidence:

  • provider.identity passes when Slack auth.test recognizes a bot token;
  • one scope.<name> record passes for every scope listed above;
  • webhook.signing_secret passes when the binding exists;
  • webhook reachability is skipped while the bridge is disabled.

A user token fails with remediation to bind the Bot User OAuth token. A missing scope is named individually so the app can be updated and reinstalled without guessing.

Step 8: Control direct-message access

The group-only bridge rejects direct-message routes, so its dm_policy does not grant or restrict channel access. Before enabling a peer-only bridge, set an explicit policy. An omitted or empty value normalizes to open and admits every Slack DM sender whose request passes signature verification.

agh bridge create and agh bridge update do not currently expose --dm-policy. Use the Web editor or HTTP API procedure in Control direct-message access, then keep the bridge disabled until agh bridge get "$BRIDGE_ID" -o json reports enabled: false, the intended dm_policy, and the intended provider_config.dm lists.

  • allowlist reads allow_user_ids and allow_usernames.
  • pairing reads pre-populated paired_user_ids and paired_usernames, then falls back to the allowlist. It does not issue pairing codes or start an interactive approval flow.

Prefer stable Slack user IDs over display names. dm_policy applies only to DMs. Slack app installation, channel membership, request signing, and the bridge route govern channel events and channel /agh invocations.

Step 9: Enable and prove the full path

agh bridge enable "$BRIDGE_ID"
agh bridge verify "$BRIDGE_ID" --json
agh bridge get "$BRIDGE_ID" -o json

The enabled verification probes the public callback. For the channel route:

  1. Invite the app to a test channel.
  2. Send a message or mention that matches the bridge's routing policy.
  3. Run /agh summarize the last message from that channel.
  4. Confirm the response appears as a top-level bot message in the same channel.

Inspect the route AGH created:

agh bridge routes "$BRIDGE_ID" -o json

Use its group target for a real outbound check:

agh bridge send-test "$BRIDGE_ID" \
  --message "AGH Slack connection check" \
  --group-id "C0123456789" \
  --json

For a peer-only bridge, use the peer_id from its inbound route instead:

agh bridge send-test "$BRIDGE_ID" \
  --message "AGH Slack direct-message check" \
  --peer-id "D0123456789" \
  --json

The command must return a delivery ID and a Slack remote message ID. test-delivery is not a substitute; it resolves the target without calling Slack.

Delivery and progress behavior

Slack text is converted to mrkdwn before the provider measures the 40,000-UTF-16-code-unit limit. Long terminal answers become ordered continuations at the resolved route target. Streaming previews stay in one mutable message until terminal delivery establishes the final continuation set.

The group-only and peer-only examples keep progress and final output at conversation level. A normal message-event bridge may use --include-thread when it receives a real Slack message timestamp, but that route shape is not safe for /agh with the current provider.

Slack defaults to:

{
  "progress": {
    "tool_progress": "new",
    "grouping": "accumulate",
    "typing": true,
    "reactions": true
  }
}

Progress uses a dedicated message and never becomes the final-text delivery anchor. Disable it for a quiet conversation:

agh bridge update "$BRIDGE_ID" \
  --delivery-progress off \
  --delivery-progress-typing=false \
  --delivery-progress-reactions=false

See Tool progress for mode and grouping semantics.

Configuration reference

FieldDefaultPurpose
webhook.public_urlnoneComplete public callback used by Slack and enabled verification.
webhook.listen_addrAGH_BRIDGE_SLACK_LISTEN_ADDRLocal listener shared by owned Slack instances.
webhook.path/slack/<bridge-id>Local callback path; each instance needs a unique path.
batching.delay_ms0Enables inbound batching when positive.
batching.split_delay_msdelay_msDelay used after the configured split threshold.
batching.split_threshold0Provider batching split threshold.
dm.allow_user_ids / paired_user_idsemptyStable Slack identities used by allowlist or pairing.
dm.allow_usernames / paired_usernamesemptyNormalized name fallbacks; IDs are safer when names can change.

The Slack API destination is an operator-owned process setting. Instance configuration cannot redirect bound credentials to another host.

Common operations

Rotate the bot token or signing secret

Disable the bridge, update the Slack app first when needed, replace the binding, then enable and verify again:

agh bridge disable "$BRIDGE_ID"
# Reinstall the Slack app or rotate the signing secret in Slack.
printf '%s' "$NEW_VALUE" | agh bridge secret-bindings put "$BRIDGE_ID" <slot> \
  --secret-ref "vault:bridges/$BRIDGE_ID/<slot>" \
  --kind secret \
  --secret-value-stdin
agh bridge enable "$BRIDGE_ID"
agh bridge verify "$BRIDGE_ID" --json

Use --kind token for bot_token.

Change the callback URL

  1. Disable the bridge.
  2. Read the complete current provider_config.
  3. Update the full object with the new public URL, local address, and path.
  4. Generate and apply a new Slack manifest or update every Slack request URL manually.
  5. Enable and verify again.

agh bridge update --provider-config replaces the provider configuration object; preserve fields you are not changing.

Troubleshooting

SymptomWhat to check
Slack rejects the Request URLEnable the bridge, confirm the public path reaches the provider, and preserve Slack's URL challenge body.
provider.identity says the token is a user tokenCopy the Bot User OAuth Token from the installed app and replace the bot_token binding.
A scope.* check failsAdd that bot scope, reinstall the app to the workspace, copy the new bot token if Slack rotated it, and verify again.
Signed events return unauthorizedConfirm the signing secret comes from the same Slack app and the proxy preserves body bytes and signature headers.
Events work but /agh or actions failConfirm slash-command and interactivity Request URLs match webhook.public_url and form bodies are forwarded without transformation.
Slack shows a /agh timeoutAGH may have accepted the prompt after Slack's three-second deadline. Inspect routes and the target session before retrying.
/agh is accepted but its bot reply failsRemove --include-thread from the bridge; a slash command has no valid parent message timestamp for thread_ts.
Channel messages are silentInvite the app, subscribe to the matching message event, and confirm the app installation has the corresponding history scope.
Normal message replies appear in the wrong threadInspect agh bridge routes and confirm an inbound message supplied the Slack thread_ts stored on that route.
Reactions or progress status fail while text worksCheck reactions:write and assistant:write; keep text delivery enabled while repairing optional affordances.
A second Slack instance degradesGive each instance a distinct webhook path while keeping the shared provider listener address consistent.
Verify reports public reachability warning or failureCheck DNS, TLS, reverse-proxy path, redirects, and upstream status. The verifier does not follow redirects or use an HTTP proxy.

Security notes

  • Keep the signing secret distinct per Slack app and rotate it after exposure.
  • Restrict app installation and scopes to the workspace where the bridge operates.
  • Use DM allowlists or pairing before distributing a direct-message app broadly.
  • Treat channel-level /agh as workspace-member prompt access; dm_policy does not restrict it.
  • Do not log request bodies, tokens, or signing material while debugging.
  • Keep the listener behind a path-specific reverse proxy; do not expose unrelated local services.

Completion checklist

  • The Slack extension is installed, enabled, and healthy in the provider catalog.
  • The app was created from the current bridge manifest or an exact manual equivalent.
  • Every scope.* check passes after installation.
  • Slack request signatures validate on the public route.
  • The bridge requires either group_id or peer_id for its selected conversation type, not both.
  • A peer-only bridge reports the intended dm_policy and sender lists before enablement.
  • An inbound event creates a route in the intended workspace with the workspace's default agent.
  • /agh <text> reaches that route as prompt arguments and later reuses the same session.
  • The /agh answer arrives through the bot API at conversation level, not through response_url.
  • send-test returns a remote Slack message ID.
  • The bridge returns to ready after one restart.

On this page