---
title: "Slack integration - setup and usage guide"
description: "Wire Fredrin to Slack: create your Slack app, paste the bot token, get review-ready and agent-blocked alerts in a channel, and file tickets with @Fredrin."
url: "https://fredrin.com/docs/slack"
updated: "2026-08-15"
---

# Slack integration - setup and usage guide

Connect Slack once, invite Fredrin into the channels you want to use, and tell
it where each message should go: **`@Fredrin send to #engineering: The update
is ready`**. Mentions get replies in their original channel/thread. A later
request can send to another joined channel without changing settings.

**No alert channel is required.** Automatic ticket-status alerts are a separate,
optional setting and start off on new connections. Existing saved alert
preferences are preserved.

Fredrin generates the Slack app as a **manifest** you paste into Slack. Add the
bot token and signing secret in Fredrin, then invite the bot into your channels.

It is bring-your-own-app: you create a small Slack app in your own Slack
workspace and paste its bot token into Fredrin. Fredrin hosts no shared Slack
app, so nothing about your Slack workspace is visible to anyone else; the token
and signing secret are stored encrypted and never logged.

## What you get

- **Optional ticket alerts, on the columns you choose.** When enabling alerts,
  these three columns are suggested:
  - **Review** — a Worker finished and opened a PR:
    `🔍 Ready for review — FRED-XXXX <title>`
  - **Blocked** — a Worker stopped and needs a human:
    `⛔ Agent blocked — needs your input — FRED-XXXX <title>`
  - **Completed** — the ticket shipped, whether you dragged it there or a merge
    completed it: `🎉 Done — FRED-XXXX <title>`

  Use **Automatic status alerts** in the Slack row’s menu to choose a channel
  and columns, including custom steps (`➡️ Moved to <Column>`). Switch alerts
  off to pause them while keeping the saved channel, columns, and messaging.
- **`@Fredrin` mentions** — say what you want in your own words and the bot
  works out what to do, answering in a thread. It can file tickets, look them
  up, list what is sitting in a column (with each ticket's pull-request state),
  read one ticket in full, comment on them, move them between columns, assign
  them, start an AI Worker building one — and answer questions about the
  codebase itself by reading the real repository on your paired machine.
- **No re-tagging inside its own threads** (the manifest subscribes the events
  this needs) — once the bot has replied in a
  thread, keep talking to it normally. Plain replies are picked up, so a
  follow-up is *"and make it high priority"*, not
  *"@Fredrin and make it high priority"*, and a confirmation is just *"yes"*.
  Elsewhere in the channel nothing changes: the bot only listens inside threads
  it has posted in.
- **`/fredrin` slash command** (optional) — the same assistant, answered
  privately to you. It can also send a requested message to a joined channel.
  It has no thread, so it can file, search and comment but
  not move, assign or start a build.

## Prerequisites

- A Fredrin project (in the desktop app) you want to wire up.
- Permission to create and install apps in your Slack workspace.
- About two minutes.
- A Fredrin deployment with **`CONNECTOR_ENCRYPTION_KEY`** set. It is required to
  save any pasted-key connector; without it the Connectors panel shows a
  "connecting is unavailable" banner. (Self-hosting only — it is already set on
  `fredrin.com`. That banner also names OAuth client IDs, which Slack does not
  need.)

## 1. Create the Slack app from Fredrin's manifest

1. In the Fredrin desktop app, open your project → **Project settings** →
   **Connectors** → **Slack** → **Connect**. (Workspace owners: use
   **Settings → Workspace → Connectors** instead to serve every project in the
   workspace with one app.)
2. The form shows a ready-made **app manifest** for your Fredrin server —
   scopes, bot user, events, Request URL and the `/fredrin` command, all filled
   in. Click **Copy manifest**.
3. Click **Open Slack** (or go to
   [api.slack.com/apps](https://api.slack.com/apps)) → **Create New App** →
   **From an app manifest** → pick your Slack workspace → paste → **Create**.
4. **OAuth & Permissions** → **Install to Workspace** → authorize.
5. Copy two values — you'll paste both into Fredrin:
   - the **Bot User OAuth Token** (OAuth & Permissions; starts `xoxb-`)
   - the **Signing Secret** (Basic Information → App Credentials)

The manifest asks for exactly the scopes the integration uses and nothing else:
`chat:write` (post requested messages, alerts, and replies), `channels:read` +
`groups:read` (list channels and verify requested destinations), `channels:join` (let Fredrin add itself to the
public channel you pick), `app_mentions:read` (receive `@Fredrin`), and
`channels:history` + `groups:history` (read the thread it is already replying
in, which is what makes follow-ups and untagged replies work).

> **Running Fredrin on `localhost`?** Slack checks the Request URL while
> creating the app and cannot reach your machine, so it will refuse the
> manifest. Delete the `"event_subscriptions"` block before pasting: alerts and
> everything outbound still work; `@Fredrin` mentions need a reachable URL.

## 2. Connect in Fredrin — no alert channel required

In **Settings → Connectors**, choose the workspace or project scope, open
**Slack → Connect**, and paste the **Bot User OAuth Token** and **Signing Secret**.
The signing secret enables mentions, thread replies, and `/fredrin`; it does
not grant channel access. Click **Connect Slack**. Leave the optional alert
channel empty to use on-demand messaging only.

In Slack, invite Fredrin into each channel you want it to use with
`/invite @Fredrin`. Public and private channels both require membership for
on-demand sends. Fredrin does not join extra destinations during a send.

The optional alert-channel dropdown can reserve a destination for later; choosing
one while connecting does **not** enable automatic alerts. It always stays a
dropdown during loading or failure. Use **Retry channels** after correcting a
permission/connection problem, or **Refresh channels** after inviting the bot.
The list includes up to 1,000 channels across five pages, excluding archived ones.
If listing lacks permissions, add `channels:read` and `groups:read` under
**OAuth & Permissions → Bot Token Scopes**, reinstall the Slack app, then retry.

To rotate a bot token, choose **Update credentials** on the connected row.
Blank signing-secret and channel fields keep their stored values, and the alert
switch and column choices carry forward. Disconnecting revokes the credential;
a new connection starts with alerts off.

## 3. Turn on automatic status alerts (optional)

Open the connected Slack row’s **⋯ → Automatic status alerts**. Turn the switch
on, pick an **Alert channel**, select at least one column, and choose **Save
status alerts**. Review, Blocked, and Completed are suggested when there is no
saved column selection. Fredrin joins a public alert channel for you; invite
it to a private channel yourself.

Turn the switch off and save to stop automatic alerts. The saved channel and
column choices remain available when you turn it back on. Mentions and requested
messages keep working throughout.

Existing connections with a saved alert channel retain their previous behavior,
including custom columns or an explicitly empty set that silences alerts. A
connection with no saved channel stays off. Each project override has its own
alert preferences; inherited connections are managed in workspace settings.

## 4. Check the event Request URL (mentions only)

The manifest already set it, and Slack verified it while creating the app. If
you removed the `event_subscriptions` block (localhost), or you are wiring an
app you created by hand, set it now:

1. **Event Subscriptions** → **Enable Events** on.
2. **Request URL**: `https://www.fredrin.com/api/webhooks/slack` — it should
   show **Verified** within a moment.
3. **Subscribe to bot events** → add **`app_mention`** (required), plus
   **`message.channels`** and **`message.groups`** (what lets you reply inside
   the bot's own threads without re-tagging it — optional; without them, tag the
   bot every time).
4. **Save Changes**, and reinstall the app if Slack prompts for it.

## 5. (Optional) the `/fredrin` slash command

The manifest already creates it. Its replies are private to whoever ran it
instead of threaded in the channel; mentions cover intake, so use it only if
your team prefers that. To add it to a hand-made app: **Slash Commands** →
**Create New Command**: command `/fredrin`, Request URL
`https://www.fredrin.com/api/webhooks/slack`, description
`File and track Fredrin tickets`, usage hint `<what you need> | status | help`.

## 6. Try it

- In a channel the bot is in: `@Fredrin file a ticket to test the Slack
  integration`. The bot replies in a thread with the ticket identifier and
  column; the ticket is on your board's Backlog.
- `@Fredrin status` → per-column counts. `@Fredrin status review` → the
  tickets in that column, each with its identifier, title and PR state.
- If you enabled automatic alerts, move any ticket into **Review** (or wait for a Worker to finish one) — the
  alert appears in your alert channel. Drag it on to **Completed** and you get
  the `🎉 Done` line; so does a PR merge that auto-completes it.

## Using it day to day

- **Send to the channel you name.** From any joined channel, ask
  `@Fredrin send to #engineering: The update is ready`. Later, ask for `#design`
  instead. A Slack channel mention keeps its ID, so a renamed channel or two
  identical names cannot silently change the destination. The bot reports the
  actual destination after Slack accepts the message. It sends one message to
  one channel per request, with no fallback to the alert channel and no bulk
  broadcast. Include the content to send; the whole source thread is not
  automatically forwarded. Messages are limited to 4,000 characters and
  broadcast pings are removed.
- **A destination problem is actionable.** Missing or ambiguous names ask you
  to correct the name or mention the exact channel. An unjoined channel needs
  `/invite @Fredrin`; an archived one must be unarchived or replaced. A name
  lookup past the five-page bound asks for a mention or ID, and a different
  Slack workspace is refused. If delivery cannot be confirmed after a network
  failure, check the destination before retrying to avoid a duplicate.

- **Just say it.** There is no command grammar to learn. *"hey can you make a
  ticket for the login bug"*, *"the signup page is broken, file that"*,
  *"what's in review?"*, *"move FRED-AB12CD to review"*, *"assign it to Sam"*,
  *"start FRED-AB12CD"* all work. The bot writes a board-shaped title rather
  than pasting your message, files into the project's entry column (Backlog on
  a default board), and answers in the thread. Attribution goes to the member
  who connected Slack, and a title identical to one filed in the last 24 hours
  is rejected as a duplicate.
- **Follow-ups work in a thread, and don't need the tag.** Once the bot has
  replied in a thread, just keep talking: *"actually put that in the other
  project"*. It re-reads the thread for context each time and stores nothing
  between messages — the thread itself is the memory. (This needs the
  `message.channels` event, which the manifest subscribes; on a hand-made app
  without it, mention the bot again each time.) Outside its own threads the bot stays quiet: a new top-level message in
  the channel still needs `@Fredrin`, and a thread it has never posted in is none
  of its business.
- **It asks before anything you'd want back.** Completing a ticket, taking a
  ticket off its current assignee, and starting a Worker (which spends real
  compute) each get a confirmation first. Reply in the same thread to go ahead —
  a plain `yes` is enough. (The ask says `@Fredrin yes`, which always works and
  is the only form that works without the `message.channels` event.) Only the
  person who asked can confirm, and a confirmation goes stale after ten minutes.
- **Several projects on one Slack app?** Name one: *"file it in my-project"*,
  `@Fredrin status my-project`. With a single connected project you never need
  to. If it is ambiguous the bot asks rather than guessing.
- **Board glance.** `@Fredrin status` lists per-column counts and answers
  instantly — it is the one phrase that skips the assistant entirely, so it
  costs nothing and never queues. Add a column and it names the tickets instead
  of counting them: `@Fredrin status review`, `@Fredrin status building`. The
  column can be its board name, its short key, or enough of the name to be
  unambiguous — if two columns match, the bot asks which. Asking in your own
  words (*"what's in review?"*, *"which tickets are running?"*) works too and
  goes through the assistant.
- **A listing shows the PR.** Each ticket in a column listing (or a search
  result) ends with what the board card shows: its pull request and whether it
  merged, how its verification run went, who it is assigned to, its priority and
  its due date — `FRED-AB12CD Fix the login bug — PR #1234 open · 5/6
  verification checks · Jae Lee · High · due 2026-08-20`. So *"what's in
  review?"* answers with which tickets are actually waiting on a merge, not just
  their titles. A ticket with none of that adds nothing to its line. The
  verification run is *stated*, never graded — same as on the board, because
  most of those checks are not merge-blocking and a partial run is not a
  failure.
- **One ticket, in full.** Ask about a specific ticket — *"what's the status of
  FRED-AB12CD?"*, *"did that one ship?"*, *"is its PR merged?"*, *"what did
  people say on it?"* — and you get its column, every linked PR with its state
  and check outcome, the assignee and creator, labels, priority, due date,
  goals, anything still blocking it, its description and its latest comments.
  Long descriptions and comments are shortened to a line each, and the reply
  says the real comment count when it is showing only the newest few.
- **Ask about the code.** *"how does auth work in this project?"*, *"where is
  the rate limiter?"*, *"what does `webhook-slack.ts` do?"* — the bot hands the
  question to the project's own coding agent, which reads the actual repository
  on the paired machine of the member who connected Slack and posts what it
  finds back into the thread, usually within a minute or two. The run is
  read-only (it cannot edit anything) and uses that machine's own Claude
  subscription. It needs that member's paired machine to be online, already
  holding a clone of the project, and running an up-to-date Fredrin agent — the
  bot tells you in the thread when one of those is missing. Codebase questions
  work from mentions and thread replies, not from `/fredrin` (the answer needs
  a thread to land in).
- **Privacy.** Slash replies are ephemeral (only you see them); mention replies
  are threaded in the channel. Alerts carry the ticket identifier, title,
  project and column name only. Nothing the bot posts is agent conversation content — no Worker prompts
  or replies, no terminal output, no screenshots. A ticket's own description and
  comments are board data and can be read back into the channel; if that is not
  wanted in a shared channel, use the bot from a private one.
- **Acting on an alert.** Alerts name the ticket (`FRED-XXXX`); open the
  Fredrin desktop app to review the diff and ship or send back.
- **Automatic alerts are per connection.** One automatic-alert channel per
  Slack connection; on-demand destinations are chosen per request. Connect at the **workspace** level and every project alerts into
  that channel; give one project its own Slack connection (**Override for this
  project**) and it gets its own channel and its own alert events.

## Troubleshooting

- **The event Request URL won't verify in Slack.** Check it is exactly
  `https://www.fredrin.com/api/webhooks/slack` and that the Fredrin server is
  reachable from the internet (a `localhost` dev server is not — see step 1).
  You do **not** need to connect the connector first any more: the handshake is
  answered before any credential is involved, precisely so Slack can verify the
  URL while creating the app from the manifest.
- **`@Fredrin` gets no reply.** The bot must be **in the channel**
  (`/invite @Fredrin`); the app must be subscribed to the **`app_mention`**
  event (step 4); and the connector must be connected with the signing secret.
- **Untagged replies in a thread are ignored.** Three usual causes: the
  **`message.channels`** bot event is not subscribed (step 4 — `message.groups`
  for private channels); the **`channels:history`** scope is missing, so the bot
  cannot check whether the thread is one of its own; or the bot has not actually
  posted in that thread yet. It deliberately only answers untagged messages in
  threads it has spoken in — start the conversation with `@Fredrin` and every
  reply after that works without the tag.
- **The bot says it can't read the thread.** Add the **`channels:history`**
  scope (**`groups:history`** for private channels) in *OAuth & Permissions*
  and reinstall the app. Without it the bot can answer a one-shot request but
  cannot see a thread, so follow-ups, confirmations and untagged replies do not
  work.
- **It replies "Slack intake needs an OpenRouter key".** The assistant runs on
  an LLM, and this Fredrin deployment has no OpenRouter key configured. An
  admin adds one in the admin console; `status` and `help` keep working
  meanwhile.
- **It says it is being asked a lot in this channel.** Each channel gets a
  budget of about five assistant replies a minute so a busy channel cannot run
  up a bill. Wait a moment and ask again; `status` is never throttled.
- **`/fredrin` shows a Slack error ("dispatch_failed").** Same causes: request
  URL wrong, or the connector was connected without the signing secret →
  reconnect and paste it.
- **No automatic alerts arrive.** New connections start with alerts off. Open
  **Automatic status alerts**, turn them on, choose a channel and at least one
  column, then save. The bot must be in the channel; private channels need an
  invitation. These settings do not affect requested messages.
- **A column I ticked posts nothing.** The alert set belongs to the credential
  that this project *resolves* to. If the project inherits the workspace Slack
  connection, change the set in **Settings → Workspace → Connectors**; if it has
  its own override, change it on the project row. The two are separate.
- **"Slack rejected the bot token."** The token was revoked or reinstalled
  away. Reinstall the app to your Slack workspace and reconnect with the fresh
  `xoxb-` token — blank optional fields keep their stored values.
- **Commands rejected after long queues or clock skew.** Requests older than
  five minutes are refused (replay protection). Just re-run the command.

## Security notes

- The bot token and signing secret are stored AES-256-GCM-encrypted in your
  Fredrin workspace and are never logged or shown again.
- Every inbound request that *does* anything — a mention, a thread reply, a
  slash command — is verified against your app's signing secret (timing-safe
  HMAC with a five-minute replay window) before anything is read or created.
  Unverified requests are rejected. The one exception is Slack's URL-verification
  handshake, which is answered without a signature because it has to work before
  your app exists: the reply is the challenge string Slack itself just sent,
  echoed back. It reads nothing, identifies no workspace and writes nothing.
- Redelivered events are deduplicated, and the bot never acts on bot-authored
  messages (no reply loops).
- Alerts are one-way notifications with ticket metadata only — identifier,
  title, project and column name; disconnecting the connector stops both
  directions immediately, and turning off **Automatic status alerts** stops
  status notifications while retaining on-demand messaging.
- **The assistant reads the thread it is replying in, and sends it to an LLM.**
  When you talk to the bot inside a thread, that thread's recent messages are
  read at that moment and included in the request to the model provider
  (OpenRouter) so follow-ups make sense. It is never read from any other
  channel or thread, and **none of it is stored** — not by Fredrin, not between
  messages. The only things written to your board are the ones you asked for: a
  ticket, a comment, a move. A request to send a message posts only the requested
  content to the named channel; that content is not stored by Fredrin.
- **What `message.channels` does and does not expose.** Subscribing to it means
  Slack delivers every message in the channels the bot is in. Fredrin acts on
  one slice of that — a reply inside a thread its own bot has posted in — and
  reads a thread's contents only for such a reply. A message anywhere else, and
  every thread the bot has not spoken in, is discarded without being read,
  answered, sent to a model, or stored. If that is more than you want in a shared
  channel, leave the event unsubscribed and keep tagging the bot; everything else
  works unchanged.
- **A codebase question leaves Slack for your own machine.** When someone asks
  about the code, the question (as the assistant rephrases it) is sent to the
  paired machine of the member who connected Slack and answered there by that
  machine's Claude Code, reading the local clone — on that member's own
  subscription. The run is read-only, and nothing about it is stored by
  Fredrin: not the question, not the answer, no session, no transcript. The
  answer — which is derived from your repository's contents — is posted into
  the channel thread, so treat channels the bot is in as places repo detail
  may appear.
- **Anyone who can mention the bot can act as the member who connected Slack.**
  Slack identities are not mapped to Fredrin accounts, so the bot carries out
  every request with that member's permissions — including starting a Worker,
  which spends compute. The in-thread confirmation is there to catch mistakes,
  not to authorize: whoever can ask can also confirm. Invite the bot only to
  channels whose members you would give that access to.
