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
| Surface | AGH behavior |
|---|---|
| Inbound transport | Receives Events API JSON plus Slack command and interaction form posts over public HTTPS. Socket Mode is not used. |
| Authentication | Verifies X-Slack-Signature over the exact request body and rejects request timestamps outside a five-minute window. |
| Routed events | Handles messages, app mentions, reactions, /agh prompt commands, and interactive actions after DM policy and deduplication. |
| Outbound delivery | Creates, 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 responses | Formats Slack mrkdwn, then splits terminal text at 40,000 UTF-16 code units with ordered, numbered, fence-balanced continuations. |
| Tool progress | Defaults 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 type | Routing flag | Session ownership |
|---|---|---|
| Channels | --include-group | One route per Slack channel. |
| Direct messages | --include-peer | One 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:
| Value | Required | Where it comes from |
|---|---|---|
| AGH workspace ID | yes | agh workspace list -o json; this workspace's default agent receives new routed messages. |
| Public callback URL | yes | Stable HTTPS URL that forwards to the Slack provider listener and path. |
| Local listener address | yes | provider_config.webhook.listen_addr or AGH_BRIDGE_SLACK_LISTEN_ADDR in the daemon environment. |
| Bot User OAuth token | yes | Slack app OAuth & Permissions after installation; use the bot token, not a user token. |
| App signing secret | yes | Slack app Basic Information → App Credentials. |
| Slack app administration | yes | Permission 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/supportThe 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 jsonCopy the returned ID:
BRIDGE_ID=brg_123This 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 jsonComplete 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.jsonWithout --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
- Open the Slack app dashboard.
- Select Create New App → From an app manifest.
- Choose the target Slack workspace.
- Paste or upload
slack-manifest.json. - Review the request URLs, bot events, OAuth scopes, interactivity, and slash command.
- Create the app.
The manifest subscribes to these bot events:
app_mentionmessage.channelsmessage.groupsmessage.immessage.mpimreaction_addedreaction_removed
It requests these bot scopes:
app_mentions:readassistant:writechannels:historychat:writecommandsgroups:historyim:historympim:historyreactions:readreactions: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
/aghanswer 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
- Open OAuth & Permissions and select Install to Workspace.
- Approve the requested bot scopes.
- Copy the Bot User OAuth Token. Use the bot credential Slack issued for this install.
- 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 jsonBridge reads return only the secret references and redacted metadata.
Step 7: Verify the disabled bridge
agh bridge verify "$BRIDGE_ID" --jsonExpected evidence:
provider.identitypasses when Slackauth.testrecognizes a bot token;- one
scope.<name>record passes for every scope listed above; webhook.signing_secretpasses when the binding exists;- webhook reachability is
skippedwhile 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.
allowlistreadsallow_user_idsandallow_usernames.pairingreads pre-populatedpaired_user_idsandpaired_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 jsonThe enabled verification probes the public callback. For the channel route:
- Invite the app to a test channel.
- Send a message or mention that matches the bridge's routing policy.
- Run
/agh summarize the last messagefrom that channel. - 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 jsonUse its group target for a real outbound check:
agh bridge send-test "$BRIDGE_ID" \
--message "AGH Slack connection check" \
--group-id "C0123456789" \
--jsonFor 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" \
--jsonThe 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=falseSee Tool progress for mode and grouping semantics.
Configuration reference
| Field | Default | Purpose |
|---|---|---|
webhook.public_url | none | Complete public callback used by Slack and enabled verification. |
webhook.listen_addr | AGH_BRIDGE_SLACK_LISTEN_ADDR | Local listener shared by owned Slack instances. |
webhook.path | /slack/<bridge-id> | Local callback path; each instance needs a unique path. |
batching.delay_ms | 0 | Enables inbound batching when positive. |
batching.split_delay_ms | delay_ms | Delay used after the configured split threshold. |
batching.split_threshold | 0 | Provider batching split threshold. |
dm.allow_user_ids / paired_user_ids | empty | Stable Slack identities used by allowlist or pairing. |
dm.allow_usernames / paired_usernames | empty | Normalized 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" --jsonUse --kind token for bot_token.
Change the callback URL
- Disable the bridge.
- Read the complete current
provider_config. - Update the full object with the new public URL, local address, and path.
- Generate and apply a new Slack manifest or update every Slack request URL manually.
- Enable and verify again.
agh bridge update --provider-config replaces the provider configuration object; preserve fields
you are not changing.
Troubleshooting
| Symptom | What to check |
|---|---|
| Slack rejects the Request URL | Enable the bridge, confirm the public path reaches the provider, and preserve Slack's URL challenge body. |
provider.identity says the token is a user token | Copy the Bot User OAuth Token from the installed app and replace the bot_token binding. |
A scope.* check fails | Add that bot scope, reinstall the app to the workspace, copy the new bot token if Slack rotated it, and verify again. |
| Signed events return unauthorized | Confirm the signing secret comes from the same Slack app and the proxy preserves body bytes and signature headers. |
Events work but /agh or actions fail | Confirm slash-command and interactivity Request URLs match webhook.public_url and form bodies are forwarded without transformation. |
Slack shows a /agh timeout | AGH 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 fails | Remove --include-thread from the bridge; a slash command has no valid parent message timestamp for thread_ts. |
| Channel messages are silent | Invite 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 thread | Inspect agh bridge routes and confirm an inbound message supplied the Slack thread_ts stored on that route. |
| Reactions or progress status fail while text works | Check reactions:write and assistant:write; keep text delivery enabled while repairing optional affordances. |
| A second Slack instance degrades | Give each instance a distinct webhook path while keeping the shared provider listener address consistent. |
| Verify reports public reachability warning or failure | Check 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
/aghas workspace-member prompt access;dm_policydoes 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_idorpeer_idfor its selected conversation type, not both. - A peer-only bridge reports the intended
dm_policyand 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
/aghanswer arrives through the bot API at conversation level, not throughresponse_url. send-testreturns a remote Slack message ID.- The bridge returns to
readyafter one restart.