> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect Slack

> Install your agent in a Slack workspace as a private app with a bot token or as a distributable public app, then choose when it answers

After this guide, people in a Slack workspace can message your agent directly or mention it in a channel, and it answers in a thread. Lua fills in the Slack app for you: Slack opens its create-app page with the app named after your agent and its permissions and events already set. A private connection uses the app's bot token and serves one workspace; a public connection installs the app through Lua and can be installed by other workspaces. Both paths exist in the admin dashboard and in `lua channels`.

*Verified against lua-cli 3.41.0. The CLI steps on this page (creating the app from Lua's manifest, the signing secret, and the update and re-check actions) ship in lua-cli 3.42.0. lua-cli 3.41.0 can no longer connect Slack, because the signing secret is now required: until 3.42.0 is out, connect Slack from the admin dashboard.*

**Before you begin**

* A Slack workspace where you can create and install apps.
* An agent with a promoted production version; see [Release an agent](/ship/releasing).

<Steps>
  <Step title="Create the app from Lua's manifest">
    <Tabs>
      <Tab title="Dashboard">
        In the [admin dashboard](/get-started/for-operators) open **Agents**, select the agent, select **+** in the **Channels** section of its **Overview** tab, and choose **Slack**. Choose **Private — just my workspace** or **Public — a distributable app**, then **Create a new app (recommended)**, and select **Create the app in Slack**.
      </Tab>

      <Tab title="CLI">
        Run `lua channels`, choose **Link new channel**, then **Slack (Private)** or **Slack (Public)**, then **Create a new app from Lua's manifest**. The CLI prints the create-app link and offers to open it.

        ```bash theme={null}
        lua channels
        ```
      </Tab>
    </Tabs>

    Slack opens **Create an app** with the manifest filled in. Pick the workspace, select **Next**, then **Create and Install**. To use an app you already have, choose **I already have a Slack app** instead; Lua gives you its manifest to apply after you connect.
  </Step>

  <Step title="Provider console: copy the values Lua needs">
    All of them are in the new app's settings in Slack.

    * **Signing Secret**, always: **Basic Information → App Credentials → Signing Secret** (select **Show**). It is not the Client Secret just above it.
    * Private: install the app first with **Install App → Install to Workspace → Allow**, then copy the **Bot User OAuth Token**, which starts with `xoxb-`.
    * Public: the **App ID**, **Client ID** and **Client Secret** from **Basic Information → App Credentials**.
  </Step>

  <Step title="Connect it">
    Paste the values into the dashboard form or the CLI prompts and select **Connect**. The signing secret is required: Lua checks every event Slack sends against it (see [Signing secret](#signing-secret)).
  </Step>

  <Step title="Public only: install through Lua">
    In the dashboard select **Install in Slack**: Slack's install screen opens in a small window, and the dashboard confirms once you allow it. The CLI prints the install link instead, which works for 30 minutes; allowing it lands on Lua's result page with **Open in Slack**. To let other workspaces install the app, turn on **Manage Distribution** in Slack and share its install button, or the link `https://auth.heylua.ai/slack/install/<appId>`.
  </Step>

  <Step title="Verify">
    Send the app a direct message with `hello`; it answers in a thread under your message. That first message also confirms the signing secret: the connection's card stops showing **Verifying**. Invite the app to a channel and mention it; it answers in a thread under that message. `lua channels list` shows a `SLACK` entry.

    ```bash theme={null}
    lua channels list
    ```
  </Step>
</Steps>

## Signing secret

Slack signs every event it sends with the app's signing secret. The first event that matches the saved secret confirms it; from then on Lua rejects any event that doesn't match, including one with no signature at all, so nobody can make the agent act on a forged message. Until that first match, an event that doesn't match is still answered, and the card says **Signing secret doesn't match**: paste it again from **Basic Information**. After it was confirmed, an event that doesn't match is rejected and the connection shows **Signature check failed**. If you regenerated the secret in Slack, paste the new one. If you didn't, it was most likely a forged request, and the message clears with the next real event.

Saving a different secret for a connection that is already verified never turns the check off. Until Slack sends an event signed with the new secret, events signed with either the previous or the new one are accepted, and the status shows **Verified; new secret not confirmed yet**. The first real event signed with the new secret makes it the only one. So a mistyped secret doesn't stop the bot, and forged events stay rejected. While **Signature check failed** shows, though, Slack is already signing with a secret Lua doesn't have: only the right one brings the bot back.

Connections made before Lua asked for the secret keep working as before and show **Update available**; see [Update an existing app](#update-an-existing-app).

## Update an existing app

Lua can't change an app in your workspace; you apply each new version of the manifest. The Slack connection under **Channels** shows **Update available** until Slack has shown Lua the current manifest and the signing secret is saved, and it says which of the two is missing. Select **Update**; the dialog walks you through it:

1. Open **App Manifest** in Slack, switch to **JSON** and keep a copy of what's there. Pasting it back and reinstalling undoes the update.
2. Apply Lua's manifest on the same **JSON** tab and **Save Changes**. It keeps your app's name and every permission the app already has. If your app has features of its own, such as slash commands, shortcuts or agent mode switched on in Slack, the dialog first asks for your current manifest and gives you a merged one that keeps them, with a list of what it changes in Slack.
3. Save the signing secret.
4. Reinstall: a public app with **Install in Slack**; a private app with the banner Slack shows, or **Install App → Reinstall to Workspace**. The bot keeps its token and keeps answering.
5. Open the bot's DM in Slack. Slack sends Lua a signed event that confirms both the signing secret and the new manifest, and the dialog says **Done**.

After that, don't regenerate the signing secret in Slack. If you do, paste the new one in Lua straight away: once the secret is confirmed, Lua rejects every event that doesn't match it.

In the CLI, choose **List channels**, pick the Slack connection, and use **Show the update steps and manifest**, **Add or replace the signing secret** and **Re-check permissions with Slack**. The CLI's manifest keeps the permissions your app holds; for an app with slash commands, shortcuts or agent mode switched on in Slack, update from the dashboard so the merge keeps them. What each permission is for is on [Slack permissions](/channels/slack-permissions).

## When the app is uninstalled

If someone removes the app from the workspace or revokes its token, Slack tells Lua, Lua checks with Slack that the token no longer works, and the card shows **Uninstalled in Slack**. Replies that fail because the token is dead trigger the same check. The agent's settings and conversations are kept. Select **Reinstall**: a public app reinstalls through Lua; a private app needs a new bot token from **Install App → Reinstall to Workspace**, pasted with the signing secret, and the same connection comes back.

## Settings

Open **Agents**, select the agent, and in the **Channels** section of its **Overview** tab select the Slack connection, or select it on the **Channels** page. The panel shows the connection's status first, then every setting with its default. A choice saves when you pick it and can be undone from the confirmation under it; a text field saves when you leave it. **Change history** lists who changed which setting, and when.

| Status | What it shows | Button |
| - | - | - |
| Connection | **Active**, **Uninstalled in Slack**, or **Setup not finished** | **Reinstall**, **Finish setup** |
| Bot | The bot's name and the workspace | |
| Signing secret | **Verified**, **Not saved**, **Waiting for Slack's first event**, **Doesn't match**, **Check failed**, or **Verified; new secret not confirmed yet**; see [Signing secret](#signing-secret) | **Update**, **Fix** |
| App version | **Up to date**, or **Update available** with the permissions the app lacks | **Update**; see [Update an existing app](#update-an-existing-app) |
| Reactions | Whether Slack lets the app add reactions (`reactions:write`); **Not checked yet** until the bot's next reply reports its permissions | **Update** |
| Agent mode | **Off**, **On**, or **On, switched on in Slack** | **Set up**, **Change**; see [Agent mode](#agent-mode) |
| Last event from Slack | When Slack last delivered a message for this connection, answered or not. Recorded from 30 September 2026 | |

The buttons open the same steps as the connection card's.

| Setting | Key | What it does | Default |
| - | - | - | - |
| Who the agent answers | `slack.responseMode` | `dms_and_mentions`: every direct message, and a mention in any channel the bot is in. `mentions_only`: direct messages get no answer. `auto_respond`: also every message a person sends in the channels you pick. A mention, including one from another Slack app, and a click on one of the bot's buttons are always answered | `dms_and_mentions` |
| Channels where it answers every message | `slack.autoRespondChannels` | Channel ids, picked from the channels the bot is in. Shown and used only with `auto_respond` | None |
| People from other companies | `slack.externalUsers` | People from another workspace, in a Slack Connect channel or a direct message. `answer`: the same as your own people. `ignore`: their mentions, button clicks and direct messages get no reply. `notice`: they get one line, "I only answer people from *your workspace*." Their messages in a thread are kept as context either way. People from another workspace of your own Enterprise Grid organisation count as yours | `answer` |
| Show that the agent is working | `slack.reactions.enabled` | The reaction and the "is working…" status described [below](#while-the-agent-works); off, neither | On |
| Working reaction | `slack.reactions.working` | An emoji, or a `:shortcode:` for one of your workspace's custom emoji. Needs `reactions:write`; greyed out while the app lacks it | 👀 |
| Done reaction | `slack.reactions.done` | Replaces the working reaction once the answer is posted. Needs `reactions:write`. Set to `""` through the API to only remove the working reaction | ✅ |
| Welcome people in DMs | `slack.welcomeDm` | One short welcome the first time someone opens the bot and their direct message with it is empty; see [Home tab and greetings](#home-tab-and-greetings) | On |
| Say hello in new channels | `slack.channelHello` | One short hello when a person adds the bot to a channel; see [Home tab and greetings](#home-tab-and-greetings) | On |
| Intro | `slack.intro` | One or two sentences on what the bot does, up to 300 characters, on the Home page, in the welcome and in the hello | The agent-mode description, else "An AI assistant for this workspace." |
| Example questions | `slack.examplePrompts` | Up to 4 questions of up to 75 characters, on the Home page and in the welcome | The agent-mode prompts, else none |
| Support contact | `slack.supportContact` | A line at the bottom of the Home page, up to 150 characters, e.g. "Questions? Ask #it-help" | None |

To change settings from code, send only the keys you change. `null` resets a setting to its default. The response is the connection's full status and settings, each with its `value` and its `source`. `<channel>` is the connection's `id` or its identifier (`T…-A…`).

```bash theme={null}
curl -X PATCH "https://api.heylua.ai/admin/agents/<agentId>/channels/<channel>/settings" \
  -H "Authorization: Bearer <api key>" -H "Content-Type: application/json" \
  -d '{"changes":{"slack.responseMode":"mentions_only","slack.reactions.enabled":false}}'
```

An invalid value answers `400` with `errors`, keyed by setting, and changes nothing. `GET` on the same path reads the settings, and `GET …/settings/history` lists past changes, newest first. The older `PUT …/channels/<identifier>` with `{"slack":{…}}` still takes `responseMode`, `autoRespondChannels`, `reactions` and `externalUsers`, checks them the same way and records them in the history. Changing settings needs the Builder role on the agent; Operators can read them.

### While the agent works

As soon as the agent takes a message, it puts 👀 on it, and Slack shows **"*Your app* is working…"** (with your app's name) under it (inside the thread once the thread has replies). The status goes away when the answer arrives; 👀 then becomes ✅. If the turn fails, both go away and no ✅ is added. A button click shows the status only.

The status needs only `chat:write`, which every install has. The reaction needs `reactions:write`, an optional permission: an app installed before it was in the manifest shows the status alone until it is [updated](#update-an-existing-app), and so does an app whose admin left it unticked.

Turn both off with **Show that the agent is working**, and pick other emoji under [Settings](#settings).

## Agent mode

Slack's agent mode shows the bot as an agent: an **AGENT** badge next to its name, up to four prompt buttons at the top of its direct message, and a **Stop** button under "*Your app* is working…" while it answers. It is off by default. Leave it off for workspaces with guests or Slack Connect partners.

**Turn it on**

* **A new app:** in the **Create your Slack app** step, tick **Agent mode**. Check the description (filled in from the agent, up to 300 characters, shown in Slack's **Agents & tools** list) and add up to four prompts, each a button label and the message a click sends. The create link carries them.
* **An app you already have:** tick **Agent mode** on the step where you paste its values.
* **A connected app:** open the connection's [settings](#settings) and select **Set up** next to **Agent mode**. Save it there, then apply the updated manifest in Slack and reinstall, as in [Update an existing app](#update-an-existing-app). The same step changes the prompts or turns agent mode off.
* **CLI** (from the lua-cli release after 3.42.0-beta.1): `lua channels` asks about agent mode when you connect Slack; on a connection, **Turn agent mode on or off, or change its prompts** saves it and prints the manifest to apply.

If agent mode was switched on by hand in Slack's app settings, Lua's manifest keeps it, but Lua can't read the prompts set there. Add them in Lua first, or applying the manifest removes them.

**Stop**

Anyone in the thread can select **Stop**. The agent stops at once and posts nothing more. Where the app may add reactions, 👀 becomes ⏹️. The conversation keeps the message with the note `[Reply stopped by <name> before it was sent]`, so a later question knows what happened. What a tool already did, such as an email sent or a message posted from code, is not undone. A Stop in the first second or two, before the agent has started, also ends the turn, but the message is not kept in the agent's memory.

While a turn runs, an agent-mode app shows Slack's agent status in place of the "is working…" status described above. It clears when the answer arrives, when the turn fails, and on Stop. An app switched to agent mode in Slack without the `agent_session_stopped` event keeps the plain "is working…" status and shows no Stop button.

## Home tab and greetings

**Home tab.** Selecting the bot's name in Slack opens its **Home** tab. The page shows:

* the bot's name and one line about it;
* how to reach it: a message in the **Messages** tab (left out when the bot answers mentions only), a mention in a channel it's in (it answers in the thread), or `/invite` to add it to a channel;
* example questions, under **Try asking**;
* a support line, when you wrote one;
* the note "Answers come from AI and can be wrong."

Lua writes the page for each person when they open the bot, and again only when its content changed. It has no buttons.

**Welcome in a direct message.** The first time someone opens the bot (its Home tab or its direct message) while their direct message with it is still empty, the bot posts one short welcome there: what it does, and what to ask. It is skipped for an agent-mode app with suggested prompts (Slack shows those at the top of the direct message), when the bot answers mentions only, and for people from another company the bot doesn't answer.

**Hello in a channel.** When a person adds the bot to a channel, it posts one short hello saying how to ask it. It stays quiet in group direct messages and in Slack Connect channels whose outside people it doesn't answer.

**What the bot says about itself.** In the Slack connection's [settings](#settings) you can write an **Intro** (up to 300 characters), up to four **Example questions** and a **Support contact** line. The Home page, the welcome and the hello use them. Left empty, the bot uses the agent-mode description and prompts if it has them, otherwise "An AI assistant for this workspace." Two switches, both on by default, turn the welcome and the hello off: **Welcome people in DMs** and **Say hello in new channels**.

The greetings are not added to the agent's memory of a direct message. An earlier hello in a channel is part of the channel history the agent reads when someone mentions it there.

All of this needs the current manifest. If an app's manifest still has the Home tab off, Slack refuses the page the first time someone opens the bot's direct message, and the connection shows **Update available** until the updated manifest is applied. See [Update an existing app](#update-an-existing-app). The Home tab alone needs no reinstall.

## Conversations

A direct-message thread is a private conversation between one person and the agent. A channel thread and a group DM (a direct message between several people and the app) are shared conversations: the agent keeps one transcript for everyone in them and knows who said what. Channels here are public and private channels, including Slack Connect channels shared with other workspaces.

| | Direct message | Channel | Group DM |
| - | - | - | - |
| One conversation per | Thread, private to the person | Thread, shared | Top level of the group DM, and each thread in it; shared |
| The agent answers | Every message, unless the mode is `mentions_only` | A mention, a click on one of its buttons, or any message in a channel listed under auto-respond | A mention, or a click on one of its buttons |
| Where it replies | In a thread under the message | In the thread; a mention in a top-level message opens a thread under it | Where the mention was: the top level of the group DM, or the thread |
| Read at the first mention | Not applicable | The thread so far, up to 100 messages, including the agent's own earlier replies if any and the names of files shared in it; for a top-level mention, the channel's last 20 top-level messages | The group DM's last 100 messages, or the thread so far |
| Messages that don't mention the agent | Not applicable | Kept as context without a reply, outside auto-respond channels | Kept as context without a reply |
| Personal memory | On | Off | Off |

* **Later messages are context.** After the first mention, every message in that thread or group DM joins the transcript without a reply, so the next mention knows the whole discussion. In a channel the agent follows the thread, not later top-level messages.
* **Edits and deletes follow Slack.** When someone edits a message, the stored copy is replaced by `(edited) <new text>`; the old wording is gone. When someone deletes a message, including the first message of a thread that has replies, it stays in the transcript as `[message deleted]`, with its sender and time. This applies in channel threads, group DMs and direct messages, to the agent's memory, the admin dashboard, and your org memory. An edit never gets a reply, even when it adds a mention. What is not changed: the agent's own earlier replies (edit or delete them in Slack), messages sent before 29 September 2026 (a delete or edit of those is not applied), a long conversation's summary notes, files already copied when the message arrived, and facts the agent saved to a person's memory (remove them from memory settings).
* **Speakers are named.** Each message reaches the agent with the sender's name, and people mentioned in it appear by name. Posts from other Slack apps, such as alerts or reports, are kept as context under the app's name, and an app that mentions the agent gets an answer. An auto-respond channel answers people only, so two agents there never answer each other. A message an app posts for someone with their own Slack sign-in shows in Slack as theirs, and the agent treats it as theirs: it answers it, remembers it under their name, and follows their edits. Slackbot's own posts, such as reminders, are not kept.
* **Who each person is.** Lua knows a Slack person by their own workspace and Slack member id (their organisation, on Enterprise Grid). The first time the bot sees them, the email Slack shows for them links them to the Lua account with that email; after that the member id decides, so a changed or hidden email never turns them into someone new. People from other companies are recognised the same way. Only the person talking to the bot counts as signing in: others writing in the thread, people mentioned, earlier authors and people you message from code are recorded without it. Each other app is one Lua user, whichever way Slack names it. If Slack can't say who someone is, their message in a thread isn't kept, and a person talking to the bot is asked to send it again.
* **Slack Connect.** People from other workspaces in a shared channel are answered like anyone else, unless **People from other companies** says otherwise (see [Settings](#settings)), and the files they attach reach the agent too. A file the bot can't open reaches the agent as a note naming it, even when the message also has text, so the agent can tell the person.
* **Personal memory stays personal.** Nothing a person told the agent in a direct message is brought into a channel or group DM. Each channel thread is its own conversation: nothing carries over from one thread to another, or from a thread into direct messages.
* **Direct messages.** To continue a conversation, reply in the thread under the agent's answer; a new top-level message starts a fresh conversation. Each thread has its own transcript, though an agent with personal memory can still recall what it learned about the person earlier. `link-me-to:<agentId>` and `remove-my-link` work in a direct message; see [Test an agent on Lua's channels](/channels/quick-testing).
* **Your code sees the room.** In a tool, a condition, or a processor, [`Lua.request.conversation`](/reference/sdk/lua#requestconversation) lists the people in a channel thread or group DM and marks the current speaker. It is `undefined` in a direct message. Its `externalId` is the thread's key, `T…:A…:C…:threadTs` (workspace, app, channel, thread), or `T…:A…:G…` for a group DM; pass it to `Channels.send` as `to: { conversationId }` to post there later through the same bot ([Post from code](#post-from-code)).
* **One reply per message.** Slack sometimes delivers the same event twice, and a mention in a channel arrives both as a mention and as a message; the agent answers each message once. When a turn fails, the agent posts a short notice instead of staying silent.
* **The admin dashboard shows the room.** **Activity → Group chats** lists each Slack thread and group DM, titled `#channel · opening words` or `Group DM · opening words`, with its transcript and the people in it. The tab is read-only. Direct-message conversations appear under **Conversations**.
* **Rooms close when the agent leaves.** Disconnecting Slack in the dashboard archives the connection's threads and group DMs (**Disconnected** in Group chats); removing the app from a channel archives that channel's threads (**Bot removed**), and uninstalling the app in Slack archives them all (**Uninstalled**). Their transcripts are kept, and a mention in the same thread after the app is back reopens the room with its memory. Someone who leaves a channel leaves its threads, and a renamed channel renames them. The channel events behind this reach apps created from, or updated to, the current manifest.

## Channel behavior

| Capability | Supported |
| - | - |
| Inbound text and files | Yes, in direct messages, group DMs, and channels. In channels and group DMs only mentions and clicks on the agent's buttons are answered, unless the channel is listed under auto-respond; see [Conversations](#conversations) |
| Reply placement | In a thread under the message in direct messages and channels, or in the thread the message was in; at the top level of a group DM for a mention made there |
| Reply formatting | Standard Markdown, rendered by Slack: headings, bold, italics, strikethrough, links, lists, tables, quotes and code blocks. A reply with no `**` is read as Slack's own syntax, so `*bold*` stays bold and `~strike~` strikes |
| Long replies | Never cut. A reply longer than one Slack message (12,000 characters, or more than 20 headings, tables and dividers) arrives as consecutive messages in the same thread, with buttons and cards on the last |
| Response components | `list-item` and `horizontal-list-item` as cards, `actions` as buttons, `links` with previews, `images` (up to 10), `documents` as files or links (see [Files](#files)), `payment`, `reaction`; the rest are not rendered, see the [rendering matrix](/channels/formatting/overview) |
| Proactive sends | `Channels.send` with `channel: 'slack'`, through the agent's own bot: a channel the bot is in, a thread, or a DM to a workspace member; see [Post from code](#post-from-code). `user.send()` never picks Slack: it delivers to the end user's most recent conversation on WhatsApp, Messenger, Instagram, Teams, MessageBird, or SMS |

## Files

When the agent's answer carries a [`documents`](/channels/formatting/documents) block, the files follow the answer as one more message in the same thread or direct message.

* **With `files:write`** (in the manifest since 29 September 2026; see [Update an existing app](#update-an-existing-app)), each file stored on Lua's CDN, up to 25 MB and 10 per message, is uploaded as a Slack file under a `📎` line naming the files. People preview and download it in Slack, under Slack's access rules.
* **Without it**, and for a file hosted elsewhere, larger than 25 MB, or one Slack refuses (a workspace that blocks uploads or that file type), the message lists each file as a link: `📎 Loan Agreement (loan_agreement.pdf)`. A CDN link opens for anyone who has it.
* After you re-apply the manifest, the first answer can still send links; the agent learns the new permission from that answer's post and uploads from the next one.
* The agent remembers the block it wrote: the file's name and link. Each uploaded file message has its own delivery record, keyed by Slack's file id.

## Post from code

`Channels.send({ channel: 'slack', to, text })` posts through the agent's own Slack bot, the app you connected above, never a Lua bot. It works from a tool, a job, a webhook, or a workflow code step. Address exactly one of `to.conversationId`, `to.userId`, or `to.email`; a `phoneNumber` is refused.

| `to` | Where the message goes |
| - | - |
| `conversationId` set to `Lua.request.conversation.externalId` | The channel thread or group DM that turn ran in. The key names the bot |
| `conversationId: 'C…'` | A new top-level message in that channel; a private channel's id may start with `G` |
| `conversationId: 'C…:threadTs'` | A reply in that thread; `threadTs` is the `ts` of the thread's first message |
| `conversationId: 'U…'` | That member's DM with the bot; Slack opens the DM |
| `conversationId: 'D…'` or `'D…:threadTs'` | A DM channel, at the top level or in one of its threads |
| `conversationId: 'U…,U…'` | A group DM with 2 to 8 members, once the app has been re-authorised with the `mpim:write` scope from the updated manifest |
| `userId` | A DM to that Lua user: the Slack member they are in the bot's workspace (or its Enterprise Grid organisation), otherwise found by their email address in Lua |
| `email` | A DM to the workspace member with that email address |

**Which bot posts.** A `Lua.request.conversation.externalId` names its bot. Otherwise the send uses the agent's only active Slack connection; an agent with more than one must pass `options.channelIdentifier`, the connection identifier `T…-A…`, or the send answers `400` with a list of its connections.

**The bot must be in the channel.** Someone invites it with `/invite @your-bot`. Posting in a public channel the bot was never invited to needs the `chat:write.public` permission: apps set up from the dashboard's manifest have it, and older apps get it by applying the updated manifest and reinstalling. A private channel always needs the invite.

**What arrives.** The text renders exactly like the agent's replies: Slack Markdown, `:::` components (a `::: documents` block included, see [Files](#files)), and a long text split into consecutive messages. A text that is only a documents block posts a `📎` line naming the files first, and the files under it. One send makes one delivery record, and `messageId` is the first message's `ts`. If a later part of a long text fails, the send still returns `delivered: true`, with a `warning`.

**What the agent remembers.**

* A post into a thread or group DM where someone has already mentioned the bot is recorded in that conversation, so the next mention knows what the agent posted. The result has `persisted: true`.
* Where nobody has mentioned the bot yet, the result has `persisted: false` and a `warning`. When someone later mentions the bot in that thread, the history read at the first mention brings in the bot's earlier posts.
* A DM is recorded under the person's Lua user, in the conversation of the post's own thread: a reply in that thread continues with the context, and a new top-level message starts a fresh conversation, as with any direct message.
* Posts your code makes by calling Slack's API directly are not recorded. Use `Channels.send` when the agent should remember them.

```ts src/skills/tools/EscalateToLeadsTool.ts theme={null}
import { LuaTool, Channels } from 'lua-cli';
import { z } from 'zod';

// The #support-leads channel; the bot must be a member.
const LEADS_CHANNEL = 'C0123ABCDEF';

export default class EscalateToLeadsTool implements LuaTool {
  name = 'escalate_to_leads';
  description = 'Tell the support leads in Slack about a problem that needs them';
  inputSchema = z.object({ summary: z.string() });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const sent = await Channels.send({
      channel: 'slack',
      to: { conversationId: LEADS_CHANNEL },
      text: `Escalation: ${input.summary}`,
    });
    return { posted: sent.delivered, messageId: sent.messageId };
  }
}
```

A job that posts into a thread stored during an earlier turn is in [Send proactive messages](/build/send-proactive-messages#post-in-slack).

**When Slack refuses.**

| Status | When | Fix |
| - | - | - |
| `404` | The bot can't see that channel, thread, or member, nobody in the workspace has that email, or the Lua user is neither known in this workspace nor has an email address. The message carries a hint and, when Slack answered, Slack's code, for example `(Slack: channel_not_found)` | Check the id or the address |
| `422` `VENDOR_REJECTED` | The bot is not in the channel (`not_in_channel`), the channel is archived, the person turned off DMs from apps (`messages_tab_disabled`), the app lacks a scope, or the app was uninstalled or its token revoked | Invite the bot; for a missing scope, re-apply the manifest and reinstall the app; for an uninstalled app, reconnect Slack |
| `503` `VENDOR_UNAVAILABLE` | Slack is rate-limiting the bot (about one post per second per channel) or is down | The call does not wait out a rate limit: retry after `retryAfterSeconds`. After a timeout or an outage the post may still have landed once, although the delivery record says `failed` (Lua never sends it a second time itself), so look in the channel before retrying |

On the [REST send](/reference/rest/channels), an `X-Idempotency-Key` makes a retry safe when a call got no answer: a replayed key returns the first send's outcome instead of posting again.

**Elsewhere.** `user.send()` never picks Slack. A workflow run started with `replyTo: { channel: 'slack', threadId }` posts its result through the same bot ([Workflows REST](/reference/rest/workflows)). With the `outboundChannels` [feature](/concepts/knowledge-and-features#features) on, the model gets `sendSlackMessage`.

## Limits

* Permissions: twelve required, eight optional, and `chat:write.public`, listed with what each is for on [Slack permissions](/channels/slack-permissions).
* One private connection per workspace and app; one public connection per App ID per agent.
* `images` renders up to 10 images.
* `documents` uploads up to 10 files per answer, each up to 25 MB and stored on Lua's CDN; the rest arrive as links.
* `Channels.send` posts about one message per second per channel, Slack's own limit for the bot.
* Link commands (`link-me-to:`, `remove-my-link`) and the links they create apply to direct messages only, not to channels or group DMs.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Slack says the Request URL didn't respond">
    Right after the app is created, Slack's first check of the events address can fail. Open the app's **Event Subscriptions** page and select **Retry** next to **Request URL**; it shows **Verified**.
  </Accordion>

  <Accordion title="Missing required scope: users:read">
    The bot token was issued before the manifest was applied. Apply the manifest, reinstall the app to the workspace, and link again with the new `xoxb-` token.
  </Accordion>

  <Accordion title="A public channel stays Pending setup">
    The app's credentials are saved but it was never installed through Lua. Select **Finish setup** on the card, or get a new link in the CLI (**List channels** → the connection → **Get an install link**).
  </Accordion>

  <Accordion title="This install link has expired">
    Install links work for 30 minutes and only for the agent and app they were made for. Start again from the dashboard or `lua channels`.
  </Accordion>

  <Accordion title="Signing secret doesn't match">
    The saved secret isn't the one Slack signs with, often because the Client Secret was pasted. Copy **Basic Information → App Credentials → Signing Secret** again and save it on the card. The bot keeps answering meanwhile.
  </Accordion>

  <Accordion title="Signature check failed">
    Lua rejected an event that didn't match the confirmed signing secret. If you regenerated the secret in Slack, paste the new one; until then Lua rejects every event from the app. If you didn't, it was most likely a forged request: Lua rejected it, and the message clears with the next real event.
  </Accordion>

  <Accordion title="This token was revoked">
    Disconnecting a Slack channel in Lua revokes its bot token, and a revoked token can't be connected again. Reinstall the app in Slack (**Install App → Reinstall to Workspace**) to get a new one.
  </Accordion>

  <Accordion title="The agent answers DMs but ignores a channel">
    That is the default `dms_and_mentions` mode. Mention the app, or set `responseMode` to `auto_respond` and add the channel's ID to `autoRespondChannels`.
  </Accordion>

  <Accordion title="The agent forgot what we discussed">
    Each direct-message thread is its own conversation, and a new top-level message starts a fresh one. To continue, reply in the thread under the agent's earlier answer. In a channel, mention the agent in the thread where the discussion happened; another thread is a separate conversation.
  </Accordion>

  <Accordion title="Channels.send answers not_in_channel">
    The agent's bot is not a member of that channel. Run `/invite @your-bot` in the channel and send again.
  </Accordion>

  <Accordion title="The agent answered someone from another workspace">
    That is expected. In a Slack Connect channel, people from other workspaces are answered like anyone else in the channel. Personal memory is off there, so nothing anyone told the agent in a direct message is brought in. To keep the agent out of a shared channel, remove the app from it.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Slack permissions" href="/channels/slack-permissions">Every permission and event the app asks for, and why.</Card>
  <Card title="Connect Microsoft Teams" href="/channels/teams">The other team-chat channel, admin dashboard only.</Card>
  <Card title="Response formatting" href="/channels/formatting/overview">What each component becomes in Slack.</Card>
  <Card title="lua channels" href="/reference/cli/channels">Every menu item and what it prints.</Card>
  <Card title="About channels" href="/concepts/channels">Channels and integrations, and why Slack is on both lists.</Card>
</Columns>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.