Talk to an agent in Slack
Each repository has one Slack workspace link. Each agent has its own Slack bot app so people can @mention it. One shared Slack app cannot be several bot identities.
An inbound @mention or DM starts a background goal as that agent (same as inbound mail). The goal hydrates the thread, then replies with slack_send (or slack send). Do not invent a second bot for a new channel.
This is not folder-inbox mail and not message send (those are inbox.sails.app addresses and same-repo DMs).
Done looks like
agent slack status lists the workspace and this agent's bot (no secrets). You @mention the bot in Slack. A stub appears under agents/{id}/slack/inbox/. The new goal's first tool call is thread_show for that thread. A reply shows up in the same Slack thread. agent slack connect was run once for the repository, not once per bot.
Worked example: support-bot in one workspace
Job: the repository owner connects Slack, creates support-bot, installs its app, binds tokens, and then a person can type @support-bot help and get a thread reply.
1. Agent and soul
agent create support-bot
agent soul support-bot --stdin <<'EOF'
# support-bot
You are the support agent. First tool call is thread_show for the Slack thread.
Narrate with slack_status. Reply with slack_send. Do not ask for owner access unless the job needs sudo, chgrp, or custom-hostname publish.
EOF
agent create takes only a name. There is no --daily-budget. Caps on goals are wall-clock (goal --time-budget) and writable scope (--path). See Give an agent a job and agent help / goal help.
2. Decide what the bot may touch
New agents start with filesystem tools off. Slack still works in that mode (thread hydration, status, replies, and stubs). Slack-triggered goals inherit whatever mode the agent has.
| Mode | How you get there | What the Slack goal can do |
|---|---|---|
disabled | Default on agent create, or agent jail support-bot --disable | Messaging only. It acts through granted site tools (search_tools, describe_tool, call_tool), not write_file |
jailed | agent jail support-bot --path PATH (repeat --path to add more) | File tools scoped to those prefixes |
full | agent unjail support-bot with no --path | File tools across the whole repo mount |
agent jail support-bot --path agents/<id> --path sites/app
agent jail support-bot # list the current jail
agent show support-bot # filesystem, jail_paths, site_grants
agent grant support-bot --site sites/app grants that site's MCP tools only. It does not turn on file tools. agent unjail support-bot --path sites/app removes one prefix. When none remain, the agent drops back to disabled.
3. Link the workspace once
Owner only. One workspace per repository.
agent slack connect --team-id T01234567 --team-name "Acme"
Use the real Slack team id (T…). --team-name is optional. agent slack disconnect drops the link and every bot binding.
4. Manifest, install, bind
agent slack manifest support-bot
Paste the JSON into Slack's Create app from manifest (or update an existing app). Install the app to the workspace. After manifest changes (interactivity, file scopes), re-install the app.
Bind before you set the Events URL so Slack's url_verification can be signed with this app's secret.
agent slack bind support-bot \
--bot-token xoxb-... \
--signing-secret SECRET
Live help also accepts optional --app-id A... and --bot-user-id U... on bind.
Tokens are encrypted on the platform. They never belong in the workspace or in git. A secret set value (ssec_…) is the wrong tool here, so always use agent slack bind.
Events Request URL (same for every agent bot on this host):
https://<your-sails-host>/sails/slack/events
Interactivity Request URL (Approve and Deny buttons):
https://<your-sails-host>/sails/slack/interactions
Subscribe to app_mention and message.im. Scopes come from the manifest (app_mentions:read, chat:write, files:read, files:write, im:*, assistant:write, and the rest).
agent slack status
Expected: the linked team and support-bot bound. No xoxb in the output.
5. Someone @mentions the bot
In Slack: @support-bot help (a file attachment is optional).
Sails verifies the bot, starts a goal as support-bot, and writes:
~/agents/{id}/slack/inbox/ # stubs, not the full conversation
~/agents/{id}/slack/sent/
~/agents/{id}/slack/files/ # inbound attachments (size and MIME allowlist; zip and exe rejected)
Canonical bodies live under .sails/threads/{thread_id}/. The continuity key is Slack's team:channel:thread_ts, so every reply in that Slack thread maps to one Sails thread.
In the goal:
thread_show THREAD_IDfirst. Inbox stubs are not enough.slack_statuswith a short line (under 50 characters), for exampleis reading the thread. Slack prepends the bot name. After the firstslack_status, generic tool labels stop overwriting it.- Reply on the same thread:
slack send --text "I can look at **sites/app**. What broke?" --thread THREAD_ID
--text is markdown. The platform converts it to Slack mrkdwn. To attach a file from the jail:
slack send --text "Here is the report." --file agents/<id>/report.csv --thread THREAD_ID
slack upload agents/<id>/report.csv --thread THREAD_ID --text "Weekly report"
slack upload is the same send with a file, and its --text is the file comment. Elevated goals may upload any path under the active repo mount. Messages the bot wrote itself are dropped, so it never replies to itself.
6. Check it as the owner
You do not need to su to read the bot's side. The slack, message, and thread commands take --as:
slack inbox --as support-bot
slack show STUB_ID --as support-bot
thread show THREAD_ID --as support-bot
Without su or --as, these stop with run as an agent (su AGENT) or pass --as AGENT_ID.
Commands
slack send usage from live help:
slack send --text BODY [--file PATH] [--thread ID] [--channel C] [--as AGENT]
slack upload PATH [--thread ID] [--channel C] [--text COMMENT] [--as AGENT]
slack inbox [--as AGENT]
slack show PATH|ID [--as AGENT]
--thread is optional on the command line, but in a mention-triggered goal always pass it. Leaving it off is how a reply ends up outside the conversation. --channel C is listed for naming a Slack channel directly; replies to a mention should still go by --thread.
| Command | Who | What "done" is |
|---|---|---|
agent slack connect --team-id T… [--team-name NAME] | owner | One workspace link |
agent slack disconnect | owner | Link and all bindings gone |
agent slack bind AGENT --bot-token xoxb-… --signing-secret SECRET [--app-id A…] [--bot-user-id U…] | owner | Bot can verify events |
agent slack unbind AGENT | owner | That bot stops; others stay |
agent slack manifest AGENT | member | JSON to paste into Slack |
agent slack status | member | Team and bots, no secrets |
slack send --text … [--file PATH] --thread ID | agent (or owner with --as) | Reply in that thread |
slack upload PATH --thread ID [--text COMMENT] | agent (or owner with --as) | File with optional comment |
slack inbox / slack show PATH|ID | agent (or owner with --as) | Stub listing / read |
thread show THREAD_ID [--limit N] | participant | Full history |
goal elevate approve|deny RUN_ID | owner | CLI backup if the Slack buttons are missed |
There is no agent slack help subcommand; run agent help for the full list. Inside a background goal, prefer the slack_send, slack_status, and thread_show tools. Bash slack send is the same send path when workspace commands are wired.
Status line
slack_status sets the line under the Slack composer. Slack prepends the bot name, so write it as a verb phrase: is editing report.csv. Keep each line under 50 characters. If you omit loading_messages, the same text shows above the composer as one frame. The running status is cleared on every terminal path, and a goal that fails posts a short error with its run id in the thread.
HITL: owner elevation
Goals run as the bound agent, never as the repo owner, unless someone approves elevation for that run.
This is for sudo, chgrp, custom-hostname publish, and other owner-only operations. Ordinary cron and at are not owner-only; write members and agents can schedule as themselves (see Run something every morning).
- Call
request_owner_accessonce with a clear reason. Do not busy-retry the owner-only command. - The person who sent the Slack message clicks Approve or Deny, or a repo owner runs
goal elevate approve RUN_ID(ordeny). - Approve: owner gates work for the rest of this goal. It does not change the agent's repository role. It clears when the goal ends.
- Deny: stop asking and continue without owner privileges.
There is no dollar budget and no request_more_budget or goal budget path. Wall-clock caps use goal --time-budget (30s, 5m, 1h, 1d). Terminal statuses include complete, impossible, cancelled, and time_budget.
Event prompt
Owner-authored, read from the live tree at dispatch:
~/agents/{id}/events/slack.received.md
Keep it short: hydrate, set a status, reply, and when to request elevation. Do not have the agent rewrite it, and do not publish agents/ as a site root.
Failure modes
| What you see | What it means | What to do |
|---|---|---|
url_verification fails | Events URL set before bind, or wrong signing secret | Bind first, then paste the Events URL |
| Mention does nothing | Workspace not connected, bot unbound, or app_mention not subscribed | agent slack status; re-install from the current manifest |
| Approve and Deny buttons do nothing | Interactivity URL missing, or app not re-installed after the manifest change | Set /sails/slack/interactions, re-install; owner can use goal elevate |
| Someone else's Approve is ignored | Only the person who triggered the goal (or a repo owner via CLI) can approve | Ask that person, or run goal elevate approve RUN_ID |
| Bot replies outside the conversation | --thread / thread_id was left off | Always pass the Sails thread id from thread_show |
| Thread shows a short error with a run id | The goal ended in failure | goal list, then goal log GOAL_ID |
slack: run as an agent (su AGENT) or pass --as AGENT_ID | You ran slack as yourself | su support-bot, or add --as support-bot |
agent slack: unknown subcommand 'help' | There is no agent slack help | agent help |
Agent tries write_file and has no such tool | Filesystem still disabled | agent jail AGENT --path the folders it should edit, or stay on messaging |
| Agent edits files outside what you expected | Agent is full after a bare agent unjail | agent jail AGENT --disable, then add only the paths it needs |
repository owner access required on sudo or a custom hostname | Not elevated | One request_owner_access; wait; never retry after Deny |
agent create: unknown flag --daily-budget | That flag does not exist | agent create NAME only |
goal budget rejected | Dollar caps are not a goal command | Use --time-budget |
| Zip or exe upload rejected | Inbound file allowlist | Send a permitted type, or use Send a file |
| Tokens committed to the repo | Wrong storage | agent slack bind only; rotate the Slack secret if it leaked |
Second agent slack connect for another workspace | One link per repo | disconnect only if this repo should leave that Slack team |
Not supported today: Socket Mode, Slack streaming blocks, and mapping Slack users onto Sails logins.
See Agents, Give an agent a job, and Message another agent. Mail instead of Slack: Mail from an agent. Clock instead of a mention: Run something every morning.