How to Auto-Log Slack Messages to a Notion Database
Summary
- The native Slack-Notion integration cannot reliably capture thread replies or route updates to the right people, so teams need a custom workflow that maps Slack messages and threads to Notion records.
- Use a Slack Bot User OAuth Token (
xoxb-) with the right history and user scopes, create an internal Notion connection, and explicitly add that connection to the target database or Notion returns401or empty results. - For thread-level logging, store the parent message
tsas a Thread TS property and append replies as blocks; add idempotency checks because Slack Events API retries can create duplicate rows. - Jinba Flow handles the Slack Events API endpoint, Slack and Notion connectors, retries, and rate limits, with 80% deterministic execution at roughly $5-20 per month at scale versus $300+ for stochastic AI-agent platforms.
Organizations that use both Slack and Notion encounter a recurring operational gap. Decisions and updates accumulate in Slack threads, but the Notion database intended to capture them remains empty. The interim solution is manual copy-pasting, which is not durable. Missing links in threads and uncreated Notion tasks cause tracking failures.
The native Slack-Notion integration does not close this gap. Notion's built-in automations can send Slack messages for overdue tasks, but they cannot route those messages to the right individual team members. Syncing Slack threads into Notion comments fails at the API level: Slack does not expose thread events cleanly for workflows, and Notion's API does not support comment streaming, so most setups are one-way or unreliable.
This article provides a complete, step-by-step workflow to auto-log Slack messages into a Notion database, including a working YAML example that can be run immediately.
Step 1: Configure Your Slack App
Navigate to api.slack.com/apps and create a new app "From scratch."
The required credential is a Bot User OAuth Token, prefixed with xoxb-. Bot tokens represent the app rather than a user identity, so the integration keeps working if the person who installed it leaves the workspace. User tokens (xoxp-) are tied to an individual and do not provide this stability.
Under OAuth and Permissions, add the following Bot Token Scopes:
channels:history: read messages from public channelsgroups:history: read messages from private channelsim:history: read direct messagesmpim:history: read group direct messagesusers:read: resolve user IDs such as<@U12345>into display names
Click Install to Workspace, then copy the Bot User OAuth Token and store it in a secrets manager or environment variable.
Step 2: Configure Your Notion Connection
Notion offers three authorization paths: internal connections, personal access tokens (PATs), and public connections via OAuth. For a private, single-workspace workflow, an internal connection is the correct choice. It is scoped to one workspace, requires no OAuth redirect flow, and uses a static credential called an internal connection API token.
Go to www.notion.so/my-integrations, create a new internal connection, and copy the Internal Integration Secret that Notion generates.
A common implementation gap: a valid token will still receive a 401 or empty result from the API if the connection has not been explicitly granted access to the target database. Open the Notion database, click the ... menu in the top-right corner, select Add connections, and choose the connection just created. This explicit grant is required. Without it, every API call fails.

