

How to Archive Slack Messages to Notion with n8n
Automatically saves flagged Slack messages or full threads to a designated Notion database page the moment a reaction emoji is added, preserving decisions and discussions in your team's knowledge base.
Steps and UI details are based on platform versions at time of writing — check each platform for the latest interface.
Best for
Engineering or product teams who want to capture Slack decisions into Notion without copy-pasting, triggered by adding a specific reaction emoji to a message.
Not ideal for
Teams that need to archive every message in a channel automatically — that volume requires a different architecture with a database-backed deduplication layer.
Sync type
real-timeUse case type
backupReal-World Example
A 12-person product team uses this to save any Slack message reacted with 📌 directly into their Notion 'Decisions Log' database. Before the automation, important decisions buried in #product channel were lost within days and new hires had no context. Now every pinned message appears in Notion within 10 seconds, tagged with the channel, author, and timestamp.
What Will This Cost?
Drag the slider to your expected monthly volume.
Each platform counts differently — Zapier: 1 task per trigger. Make: 1 operation per module per record. n8n: 1 execution per run.





Prices shown for annual billing. Based on published pricing as of April 2026.
Estimated ROI
1000
min saved/mo
$583
labor value/mo
Free
no platform cost
Based on ~2 min manual effort per operation at $35/hr fully loaded labor cost.
Implementation
Import this workflow directly into n8n
Copy the pre-built n8n blueprint and paste it straight into n8n. All modules, filters, and field mappings are already configured — you just need to connect your accounts.
Before You Start
Make sure you have everything ready.
Field Mapping
Map these fields between your apps.
| Field | API Name | |
|---|---|---|
| Required | ||
| Message Text | ||
| Author | ||
| Channel | ||
| Timestamp | ||
| Message Link | ||
3 optional fields▸ show
| Archived By | |
| Reaction Used | |
| Thread Timestamp |
Step-by-Step Setup
api.slack.com/apps > Create New App > Event Subscriptions
Create a Slack App and Configure Event Subscriptions
You need a custom Slack App to receive reaction events via webhook — n8n's built-in Slack node uses this app under the hood. Go to api.slack.com/apps, click 'Create New App', choose 'From scratch', name it something like 'n8n Archiver', and select your workspace. Once created, navigate to 'Event Subscriptions' in the left sidebar and toggle it ON. You'll paste your n8n webhook URL here in step 3, after n8n generates it.
- 1Go to api.slack.com/apps and click 'Create New App'
- 2Choose 'From scratch', enter app name 'n8n Archiver', select your workspace
- 3Click 'Event Subscriptions' in the left sidebar
- 4Toggle 'Enable Events' to ON
- 5Leave the Request URL field empty for now — you'll return after step 3
This Code node runs between the users.info HTTP Request and the Notion node. It converts Slack's raw Unix timestamp to ISO 8601, builds a clean Slack message deep-link URL, and passes all transformed fields forward so your Notion node only needs simple expression references. Paste it into a Code node set to 'Run Once for Each Item' mode.
JavaScript — Code Node// Runs in n8n Code node — 'Run Once for Each Item' mode▸ Show code
// Runs in n8n Code node — 'Run Once for Each Item' mode
// Place this node between the users.info HTTP Request and the Notion node
const webhookData = $('Webhook').item.json.body.event;... expand to see full code
// Runs in n8n Code node — 'Run Once for Each Item' mode
// Place this node between the users.info HTTP Request and the Notion node
const webhookData = $('Webhook').item.json.body.event;
const messageData = $('HTTP Request').item.json.messages[0];
const userData = $('HTTP Request 1').item.json.user;
// Convert Slack's Unix timestamp (with microseconds) to ISO 8601
const slackTs = messageData.ts; // e.g. '1701432187.000400'
const tsSeconds = parseFloat(slackTs);
const isoTimestamp = new Date(tsSeconds * 1000).toISOString();
// Build a direct Slack message deep-link
// Slack deep-links require the timestamp without the period
const tsForUrl = slackTs.replace('.', '');
const channelId = webhookData.item.channel;
const messageLink = `https://slack.com/archives/${channelId}/p${tsForUrl}`;
// Resolve author name with fallback from real_name if display_name is empty
const authorName =
userData.profile.display_name ||
userData.profile.real_name ||
'Unknown User';
// Resolve the reactor (person who added the emoji) — separate from message author
const reactorId = webhookData.user;
// Truncate very long messages to 2000 chars (Notion rich text block limit)
const rawText = messageData.text || '';
const messageText = rawText.length > 2000
? rawText.substring(0, 1997) + '...'
: rawText;
// Detect if this message is part of a thread
const threadTs = messageData.thread_ts || null;
const isThreadReply = threadTs !== null && threadTs !== slackTs;
return {
isoTimestamp,
messageLink,
authorName,
messageText,
channelId,
reactorId,
reactionEmoji: webhookData.reaction,
isThreadReply,
threadTs: threadTs ? new Date(parseFloat(threadTs) * 1000).toISOString() : null
};api.slack.com/apps > [Your App] > OAuth & Permissions > Bot Token Scopes
Add OAuth Scopes to Your Slack App
Your Slack app needs specific permission scopes before it can read message content and reaction events. In the left sidebar of your app settings, click 'OAuth & Permissions'. Scroll down to the 'Bot Token Scopes' section. You need four scopes: reactions:read to detect when a reaction is added, channels:history to read the message content after a reaction fires, users:read to resolve the user ID to a display name, and channels:read to identify the channel name. Add each one individually.
- 1Click 'OAuth & Permissions' in the left sidebar
- 2Scroll to 'Bot Token Scopes' and click 'Add an OAuth Scope'
- 3Add 'reactions:read'
- 4Add 'channels:history'
- 5Add 'users:read'
- 6Add 'channels:read'
n8n Canvas > + > Webhook > HTTP Method: POST
Install the App and Set Up the n8n Webhook Trigger
Now install the Slack app to your workspace by clicking 'Install to Workspace' on the OAuth & Permissions page. Copy the Bot User OAuth Token that appears — it starts with xoxb-. Switch to n8n and open your workflow canvas. Click the '+' button to add a node, search for 'Webhook', and select the Webhook node. Set the HTTP Method to POST and copy the generated webhook URL from the 'Webhook URL' field at the bottom of the node panel. This URL goes back into Slack's Event Subscriptions page.
- 1Click 'Install to Workspace' on the OAuth & Permissions page and authorize
- 2Copy the Bot User OAuth Token (starts with xoxb-)
- 3In n8n, click '+' on the canvas and search for 'Webhook'
- 4Set HTTP Method to POST
- 5Copy the Production Webhook URL shown at the bottom of the node config panel
api.slack.com/apps > [Your App] > Event Subscriptions > Subscribe to Bot Events
Register the Webhook URL in Slack and Subscribe to Events
Go back to Slack's Event Subscriptions page for your app. Paste the n8n Production Webhook URL into the 'Request URL' field. Slack will immediately send a challenge request to verify the endpoint — n8n's Webhook node handles this automatically by returning the challenge value. You should see a green 'Verified' checkmark appear within 3 seconds. Then click 'Add Bot User Event' and add the event reaction_added. Save the changes.
- 1Paste the n8n webhook URL into the 'Request URL' field
- 2Wait for the green 'Verified' checkmark to appear
- 3Click 'Add Bot User Event' under 'Subscribe to Bot Events'
- 4Search for and select 'reaction_added'
- 5Click 'Save Changes' at the bottom of the page
n8n Canvas > + > IF Node > Conditions
Filter for Your Chosen Reaction Emoji
Every reaction added to any message in any channel will now hit your webhook — including thumbs-up, fire, and laugh emojis you don't want to archive. Add an IF node in n8n to filter specifically for your archival emoji (e.g., 📌 which Slack sends as 'pushpin'). Connect it after the Webhook node. In the IF node, set Condition 1 to: Value 1 = {{$json.body.event.reaction}}, Operation = 'Equal', Value 2 = pushpin. Only messages reacted with 📌 will continue to the next step.
- 1Click '+' after the Webhook node and add an 'IF' node
- 2Set Value 1 to expression: {{$json.body.event.reaction}}
- 3Set Operation to 'Equal'
- 4Set Value 2 to: pushpin (no emoji character — Slack sends the text name)
- 5Click 'Add Condition' if you want to support multiple emoji names with an OR condition
n8n Canvas > + > HTTP Request > Method: GET
Fetch the Full Slack Message Content
The reaction_added event only tells you which message was reacted to — it gives you a channel ID and a message timestamp, but not the actual message text. You need to call Slack's conversations.history API to retrieve the message. Add an HTTP Request node after the IF node's true branch. Set Method to GET, URL to https://slack.com/api/conversations.history, and add three query parameters: channel = {{$json.body.event.item.channel}}, latest = {{$json.body.event.item.ts}}, limit = 1. Add a Header: Authorization = Bearer xoxb-your-bot-token.
- 1Add an 'HTTP Request' node after the IF node's true output
- 2Set Method to GET
- 3Set URL to https://slack.com/api/conversations.history
- 4Under 'Query Parameters', add: channel = {{$json.body.event.item.channel}}
- 5Add: latest = {{$json.body.event.item.ts}} and limit = 1
- 6Under 'Headers', add: Authorization = Bearer xoxb-YOUR-TOKEN
n8n Canvas > + > HTTP Request > URL: slack.com/api/users.info
Resolve the User ID to a Display Name
The message payload contains a user field like U04XXXXXXXXX — not a readable name. Add another HTTP Request node to call Slack's users.info API and get the display name. Set Method to GET, URL to https://slack.com/api/users.info, Query Parameter: user = {{$json.messages[0].user}}, and the same Authorization header. The response will contain user.profile.display_name which you'll map to Notion in the next step.
- 1Add a second 'HTTP Request' node after the first one
- 2Set Method to GET
- 3Set URL to https://slack.com/api/users.info
- 4Add Query Parameter: user = {{$node['HTTP Request'].json.messages[0].user}}
- 5Add the same Authorization header with your bot token
This Code node runs between the users.info HTTP Request and the Notion node. It converts Slack's raw Unix timestamp to ISO 8601, builds a clean Slack message deep-link URL, and passes all transformed fields forward so your Notion node only needs simple expression references. Paste it into a Code node set to 'Run Once for Each Item' mode.
JavaScript — Code Node// Runs in n8n Code node — 'Run Once for Each Item' mode▸ Show code
// Runs in n8n Code node — 'Run Once for Each Item' mode
// Place this node between the users.info HTTP Request and the Notion node
const webhookData = $('Webhook').item.json.body.event;... expand to see full code
// Runs in n8n Code node — 'Run Once for Each Item' mode
// Place this node between the users.info HTTP Request and the Notion node
const webhookData = $('Webhook').item.json.body.event;
const messageData = $('HTTP Request').item.json.messages[0];
const userData = $('HTTP Request 1').item.json.user;
// Convert Slack's Unix timestamp (with microseconds) to ISO 8601
const slackTs = messageData.ts; // e.g. '1701432187.000400'
const tsSeconds = parseFloat(slackTs);
const isoTimestamp = new Date(tsSeconds * 1000).toISOString();
// Build a direct Slack message deep-link
// Slack deep-links require the timestamp without the period
const tsForUrl = slackTs.replace('.', '');
const channelId = webhookData.item.channel;
const messageLink = `https://slack.com/archives/${channelId}/p${tsForUrl}`;
// Resolve author name with fallback from real_name if display_name is empty
const authorName =
userData.profile.display_name ||
userData.profile.real_name ||
'Unknown User';
// Resolve the reactor (person who added the emoji) — separate from message author
const reactorId = webhookData.user;
// Truncate very long messages to 2000 chars (Notion rich text block limit)
const rawText = messageData.text || '';
const messageText = rawText.length > 2000
? rawText.substring(0, 1997) + '...'
: rawText;
// Detect if this message is part of a thread
const threadTs = messageData.thread_ts || null;
const isThreadReply = threadTs !== null && threadTs !== slackTs;
return {
isoTimestamp,
messageLink,
authorName,
messageText,
channelId,
reactorId,
reactionEmoji: webhookData.reaction,
isThreadReply,
threadTs: threadTs ? new Date(parseFloat(threadTs) * 1000).toISOString() : null
};n8n Canvas > + > Notion > Resource: Database Page > Operation: Create
Connect n8n to Notion and Create the Database Page
Add a Notion node after the user lookup step. In the node's Credential settings, click 'Create New' and authenticate with your Notion account — this opens a Notion OAuth screen where you must explicitly share the target database with the integration. Set the Operation to 'Create' and Resource to 'Database Page'. In the 'Database ID' field, paste the ID of your Notion archival database (it's the 32-character string in the database URL after the workspace name and before the ?v= parameter).
- 1Add a 'Notion' node after the users.info HTTP Request node
- 2Click 'Credential to connect with' and choose 'Create New'
- 3Complete the OAuth flow and grant n8n access to your workspace
- 4Set Resource to 'Database Page'
- 5Set Operation to 'Create'
- 6Paste your Notion database ID into the 'Database ID' field
n8n Canvas > Notion Node > Properties > Add Property
Map Slack Fields to Notion Database Properties
Your Notion database needs matching properties before you can map data into it. Create these properties in Notion if they don't exist: Message Text (Text), Author (Text), Channel (Text), Timestamp (Date), Message Link (URL), and Archived By (Text). Back in n8n's Notion node, click 'Add Property' for each field. Map Message Text to {{$node['HTTP Request'].json.messages[0].text}}, Author to {{$node['HTTP Request 1'].json.user.profile.display_name}}, and Timestamp to a formatted date using the Code node output from the pro tip below.
- 1In the Notion node, click 'Add Property'
- 2Select 'Message Text' and set value to {{$node['HTTP Request'].json.messages[0].text}}
- 3Add 'Author' property mapped to {{$node['HTTP Request 1'].json.user.profile.display_name || $node['HTTP Request 1'].json.user.profile.real_name}}
- 4Add 'Channel' mapped to {{$json.body.event.item.channel}}
- 5Add 'Timestamp' as a Date property with formatted value from the Code node
- 6Add 'Message Link' as URL: https://slack.com/archives/{{$json.body.event.item.channel}}/p{{$json.body.event.item.ts.replace('.', '')}}
n8n Canvas > + > Code Node > JavaScript Mode
Add a Code Node to Format the Slack Timestamp
Slack timestamps look like 1701432000.000000 — a Unix timestamp with microseconds. Notion's Date property needs an ISO 8601 string like 2023-12-01T14:00:00.000Z. Insert a Code node between the user lookup and the Notion node to handle this conversion. Paste the code from the Pro Tip section below. The node outputs a clean isoTimestamp field you'll reference in the Notion node's Timestamp property as {{$node['Code'].json.isoTimestamp}}.
- 1Click '+' between the users.info node and the Notion node
- 2Add a 'Code' node
- 3Set Mode to 'Run Once for Each Item'
- 4Paste the timestamp formatting code from the Pro Tip section
- 5Click 'Execute Node' to verify the output
This Code node runs between the users.info HTTP Request and the Notion node. It converts Slack's raw Unix timestamp to ISO 8601, builds a clean Slack message deep-link URL, and passes all transformed fields forward so your Notion node only needs simple expression references. Paste it into a Code node set to 'Run Once for Each Item' mode.
JavaScript — Code Node// Runs in n8n Code node — 'Run Once for Each Item' mode▸ Show code
// Runs in n8n Code node — 'Run Once for Each Item' mode
// Place this node between the users.info HTTP Request and the Notion node
const webhookData = $('Webhook').item.json.body.event;... expand to see full code
// Runs in n8n Code node — 'Run Once for Each Item' mode
// Place this node between the users.info HTTP Request and the Notion node
const webhookData = $('Webhook').item.json.body.event;
const messageData = $('HTTP Request').item.json.messages[0];
const userData = $('HTTP Request 1').item.json.user;
// Convert Slack's Unix timestamp (with microseconds) to ISO 8601
const slackTs = messageData.ts; // e.g. '1701432187.000400'
const tsSeconds = parseFloat(slackTs);
const isoTimestamp = new Date(tsSeconds * 1000).toISOString();
// Build a direct Slack message deep-link
// Slack deep-links require the timestamp without the period
const tsForUrl = slackTs.replace('.', '');
const channelId = webhookData.item.channel;
const messageLink = `https://slack.com/archives/${channelId}/p${tsForUrl}`;
// Resolve author name with fallback from real_name if display_name is empty
const authorName =
userData.profile.display_name ||
userData.profile.real_name ||
'Unknown User';
// Resolve the reactor (person who added the emoji) — separate from message author
const reactorId = webhookData.user;
// Truncate very long messages to 2000 chars (Notion rich text block limit)
const rawText = messageData.text || '';
const messageText = rawText.length > 2000
? rawText.substring(0, 1997) + '...'
: rawText;
// Detect if this message is part of a thread
const threadTs = messageData.thread_ts || null;
const isThreadReply = threadTs !== null && threadTs !== slackTs;
return {
isoTimestamp,
messageLink,
authorName,
messageText,
channelId,
reactorId,
reactionEmoji: webhookData.reaction,
isThreadReply,
threadTs: threadTs ? new Date(parseFloat(threadTs) * 1000).toISOString() : null
};n8n Canvas > Test Workflow button > Active toggle (top bar)
Test End-to-End and Activate the Workflow
With all nodes connected, click 'Test Workflow' in n8n. Go to Slack and add a 📌 reaction to any message in a channel where your bot is present. Return to n8n within 30 seconds — you should see green execution indicators on every node in the chain. Check your Notion database for the new page. If it appears with correct text, author, and timestamp, the workflow is working. Click the toggle at the top of the n8n canvas to set the workflow from Inactive to Active.
- 1Click 'Test Workflow' in the n8n top bar
- 2In Slack, add a 📌 reaction to any message in a public channel your bot is in
- 3Watch each node light up green in n8n (takes 5-10 seconds)
- 4Open your Notion database and verify the new page appears
- 5Click the 'Inactive' toggle in the top bar to switch the workflow to 'Active'
Going live
Production Checklist
Before you turn this on for real, confirm each item.
Troubleshooting
Common errors and how to fix them.
Frequently Asked Questions
Common questions about this workflow.
Analysis
Use n8n for this if you want full control over the API calls and your team has at least one person comfortable reading JSON. This workflow makes three API calls per trigger — Slack's conversations.history, users.info, and Notion's page create — and n8n's HTTP Request node handles all of them without forcing you into pre-built connectors that hide what's actually happening. The Code node for timestamp conversion is genuinely useful here, not just a workaround. The one scenario where you'd skip n8n: if nobody on your team will maintain it. n8n requires a working instance with a public URL, and if your self-hosted setup goes down, you lose Slack messages silently with no alert. In that case, use Make — it runs on Make's infrastructure and doesn't require you to manage a server.
Cost math: this workflow runs once per reaction event. Each execution calls 3 external APIs and uses 1 Code node. On n8n Cloud's Starter plan at $20/month, you get 2,500 executions included. If your team pins 50 messages per day, that's 1,500 executions per month — well within the free tier. At 100 messages per day (3,000/month), you exceed the limit by 500 executions, adding roughly $5-10/month depending on your plan. Self-hosted n8n has no execution limits at all — your only cost is the server, typically $5-10/month on a small VPS. Make would cost $9/month for up to 10,000 operations (each run uses ~5 operations), making it slightly cheaper at low volume but with no self-hosted option.
Make does one specific thing better here: its Slack module has a built-in 'Watch Reactions' trigger that requires zero Slack app configuration — no api.slack.com setup, no scope management. You connect Slack in 30 seconds and pick the emoji from a dropdown. Zapier's Slack trigger for reactions is also plug-and-play but it polls every 5 minutes, meaning your Notion pages appear up to 5 minutes late — unacceptable for time-sensitive decisions. Power Automate has no native Slack connector worth using; you'd end up routing through a custom webhook anyway, which puts you back at the same complexity as n8n but with a worse debugging experience. Pipedream's Slack event source is excellent — it handles the OAuth and webhook verification automatically — but Pipedream's free tier caps at 100 workflow executions per day, which most teams hit within a week. n8n wins for teams who've already invested in self-hosting or want zero per-execution pricing at higher volumes.
Three things you'll hit after setup. First: Slack rate limits conversations.history at 50 requests per minute per workspace. If multiple people react to messages simultaneously, you won't hit this in normal use, but if you're testing repeatedly or have a very active channel, you'll see 429 errors. Add a Wait node set to 1 second between the history call and the users.info call to add breathing room. Second: Notion's text property has a 2,000-character limit per rich text block. Long Slack messages — code snippets, meeting notes — get silently truncated if you don't handle this. The Code node in this guide includes the truncation logic, but watch for it in your Notion pages. Third: the reaction_added webhook fires even for emoji reactions on messages the bot can't read — like messages in channels it hasn't joined. The conversations.history call will return not_in_channel with a 200 OK status (Slack wraps errors in successful HTTP responses), and your workflow will fail silently at that step unless you add an IF node checking $json.ok === true after each Slack API call.
Ideas for what to build next
- →Archive Full Threads, Not Just Single Messages — Extend the HTTP Request step to call conversations.replies with the thread_ts value instead of conversations.history. This pulls every reply in the thread and concatenates them into one Notion page, giving you complete context for decisions made across multiple messages.
- →Route Different Emojis to Different Notion Databases — Add a Switch node after the IF filter and branch on the reaction name — 📌 goes to the Decisions Log database, 📚 goes to a Resources database, and 🐛 goes to a Bug Reports database. Each branch gets its own Notion node pointed at a different database ID.
- →Send a Confirmation Reaction Back to Slack — After the Notion page is created successfully, add a final HTTP Request node calling Slack's reactions.add API to post a ✅ reaction on the original message. This confirms to the team member who pinned it that the archival worked without them leaving Slack.
Related guides
How to Archive Slack Messages to Notion with Pipedream
~15 min setup
How to Archive Slack Messages to Notion with Power Automate
~15 min setup
How to Archive Slack Messages to Notion with Zapier
~8 min setup
How to Archive Slack Messages to Notion with Make
~12 min setup
How to Share Notion Meeting Notes to Slack with Pipedream
~15 min setup
How to Share Notion Meeting Notes to Slack with Power Automate
~15 min setup