slack() from @trigger.dev/slack turns a chat.agent into a native Slack agent. Each Slack agent session (a conversation with the agent, or a thread in a channel) becomes a durable Trigger.dev session. A message runs as a turn, the session shows the agent working, the reply streams into the thread as the model writes it, and Slack’s stop button stops the turn.
/trigger/support.ts
run() is the same function that answers a browser chat or a Linear session. Signature verification, Slack’s url_verification handshake, retry dedup, per-thread state, status updates and streaming are handled by the connector.
Setup
1
Deploy the agent
Declare the endpoint with
webhookSource() from @trigger.dev/slack and pass it to slack({ endpoint }). Deploying registers the hosted endpoint and gives you its URL.2
Create the Slack app from a manifest
At api.slack.com/apps, create an app From a manifest and paste this, with the endpoint’s URL in both request URLs:
manifest.yaml
agent_view makes the app an agent, agent_session_stopped turns on the stop button, interactivity delivers clicks on the agent’s buttons, files:read lets the agent read the images and PDFs users share with it, and reactions:write lets it mark a message it picked up mid-reply. Drop message.channels, message.groups and their history scopes if the agent should only answer in its own conversations.3
Connect the endpoint
Paste the app’s Signing Secret (Basic Information, App Credentials) as the endpoint’s signing secret. Until it is set the endpoint rejects Slack’s signed requests, so Slack shows the request URL as not verified: once the secret is in, click Verify on the app’s Event Subscriptions page (or Click here to verify on the App Manifest page). Install the app to your workspace and pass the bot token (
xoxb-...) as token. The endpoint’s setup prompt walks an AI coding agent through these settings.4
Try it
Open the agent in Slack and send it a message. The session shows it working within a second, and the reply streams in. In a channel, invite the bot (
/invite @Support Agent) and message it in a thread.What Slack shows
Set
title to name the session yourself; return null to keep Slack’s own. A title the user sets in Slack is never overwritten. Slack drops a processing status after an hour, so a longer turn re-sends it every 50 minutes. progress: "off" streams the reply without task cards, and delivery: "final" posts the whole reply once the turn ends instead of streaming it.
Retry-After and resend the same batch. If the worker running a turn dies mid-stream, the next worker closes that stream with a short “Interrupted” note before it picks the conversation back up.
Approvals and buttons
A tool with noexecute pauses the turn until the user answers it in Slack. By default slack() posts Approve and Deny buttons, and a click resolves the tool to { approved: boolean }, collapses the buttons to the decision and resumes the turn. See human-in-the-loop for the tool definition.
approval replaces those buttons for every such tool. Build buttons with button(), whose output is what the click resolves the tool to:
interactions gives a single tool its own UI, keyed by tool name, so a tool can ask the user to pick, rate or choose and continue with the answer:
pickEmoji to { picked: "wizard" }, the buttons collapse to the choice, and the turn continues.
Buttons that send the agent an action
Buttons that aren’t answering a tool, such as follow-up questions, are built withslackButton() and posted with the surface’s post(). A click reaches onAction, validated against actionSchema like a browser action, and returning chat.turn(message) answers it in the thread:
onAction also gets the click itself as channel.event, typed with SlackBlockActions: who clicked, and the message the button was on. An action doesn’t have to answer; return nothing to only do work. Here actionSchema accepts a pick action alongside ask:
button() or slackButton() reach the agent. Buttons your own code builds by hand go to a webhook() subscribed to the same endpoint, and never start a turn.
Stopping
Slack shows a stop button while the agent is working. It aborts the running turn through the run’ssignal, exactly like a browser chat’s stop: the model call and any tool that honours the signal stop, the partial reply stays in the thread, and the status goes back to active. The next message continues the conversation.
- A stop pressed while nothing is running does nothing, and a repeated or delayed stop never stops a later turn.
- A stop that reaches a session waiting on an approval cancels it: the buttons are replaced with “Stopped”, the pending tool call is recorded as stopped so the model sees that on the next turn, and a later click resumes nothing.
- Stopping cannot undo what already happened. A tool that finished before the stop has had its effect.
Channel threads
In a channel the agent answers every thread it can see. To have it answer only when it is @mentioned, passstartOn with the mentions helper and your bot’s user id. A mention in a channel starts a session and the agent answers in a thread under it, then follows the rest of that thread without being mentioned again. startOn only applies to channels: a message in the agent’s own conversation always starts a session.
seedFromThread: true loads the thread’s earlier replies into the new session first, so the agent answers with the context a reader of the thread has. It loads the history once, on the session’s first turn, paging through conversations.replies and keeping the newest 50 replies ({ limit } changes that). Slack limits new commercially distributed apps outside the Marketplace to 15 replies per call and one call a minute, so such an app may only get the thread’s first page; internal apps keep the normal limits. A rate limit keeps what was fetched rather than failing the turn.
Messages sent while the agent is working
A message sent while the agent is still answering gets 👀 as soon as it arrives, so the user knows it was seen (receivedReaction changes the emoji, null turns it off). It then waits and runs as the next turn, in the order messages arrived. With pendingMessages on the agent, it can be folded into the reply the agent is already writing instead: shouldInject decides at each step, and the message reaches the model before its next step. A message shouldInject declines still runs as its own turn.
Files
Images (PNG, JPEG, GIF, WebP) and PDFs that a user shares in the conversation reach the model with their message, including a file sent with no text. The transcript keeps each file’s Slack URL, and thestreamText given to run() downloads it with the bot token when the model reads it, once per run. An image over 5 MB is sent as its largest Slack thumbnail.
Other file types, files over 20 MB, and files the app can’t read (deleted, or shared from another workspace) reach the model as a one-line note naming the file, so it can say it can’t open them. A file that is deleted after it was shared becomes the same note on later turns, and the conversation carries on.
A custom inbound keeps file support by adding slackFileParts() to its message:
streamText passed to run(). A streamText imported from ai sends the Slack URL to the model as is, which the model can’t open.
Answering into the session from elsewhere
The first Slack message binds the session to its thread and to the user it is answering. Achat.event action that returns chat.turn(), or a browser client on the same session, answers into that Slack thread with the same status, streaming and approval buttons.
Slash commands
Slack posts a slash command as a form, which reaches subscribers as an object of its fields, and the endpoint acknowledges it with an empty200 so Slack shows the user nothing until you reply. Give slash commands their own endpoint, typed with SlackSlashCommand, and set its URL as the command’s Request URL under Slash Commands in the Slack app’s settings. Use the same Signing Secret as the agent’s endpoint. Reply through the command’s response_url:
/trigger/commands.ts
chat.event keyed on {event.channel_id}:{event.user_id} routes a user’s commands to one session, and a waiter can match on event.command.
Block Kit replies
Replies are sent as markdown. For a reply that is posted rather than streamed (delivery: "final"), return blocks from outbound to post Block Kit. toSlackBlocks() splits markdown into section blocks under Slack’s 3000-character limit, never inside a code fence:

