Skip to main content
Connect Slack to Cognee so your team can query and grow the knowledge graph without leaving a channel or DM. Cognee ships more than one way to reach Slack. Pick the one that matches what you run:
The built-in app, the standalone bot, and the cloud connector are three separate Slack apps (different manifest, different scopes, different transport) — do not point one at another’s server.

Built-in Slack App

The Slack app that ships inside the cognee package: slash commands and message shortcuts, served by your own self-hosted backend, with memory scoped to each linked member. Once a workspace is connected, anyone in it can search memory with /cognee-ask, save new facts with /cognee-remember, capture existing messages with the Remember this shortcut, and review every answer privately before sharing it with the channel. The member who connected the workspace can additionally import past conversations in bulk with /cognee-import. Requires cognee >= 1.5.0. On older releases /cognee-remember falls through to “Command /cognee-remember is not yet supported.” while /cognee-ask and Remember this work. History import (/cognee-import, Remember entire thread, and the /api/v1/slack/history endpoints) requires cognee >= 1.6.2.

Why Use This Integration

  • Ask from anywhere in Slack: /cognee-ask <question> searches your memory and replies privately, with a Share button to post the answer to the channel once you’ve checked it, or Discard to drop it.
  • Save what Slack never saw: /cognee-remember <text> stores free text nobody typed into Slack yet — a decision from a call, a conclusion recorded after the fact.
  • Save what Slack already has: the Remember this message shortcut (in a message’s ⋯ menu) stores any Slack message into Cognee memory, tagged with who said it and where.
  • Import conversations that already happened: /cognee-import pulls selected channels’ past days — or a whole thread via Remember entire thread — into a dataset you choose, keeping authors, timestamps and permalinks, and can keep that selection refreshed every 6 hours.
  • Per-user, not per-workspace: each member runs /cognee-link once so their asks and saves use their own Cognee account — without it, Cognee refuses to act for them rather than falling back to anyone else’s memory.
  • Self-hosted, no middleman: your Slack app talks directly to your Cognee backend. No data passes through a third-party relay.
One Slack app is installed once per workspace, but memory is per-person — that’s what /cognee-link exists to resolve. A member who skips it is refused, not silently attributed: the only unlinked member Cognee will act for is the one who completed the workspace’s OAuth Connect (their own account is used), and everyone else gets “I don’t know which Cognee account you are yet. Run /cognee-link to connect yours, then try again.” — for /cognee-ask, /cognee-remember, and Remember this alike. Nothing is written into someone else’s memory as a fallback. Workspaces connected before the installer’s Slack id was recorded have no installer exception at all, so every member there must link.
All replies are ephemeral (visible only to the person who triggered them) except a shared answer, which posts to the channel.

Prerequisites

  • Cognee >= 1.5.0 or a dev checkout — see the version note above.
  • A running Cognee backend (and frontend, for the /cognee-link confirmation page) reachable over HTTPS from the public internet — Slack will not call localhost. For local development, use a tunnel like ngrok (below); for production, this is your deployment’s normal public domain.
  • A Cognee account for each Slack member who wants to link their own memory.

Local Development: ngrok Tunnel

Slack requires a public HTTPS URL for every request URL. ngrok tunnels a public HTTPS host to your local port 8000 — that host is what every <public-host> placeholder below refers to. Skip this section if your backend already has a public domain.
  1. Install ngrok: brew install ngrok on macOS, or download the binary from ngrok.com/download.
  2. Authenticate once: sign up for a free account, copy your authtoken from the dashboard, and run ngrok config add-authtoken <token>.
  3. Start the tunnel:
ngrok prints a line like Forwarding https://<random>.ngrok-free.app -> http://localhost:8000. That https://<random>.ngrok-free.app host goes into the manifest and .env below. Leave the process running the whole time you’re testing — closing it kills the tunnel and every request URL stops resolving.
A free plan’s random host changes on every restart, which means re-editing the Slack app’s request URLs and .env each time. A free account includes one reserved domain that fixes this permanently: create it under Domains in the ngrok dashboard, then run ngrok http 8000 --domain=your-name.ngrok-free.app. Set the manifest and SLACK_REDIRECT_URI to that fixed host once and it never changes across restarts.
Free-tier ngrok shows a “Visit Site” interstitial page to browsers opening the tunnel URL directly. It does not apply to Slack’s server-to-server requests (commands, events, interactivity) — those go straight through, so it never blocks the integration itself.