Step 3: Design the Automation Logic
Two trigger models are available.
Event-driven (real-time): The Slack Events API reverses the usual direction: Slack sends a POST to the configured server when an event occurs. Subscribe to the message.channels bot event. Thread replies also fire this event and include a thread_ts field that identifies the parent message. Slack requires a fast HTTP 200 OK acknowledgement; it retries delivery when the endpoint does not respond promptly, which means the workflow must be idempotent.
Scheduled polling: Use the conversations.history method to fetch messages on a cron schedule. For threads, call conversations.replies with the parent message's ts as the thread_ts parameter.
Mapping Slack data to Notion
Notion's data model separates structured properties from free-form block content. Slack metadata belongs in properties; message body text belongs in blocks.
Slack field | Notion target |
|---|---|
| Title property |
| Author property (Text) |
| Channel property (Text or Select) |
| Posted At property (Date) |
| Thread TS property (Text, for deduplication) |
Message permalink | Slack Link property (URL) |
Full | Paragraph block via |
Use NOTION_CREATE_DATABASE_ITEM to create the row, then NOTION_APPEND_BLOCK_CHILDREN to attach the full message body as a paragraph block. Notion's rich_text property enforces a 2000-character limit, so long messages must go into blocks rather than properties.
Step 4: The YAML Workflow
The example below runs on Jinba Flow, which provides the Slack and Notion connectors, handles the Events API endpoint, and executes 80% of each run deterministically, making it more reliable and significantly cheaper than stochastic alternatives at scale.
# Jinba Flow: Log Slack channel messages to a Notion database
name: slack-to-notion-logger:
- id: INPUT_SLACK_EVENT
config:
event_type: message.channels
# channel_id: "C0123456789" # optional: filter to one channel
jobs:
log_message:
steps:
# ── New parent message ─────────────────────────────────────────────────
- id: is_new_thread
if: ${{ on[0].payload.thread_ts == null }}
run:
# Create a new Notion database item (one row per Slack thread)
- id: NOTION_CREATE_DATABASE_ITEM
config:
database_id: "YOUR_NOTION_DATABASE_ID"
properties:
"Title":
title:
- text:
content: "${{ on[0].payload.text.slice(0, 100) }}"
"Author ID":
rich_text:
- text:
content: "${{ on[0].payload.user }}"
"Channel ID":
rich_text:
- text:
content: "${{ on[0].payload.channel }}"
"Posted At":
date:
start: "${{ new Date(on[0].payload.ts * 1000).toISOString() }}"
"Thread TS":
rich_text:
- text:
content: "${{ on[0].payload.ts }}"
# Append the full message body as a paragraph block
- id: NOTION_APPEND_BLOCK_CHILDREN
config:
block_id: ${{ steps.NOTION_CREATE_DATABASE_ITEM.result.id }}
children:
- object: block
type: paragraph
paragraph:
rich_text:
- type: text
text:
content: "${{ on[0].payload.text }}"
# ── Thread reply ───────────────────────────────────────────────────────
- id: is_thread_reply
if: ${{ on[0].payload.thread_ts != null }}
run:
# Find the Notion page that represents the parent thread
- id: NOTION_QUERY_DATABASE
config:
database_id: "YOUR_NOTION_DATABASE_ID"
filter:
property: "Thread TS"
rich_text:
equals: "${{ on[0].payload.thread_ts }}"
# Append the reply as a new paragraph block on that page
- id: NOTION_APPEND_BLOCK_CHILDREN
if: ${{ steps.NOTION_QUERY_DATABASE.result.results.length > 0 }}
config:
block_id: ${{ steps.NOTION_QUERY_DATABASE.result.results[0].id }}
children:
- object: block
type: paragraph
paragraph:
rich_text:
- type: text
text:
content: "[Reply] ${{ on[0].payload.text }}"
The flow fires on every message.channels event. For a new parent message (thread_ts is null), it creates a Notion database item and immediately appends the full message body as a block. For a reply (thread_ts is present), it queries the database by the stored Thread TS value, then appends the reply to the matching page. This approach keeps one Notion page per Slack thread, which maps naturally to a case, ticket, or decision record.
Advanced Considerations
Handling Slack's mrkdwn formatting
Slack message text is not plain text. User mentions arrive as <@U12345>, channels as <#C12345|channel-name>, and links as <url|label>. Write a pre-processing step that calls users.info (using the users:read scope) to resolve user IDs into display names before the text reaches Notion. This keeps the Notion record readable without a Slack session open.
Preventing duplicates
The Slack Events API retries delivery when it does not receive a fast 200 OK. This means the workflow can receive the same event more than once. Before calling NOTION_CREATE_DATABASE_ITEM, query the database for an existing row where Thread TS equals on[0].payload.ts. If a match exists, skip creation. This idempotency check prevents duplicate rows and supports reliable integration.
Rate limits
Slack Web API methods are assigned one of four rate-limit tiers per method per workspace. conversations.history sits at Tier 3 (approximately 50 requests per minute). When a limit is exceeded, Slack returns HTTP 429 with a Retry-After header specifying the number of seconds to wait. For high-volume channels, a scheduled polling approach that batches requests is preferable to a real-time event-driven setup that may saturate the tier quickly.
Message-level versus thread-level granularity
Two storage models serve different purposes:
- Message-level: one Notion database row per Slack message. Useful for a searchable archive or a support triage log where every message warrants its own record.
- Thread-level: one Notion row per parent message, with replies appended as blocks. Useful for tracking decisions, cases, or requests that begin as a Slack thread and need a single durable record. The YAML example above implements this model.
Building a Reliable Slack-Notion Integration
The manual copy-paste cycle fails because there is no durable, automatic handoff between Slack and Notion. A custom workflow built on the Slack Events API and the Notion API closes that gap: every relevant message is recorded in the database, with its author, channel, timestamp, and full thread context intact, without manual intervention.
Jinba Flow handles the infrastructure this workflow requires: the Events API endpoint, pre-built Slack and Notion connectors, scheduled Tasks for polling approaches, and deterministic execution at 80% of each run. At scale, that runs at $5-20 per month, compared to $300 or more for stochastic AI-agent platforms or the cost of a consultant-led Power Automate implementation.
The result is a Slack-Notion integration that remains reliable: one Notion database, continuously updated from Slack, with no manual intervention and no missed threads.