Setup

1. Create the Slack App from the Manifest

Create a separate Slack app per environment (local dev, staging, production) — and per developer. Never point the production app’s manifest at your laptop: reusing one app means whoever else is testing has their events routed to your tunnel instead of the real server.Also prefer a workspace where only your app is installed. Slack lets multiple apps register the same slash command and shows a disambiguation dropdown — so in a shared workspace with several Cognee apps, /cognee-remember may silently go to someone else’s backend, and the reply looks like your server misbehaving.
  1. Go to api.slack.com/apps → Create New App → From an app manifest.
  2. Paste the manifest below, replacing every <public-host> with your backend’s public host.
  3. On the app’s Basic Information page, copy Client ID, Client Secret, and Signing Secret — you’ll need them for .env in step 2.
  4. Install the app to your workspace when prompted.
Only the callback_ids are matched server-side (remember_this, remember_thread) — the shortcut names above are yours to choose. The Options Load URL (message_menu_options_url) is what fills the import dialog’s destination-dataset picker; without it the picker comes up empty. token_rotation_enabled: false is still the right default — most Slack apps don’t rotate. If you do turn it on, Cognee handles it: a refresh writes the new tokens only, so the channel allowlist, saved sync selection, installer identity and workspace ownership all survive rotation, and an import pre-emptively refreshes a token within 120 seconds of expiry.
Editing the manifest of an existing app is not enough on its own. Slack does not grant a new command to an already-installed app — it refuses the command client-side and nothing ever reaches your server, which looks exactly like a broken backend. After editing the manifest to add a command or shortcut, reinstall the app to the workspace, then reload the Slack client (Cmd+R) so autocomplete picks it up.New bot scopes need more than a reinstall: Slack never grants them to an existing Cognee connection retroactively. Workspaces connected before history import existed lack channels:history, groups:read and groups:history — disconnect and reconnect the workspace from the Cognee Integrations page; re-saving the allowlist or reinstalling without a fresh OAuth exchange does not backfill them. Until then, /cognee-import fails rather than reading anything.
Granting the scopes does not start an import. Nothing is read until someone explicitly selects channels or threads — see Importing Channel and Thread History below. For loading history in bulk from your own code instead, dlt’s slack_source is still an option: see the dlt integration.

2. Configure Cognee

Copy the app’s credentials into your backend’s .env: Then bring the stack up from source — it runs exactly the checkout you have, with no image layer in between to go stale:
uv sync matters more than it looks: branches move pyproject.toml/uv.lock, and a venv left over from another branch fails in ways that read as application bugs rather than a dependency mismatch. Restart the backend after any .env change.

3. Install and Connect

  1. Installing the app from the manifest (step 1) prompts the workspace install once.
  2. In the Cognee frontend, go to Integrations → Connect under Slack. This runs the OAuth exchange and stores the workspace-level bot credential.
  3. In Slack, run /cognee-link and open the link it replies with in a browser where you’re already logged in to Cognee, then click Confirm. The link is a short-lived signed code (10-minute expiry), not an API key — nothing secret is ever typed into Slack. This is what points /cognee-ask and the save actions at your memory — and for anyone who didn’t run Connect, what makes them work at all (see the per-person note above).
  4. Try /cognee-ask What does Cognee do? — you should get a private reply with Share / Discard buttons.
Use the frontend’s Connect button, not Slack’s “Install to Workspace”. The OAuth state is what binds the install to a Cognee account. Slack’s own install button sends no signed state, so the callback rejects it and you land on /integrations?slack=error_invalid_state.

Using It

Ingest something before you ask. /cognee-ask only searches — it never writes, so it cannot create the slack dataset. On a completely empty memory it fails with CollectionNotFoundError: Collection 'DocumentChunk_text' not found, surfaced as “Search failed. Please try again.” — which reads like a broken integration but just means there is nothing to search. Run /cognee-remember (or ingest via the CLI) first.
@-mentioning the bot does nothing. The SDK app subscribes to app_home_opened, app_uninstalled, and tokens_revoked only — there is no app_mention handler. Use the slash commands. If you want @cognee <question> answered from channel memory, that is what the standalone channel-memory bot does. No /invite @cognee is needed for any of the above either. Every reply — including Share, which posts to the channel — is delivered by Slack on the app’s behalf regardless of channel membership.

Restricting Which Channels Can Run Commands

An opt-in, per-workspace channel allowlist is available from the Integrations page, backed by two authenticated endpoints: GET /api/v1/slack/channels lists the workspace’s public channels and flags which are currently allowed, PUT /api/v1/slack/channels sets the list. An empty list (the default) means unrestricted — a workspace that never touches this setting keeps working everywhere. This requires the channels:read scope (already in the manifest above). If a workspace connected before that scope existed, the channel list call fails — the fix is to disconnect and reconnect the workspace, not to re-save the allowlist. Once a non-empty allowlist exists, every command in every other channel returns “Cognee isn’t enabled in this channel.” — worth remembering when testing from a scratch channel. A non-empty allowlist also constrains history imports: it is checked against the source channels being imported, not just the channel a command was typed in. Selecting a channel outside it fails with “A selected channel is outside the configured Slack allowlist.”

Importing Channel and Thread History

/cognee-import imports past Slack conversations into an existing Cognee dataset. Unlike /cognee-remember, which captures one message at a time, this reads channel history in bulk — so it is restricted to the one Slack member who completed the workspace’s OAuth Connect. Everyone else gets “Only the person who connected this Slack workspace can import history.” In Slack, a workspace connected before the installer’s Slack id was recorded gets that same message even for its owner (see troubleshooting below); over the HTTP API it gets “Reconnect Slack to identify the user authorizing history access.” The dialog. /cognee-import (or /cognee-import 14, where the number is the past-days default) opens a modal with:
  • Destination dataset — a searchable picker listing only datasets the connecting user holds read, write and delete on. All three are required because re-importing an edited conversation replaces the stored document, which can fall back to a delete-and-re-add.
  • Channels — a multi-select over public and private channels.
  • Past days (for channel selection) — prefilled from the command argument.
  • Thread links (one per line) — https://<workspace>.slack.com/archives/… message or thread links. A selected thread is imported in full, including replies outside the date range.
  • Import options — two checkboxes: “Include older threads active during the period (slower)” and “Keep this selection synced every 6 hours”.
The date range picks which conversations to import; it is not a retention window. By default (thread_mode: started) it selects threads started in the period. The “older threads active during the period” checkbox (thread_mode: active) also scans older roots and keeps those with a message in the window — more Slack calls, but it catches a late reply on an old thread. Submitting clears the dialog immediately and the import runs in the background; the result comes back as an ephemeral message: “Imported N conversations (M messages) into Cognee.” A large import can outlive Slack’s response URL (valid for 30 minutes), so a missing message isn’t proof of failure — check the status endpoint below, and if the last run failed, retry; imports are idempotent.
The destination dataset is the access boundary. Importing a private channel makes its contents readable by everyone with read access to that dataset — source Slack membership is not copied into Cognee ACLs, and node sets do not enforce permissions. Pick a restricted dataset for private conversations. The dialog says as much, but it is worth deciding before you submit.
Before importing, invite the Cognee app to every channel you select (“Invite the Cognee app to each selected channel before importing.”), and make sure the connecting Slack user is a member of each one too (“The connecting Slack user must be a member of every selected channel.”) — the installation’s bot may see private channels its installer cannot, so bot membership alone isn’t treated as authorization. DMs and group DMs are not supported. How conversations are stored. One document per conversation, so replies keep their context. The body opens with the channel name and id, the workspace, the thread root and a permalink to it, then one entry per message as [ISO-8601 timestamp] <@author> (permalink) followed by the text. Joins, leaves, tombstones, empty messages and the Cognee bot’s own posts are skipped. Each document is tagged with the node sets slack, slack:<team_id> and slack:channel:<channel_id>. Document identity is derived from dataset + workspace + channel + thread root, so re-importing the same selection updates in place instead of duplicating; a conversation whose content digest is unchanged is skipped entirely.
Keeping a Selection Synced
Ticking “Keep this selection synced every 6 hours” saves the selection and enables a background refresh (interval_seconds, default 21600, range 600–604800). A refresh re-fetches previously imported conversations in the selected channels, so it catches edits, late replies and deletions — including changes made while the server was down. A stored document is deleted only when Slack explicitly answers thread_not_found/message_not_found for a channel it could still read, or when a re-fetch finds every message in the conversation now deleted, empty, or a join/leave; a lost scope, a transient error, or a conversation simply falling outside a shorter window never deletes anything. The window grows rather than rolls: saving a sync resolves “last N days” into a fixed start date, and every refresh runs from that date to now. A long-lived sync therefore fetches more each time and can eventually hit max_requests/max_messages — save it again to move the start date forward. A refresh that fails is retried after 10 minutes rather than after the full interval. An enabled sync must carry a selection and cannot pin a fixed latest date. Server-side, the worker runs inside the API process and is gated by SLACK_HISTORY_SYNC_ENABLED (default true; set it to false when an external scheduler drives refreshes instead). With the worker off, the dialog still saves the selection and tells you so: “Sync selection saved; this server’s refresh worker is disabled.”
HTTP Endpoints
Three authenticated endpoints sit under /api/v1/slack, alongside the channel-allowlist ones. All of them require a Cognee session or bearer token belonging to the Slack connection owner; JSON fields are snake_case.
Request fields, as validated by the API: The response reports conversations, messages, added, updated, unchanged and deleted. A runnable client is in examples/python/integrations/slack_history.py.
Run one API process against local embedded stores for imports. Cognee’s dataset locks and the refresh worker are process-local, so a multi-worker deployment has to designate a single history worker and serialize external import requests itself.