Frequently Asked Questions
How do I automatically send Slack messages to a Notion database?
Slack-to-Notion logging can be automated with a custom workflow that listens to Slack events and creates Notion pages through the Notion API. The native Slack and Notion integration is limited: it can send notifications, but it cannot reliably capture every thread reply or map each message to the right database record. A typical setup uses a Slack bot token with channel history scopes, an internal Notion connection with explicit database access, and logic that maps each Slack message or thread to a Notion row.
Why does my Notion API return 401 even though my token is valid?
The Notion connection likely has not been granted access to the target database. A valid internal integration secret is not enough. Open the database, click the ... menu, select Add connections, and add the connection. Until access is granted, Notion returns 401 or empty results because the connection cannot see the database. Grant access explicitly, then retry the API call.
Should I use the Slack Events API or scheduled polling?
Use the Slack Events API for near-real-time logging, and scheduled polling for high-volume channels or simpler infrastructure. Event-driven workflows receive a POST from Slack when a message is posted, but Slack expects a fast 200 OK and will retry, so the workflow must be idempotent. Polling with conversations.history avoids a public endpoint and can batch requests, which helps stay within Slack's rate-limit tiers.
How do I log Slack thread replies to the same Notion page?
Store the parent message's ts value as a Thread TS property in Notion, then append each reply as a block on that page. When a reply arrives, query the Notion database for a row where Thread TS equals the reply's thread_ts, and append the reply text to that page. This creates one durable Notion record per Slack thread, which is better for decisions, tickets, and case tracking than one row per message.
How do I prevent duplicate Notion rows from Slack retries?
Make the workflow idempotent by checking for an existing Notion row before creating a new one. Slack can deliver the same event more than once when the endpoint is slow, so query the database using the message ts or thread_ts and skip creation if a match exists. This is the most important reliability check in a production Slack-to-Notion integration.
Can I sync private channels and direct messages to Notion?
Yes, but the configuration requires the right Slack bot scopes, and the bot must be a member of those conversations. Add groups:history for private channels, im:history for direct messages, and mpim:history for group DMs. Public channels need channels:history. Without membership and scopes, Slack will not return the messages even if the token is valid.
Do I need to code a custom Slack-Notion integration?
Not necessarily, but native options are limited. No-code tools can connect Slack and Notion, but they often struggle with thread replies, duplicates, and rich Slack formatting. A custom workflow or a platform like Jinba Flow handles the Slack Events API, Notion API, retries, and rate limits without requiring the infrastructure to be built from scratch. The YAML example in this article is a working starting point.
How much does a Slack-to-Notion integration cost?
A custom integration can cost little more than hosting and API usage, while managed platforms vary. Jinba Flow runs deterministic workflows at roughly $5-20 per month at scale, compared with $300 or more for stochastic AI-agent platforms or consultant-led Power Automate implementations. The main cost driver is volume: Slack API rate limits and Notion API calls determine how much can be batched or processed per minute.