Removing the App from a Channel vs. Uninstalling

  • Removing/kicking the bot from a channel only stops it from posting there — it does not revoke anything; the workspace connection and everyone’s /cognee-links stay intact.
  • Uninstalling the app (or a token revoke) fires app_uninstalled/tokens_revoked, which revokes the stored workspace credential server-side — commands then fail with “not connected” until reconnected from Integrations.

Multiple Environments

Slack apps are tied to one set of OAuth credentials and one redirect URL each. If you run more than one Cognee deployment (local dev, staging, production), create a separate Slack app per environment rather than editing one app’s config back and forth — each environment gets its own SLACK_CLIENT_ID/SLACK_CLIENT_SECRET/SLACK_SIGNING_SECRET/SLACK_REDIRECT_URI. This avoids local testing ever breaking a shared staging or production connection.
This is separate from how many Slack workspaces one deployment can serve: any number of teams can each connect their own workspace to the same Cognee deployment through the same Connect flow — that’s already multi-tenant by design and needs no extra setup per workspace.

Troubleshooting

  1. Command `/cognee-remember` is not yet supported: most likely your backend is on a Cognee release older than 1.5.0, which has the integration but not this command. Run pip install -U cognee, or run the dev checkout directly.
  2. Same message, correct version installed: either the manifest you installed from predates the command and the app was never reinstalled (see the reinstall warning above — this is the most common cause), or you are running stale code — restart the backend process (or rebuild the container if containerized).
  3. Same message, but a different app name answers: another Cognee app in the workspace owns the command. Slack shows all of them in autocomplete — pick yours explicitly, or test in a workspace where only your app is installed.
  4. Typing the command produces nothing at all, no request reaches your server: the command is not registered on your app. Check api.slack.com/apps → your app → Slash Commands; if it’s missing, the manifest save silently failed. Add it via Create New Command, then reinstall.
  5. “Something went wrong when authorizing Cognee” during Connect: the redirect_uri your backend sent (from SLACK_REDIRECT_URI) isn’t byte-identical to an entry under the app’s redirect URLs, or .env still holds a different app’s SLACK_CLIENT_ID/SLACK_CLIENT_SECRET than the workspace you’re installing into. Re-sync .env to the exact app you’re testing and restart.
  6. Callback redirects to ?slack=error_exchange_failed: the callback never 500s — it catches everything and redirects. The traceback is in the backend log. Usual causes: SLACK_REDIRECT_URI mismatch, or a missing/malformed INTEGRATION_CREDENTIALS_KEY.
  7. Signature verification fails on every request (401): SLACK_SIGNING_SECRET in .env doesn’t match the app you actually installed — easy to hit when juggling more than one test app. Copy it again from Basic Information and restart the backend.
  8. Request URL mismatch after restarting ngrok: free-tier ngrok mints a new random host every restart. Update every request URL and redirect URL in the app’s Slack settings — or use a reserved/static domain so this never happens.
  9. Slack shows “dispatch_failed” instead of a reply: the slash command’s request URL is missing or wrong — it must point at /api/v1/slack/commands, not /api/v1/slack/events or /api/v1/slack/interactive.
  10. “This Slack workspace is not connected to Cognee”: the OAuth install (Connect in the Cognee UI) hasn’t completed for this workspace yet, or the connection was later disconnected/revoked.
  11. “I don’t know which Cognee account you are yet”: the member running the command has no active /cognee-link, and isn’t the member who completed the workspace’s Connect — so the command is refused before it reads or writes anything, rather than falling back to the installer’s memory. Run /cognee-link and confirm the link in a browser where you’re logged in to Cognee. No API key is involved; if the reply instead tells you to run `/cognee-link <api_key>`, you’re on code from before this was fixed — that argument never existed.
  12. Can’t delete a dataset — deletion hangs or errors: a running Cognee backend holds an exclusive lock on the graph database via a worker subprocess. Stop the server first, then delete.
  13. Asking returns an “I don’t know”-phrased answer as if it were real: known limitation — the ask handler substring-matches common refusal phrasings (“cannot answer”, “no relevant information”, “does not contain”, …) before forwarding an answer, but new phrasings can still slip through.
  14. “Only the person who connected this Slack workspace can import history”: history import is restricted to the member who completed the OAuth Connect, not to anyone with /cognee-link. If that is you, and you still see this, the connection predates the installer’s Slack id being recorded — reconnect from the Integrations page.
  15. /cognee-import fails on reading, or the import dialog’s dataset picker is empty: two different causes. Reading needs the new channels:history/groups:history/groups:read scopes, which existing installs only get by disconnecting and reconnecting (see the reinstall warning above). An empty picker usually means the app’s Options Load URL isn’t set to /api/v1/slack/interactive, or the connecting user has no dataset with all three of read, write and delete.
  16. “Slack request limit reached” / “Slack message limit reached” / “Slack returned an incomplete page. Nothing was imported.”: an import is all-or-nothing by design — it never stores a partial snapshot. Narrow the selection (fewer channels, fewer days, drop thread_mode: active), or raise max_requests/max_messages on the HTTP endpoint.
  17. “The Slack token does not belong to the selected workspace” or “A thread link belongs to another Slack workspace”: the {team_id} in the URL, the stored token, and the workspace host in every pasted permalink must all agree. Copying a link out of a different workspace is the usual cause.
  18. “Slack access changed during the import. Retry with the current selection.”: permissions are re-checked after the fetch — the connection was reconnected, or the channel allowlist changed, while the import was running. Just retry; imports are idempotent.
  19. “Cognee could not ingest a conversation. Retry the import.”: fetching succeeded but indexing failed. Native writes are idempotent rather than transactional, so re-running the same selection resumes it — unchanged documents are skipped and indexing is retried.
  20. “An import for this workspace and dataset is already running.”: only one import (or scheduled refresh) runs per workspace and destination dataset at a time. Wait for it to finish — check GET /api/v1/slack/history/{team_id} — then retry.
  21. A command or shortcut is acknowledged, but the follow-up reply never arrives: every deferred reply — a /cognee-ask answer, a /cognee-remember or Remember this confirmation, a /cognee-import result — is delivered by POSTing to the response_url Slack sent in the webhook payload, and that URL is accepted only in its canonical form: https://hooks.slack.com/commands/…/…/… or https://hooks.slack.com/actions/…/…/…, exactly three path segments of letters, digits, _ or -, no embedded credentials, no port other than 443, no query string and no fragment. Anything that fails those checks is dropped before any outbound request, with “Refusing to deliver to an invalid https://hooks.slack.com response_url” in the backend log; a valid one is rebuilt from https://hooks.slack.com plus the checked path rather than used as received. There is no error reply, because the original webhook has already been acked, so the member just sees nothing. Genuine Slack payloads always pass; this only bites a self-hosted deployment whose proxy or test harness rewrites response_url. Pass Slack’s value through unchanged.

Standalone Channel-Memory Bot

cognee-integration-slack is a separate Slack bot from the cognee-integrations repository. Instead of per-person slash commands, it gives a workspace per-channel memory: once a channel opts in, the bot silently ingests its messages into that channel’s own Cognee dataset, and anyone can ask @cognee <question> or /recall <question> to get an answer with cited links back to the source Slack messages. It is a thin client to a running Cognee server over HTTP (POST /api/v1/add, /cognify, /search, /forget) — no in-process Cognee and no LLM key of its own, since the server holds those. Slack I/O runs over Socket Mode, so it needs no public URL, no tunnel, and none of the manifest or OAuth setup above.

Install

The package is not published on PyPI yet. Install it from the repository:
Requires Python 3.10+. The only runtime dependencies are slack-bolt, aiohttp, and httpx. uv sync creates a project venv without activating it, so run the bot through uv run (below) or activate .venv first; pip install -e . into an already active environment needs neither.

Create the Slack App

In api.slack.com/apps, create an app with Socket Mode enabled, then:
  1. Copy the bot token (xoxb-…) and the app-level app token (xapp-…).
  2. Subscribe the bot to the message.channels and app_mention events.
  3. Add the slash commands /recall, /cognee-optin, /cognee-optout, and /cognee-forget.
  4. Install the app to the workspace and invite the bot to the channels it should remember.

Configure and Run

The bot reads everything from environment variables (a .env.example ships in the package directory):
Opt-in state lives in the bot’s process memory. /cognee-optin and /cognee-optout mutate a set that starts from COGNEE_SLACK_OPTED_IN_CHANNELS and is never written back anywhere. After a restart, only the channels listed in that variable are remembered — put every channel that should stay opted in there, or opt them in again.

Commands

An empty question gets a usage hint back. A channel with no dataset yet — never opted in, or opted in with nothing cognified — returns “I couldn’t find anything about that in this channel’s memory yet.”: the server answers the search with a 404 for the unknown dataset, which the bot treats as an empty result. Once the channel has memory, an unanswerable question gets whatever the LLM produces from the available context; unlike the built-in app, the bot does not filter refusal phrasings. A server-side failure (LLM auth, rate limit, a failed cognify) returns “Sorry — I hit an error answering that.” rather than silence; the cause is in the bot’s logs.

How It Works

  • One dataset per channel. Messages land in the dataset slack_<channel_id>, tagged with a node set of [<channel_id>]. The dataset is the boundary /cognee-forget clears, so forgetting is per channel, never per person.
  • Batched graph building. Each message is added with a cheap add call; cognify runs once a channel has COGNEE_SLACK_COGNIFY_BATCH pending messages, and always before answering a question, so an answer reflects everything said so far. Concurrent cognify runs on one channel are serialized.
  • Cited answers. Answering runs two searches: GRAPH_COMPLETION for the prose, and CHUNKS filtered to the channel’s node set for the source messages. Each stored message carries a one-line provenance header ([cognee-slack] channel=… ts=… author=… permalink=…) inside its text, which is parsed back out of the retrieved chunks to build the links. The reply is a Block Kit message: the answer, then up to five sources as links labelled #<channel ID> · <user ID> · <UTC time> (raw Slack IDs, not names — the link itself opens the original message) with a +N more note for the rest. A message whose permalink could not be resolved shows as plain text, never a broken link.
  • What gets ingested. Messages and thread replies in opted-in channels. Edits, deletions, joins, bot messages, the bot’s own posts, messages that mention the bot, and empty messages are skipped. Public channels only, via message.channels. Edited or deleted Slack messages are not re-synced.

Tests

The suite runs without Slack, Cognee, or LLM credentials — the adapter runs against a fake HTTP client, the handlers against mocks:

Channel-Memory Bot Source

Source and tests for cognee-integration-slack.

dlt Integration

Pull Slack channel history in bulk via dlt, instead of a live bot.

Cloud Integrations UI

Where “Connect Slack” lives in the Cognee UI.

Node Sets

How per-channel node-set tags scope retrieval.