Intermediate~15 min setupCommunication & ProductivityVerified April 2026
Slack logo
Notion logo

How to Archive Slack Messages to Notion with Pipedream

When a Slack message is starred or reacted to with a specific emoji, Pipedream catches the event via webhook and creates a new Notion page with the message text, author, channel, timestamp, and a link back to the original thread.

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 make decisions in Slack threads and need those decisions searchable in a Notion knowledge base without copy-pasting manually.

Not ideal for

Teams that want to archive every message in a channel — that volume will exhaust credits fast; use a dedicated logging tool like Datadog or a database sink instead.

Sync type

real-time

Use case type

backup

Real-World Example

💡

A 22-person product team at a B2B SaaS company uses a bookmark emoji (🔖) in Slack to flag decisions made during sprint planning threads. Before this workflow, engineers screenshotted threads and pasted them into Notion manually — a process that took 5-10 minutes per thread and happened inconsistently. Now, reacting with 🔖 creates a Notion page in their 'Decisions' database within 15 seconds, with the full thread context and a direct Slack link attached.

What Will This Cost?

Drag the slider to your expected monthly volume.

/mo
505005K50K

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

Skip the setup

Import this workflow directly into Pipedream

Copy the pre-built Pipedream blueprint and paste it straight into Pipedream. 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.

Slack bot token with reactions:read, channels:history, channels:read, users:read, and chat:write OAuth scopes — generate this in your Slack app's OAuth & Permissions settings at api.slack.com/apps
Slack bot must be invited to every channel you want to monitor — run /invite @YourBotName in each channel
Notion integration created at notion.so/my-integrations with read/write content capabilities, and that integration explicitly added to your archive database via the database's Share settings
A Notion database already created with at minimum these properties: Title (title type), Author (text), Slack Link (URL), Channel (text), Archived At (date)
Pipedream account on the free tier or above — the free tier includes 10,000 credits/month which covers roughly 3,300 runs of this 3-API-call workflow

Field Mapping

Map these fields between your apps.

FieldAPI Name
Required
Message Text
Author Display Name
Channel Name
Slack Permalink
Archived At
Message Timestamp
3 optional fields▸ show
Thread Replies
Reaction Emoji
Archived By

Step-by-Step Setup

1

pipedream.com > Workflows > New Workflow

Create a new Pipedream workflow

Go to pipedream.com and sign in. Click 'Workflows' in the left sidebar, then click the blue 'New Workflow' button in the top right. You'll land on the workflow canvas with an empty trigger slot at the top. This is where you'll connect the Slack event source. Give the workflow a name like 'Slack → Notion Archiver' using the pencil icon at the top.

  1. 1Click 'Workflows' in the left sidebar
  2. 2Click the blue 'New Workflow' button
  3. 3Click the pencil icon next to the default workflow name
  4. 4Type 'Slack → Notion Archiver' and press Enter
What you should see: You should see a blank workflow canvas with a single grey 'Add a trigger' block at the top and your new workflow name displayed in the header.
Common mistake — Pipedream auto-saves workflow names on blur — you don't need to hit a save button, but if you navigate away before the name saves, it reverts to the default. Wait for the name to appear in the browser tab before proceeding.
2

Workflow Canvas > Add a trigger > Slack > New Reaction Added

Add the Slack trigger for emoji reactions

Click the 'Add a trigger' block. In the search bar that appears, type 'Slack' and select it. You'll see a list of Slack trigger types — choose 'New Reaction Added'. This trigger fires via Slack's Events API the moment a user reacts to any message in a channel your bot is a member of. You'll filter to a specific emoji in a later step.

  1. 1Click the grey 'Add a trigger' block
  2. 2Type 'Slack' in the app search bar
  3. 3Select 'Slack' from the results
  4. 4Choose 'New Reaction Added' from the trigger list
  5. 5Click 'Connect Slack' and authorize with your workspace admin account
What you should see: Pipedream displays a green 'Connected' badge next to your Slack account and shows a sample event payload in the right panel with fields like reaction, item.channel, item.ts, and event_ts.
Common mistake — The 'New Reaction Added' trigger requires your Slack bot to be invited to each channel you want to monitor. If you don't see events firing, the bot is almost certainly not in that channel — run /invite @YourBotName in Slack first.
Pipedream
+
click +
search apps
Slack
SL
Slack
Add the Slack trigger for em…
Slack
SL
module added
3

Workflow Canvas > + > Built-in > Filter

Filter to your archive emoji only

After the trigger, click the '+' button below the trigger block and choose 'Add a step'. Select 'Filter' (built-in Pipedream step). You'll write a condition that checks whether the reaction name equals your chosen archive emoji — use 'bookmark' for 🔖 or 'white_check_mark' for ✅. Pipedream stops the workflow here if the condition is false, so only messages reacted to with your chosen emoji proceed to Notion.

  1. 1Click the '+' button below the Slack trigger step
  2. 2Click 'Built-in' in the step type selector
  3. 3Select 'Filter'
  4. 4In the condition field, set: steps.trigger.event.reaction === 'bookmark'
  5. 5Click 'Continue' to confirm the filter
What you should see: The Filter step shows a green checkmark and the condition expression you entered. When you test with a sample event using a different emoji, the step shows 'Workflow ended — condition not met' in the test panel.
Common mistake — Slack sends the emoji name without colons and without skin tone modifiers in the reaction field. 'bookmark' works; ':bookmark:' does not. Check the raw trigger payload to confirm the exact string Slack sends for your chosen emoji.
Slack
SL
trigger
filter
Condition
matches criteria?
yes — passes through
no — skipped
Notion
NO
notified
4

Workflow Canvas > + > Node.js Code Step

Fetch the full message text via Slack API

The reaction event payload gives you the channel ID and message timestamp but not the message text itself. Add a Node.js code step to call Slack's conversations.history API and retrieve the actual message content. You'll pass the channel and timestamp from the trigger event. This step also grabs the thread replies if the message is a thread parent, giving you the full conversation context for Notion.

  1. 1Click '+' below the Filter step
  2. 2Select 'Run Node.js code'
  3. 3Paste the fetch code into the code editor (see Pro Tip section)
  4. 4Click 'Test' to confirm the step returns message text in the exports object
What you should see: The test panel shows an exports object containing message_text, author_id, thread_replies (array), channel_id, and permalink fields populated with real data from your Slack workspace.
Common mistake — Map fields using the variable picker — don't type field names manually. Hand-typed variable names often have invisible spacing errors that produce blank output.
5

Workflow Canvas > + > Run Node.js code

Resolve the Slack user ID to a display name

The message payload contains a user ID like U04XYZ123, not a human-readable name. Add another Node.js code step that calls Slack's users.info endpoint with that ID and returns the user's real_name and display_name. Without this step, your Notion pages will show raw user IDs in the Author field, which makes the archive hard to read.

  1. 1Click '+' below the message fetch step
  2. 2Select 'Run Node.js code'
  3. 3Use the Slack OAuth token from your Connected Account to call users.info
  4. 4Export { real_name, display_name } from the step result
What you should see: The step exports object shows real_name: 'Jordan Park' and display_name: 'jordan.park' — human-readable strings ready to drop into a Notion field.
Common mistake — Slack's users.info endpoint is rate-limited to 50 requests per minute on most plans. If you archive messages frequently (multiple per minute), add a $.flow.delay(1200) call before the API request to avoid 429 errors.

This code step handles the three Slack API calls needed before you touch Notion: fetching message text via conversations.history, fetching thread replies if a thread exists, and calling chat.getPermalink — all with proper error handling. Paste this into a single Node.js code step placed between your Filter step and the Notion steps. It uses Pipedream's built-in $ helper and your connected Slack account token via process.env.SLACK_BOT_TOKEN (set this in your workflow's environment variables).

JavaScript — Code Stepimport { axios } from "@pipedream/platform";
▸ Show code
import { axios } from "@pipedream/platform";
export default defineComponent({
  async run({ steps, $ }) {

... expand to see full code

import { axios } from "@pipedream/platform";

export default defineComponent({
  async run({ steps, $ }) {
    const token = process.env.SLACK_BOT_TOKEN;
    const channel = steps.trigger.event.item.channel;
    const messageTs = steps.trigger.event.item.ts;
    const reactingUserId = steps.trigger.event.user;

    // 1. Fetch the original message text
    const historyResp = await axios($, {
      method: "GET",
      url: "https://slack.com/api/conversations.history",
      params: {
        channel,
        latest: messageTs,
        inclusive: true,
        limit: 1,
      },
      headers: { Authorization: `Bearer ${token}` },
    });

    if (!historyResp.ok) {
      throw new Error(`conversations.history failed: ${historyResp.error}`);
    }

    const message = historyResp.messages?.[0];
    if (!message) throw new Error("Message not found in channel history");

    const rawText = message.text || "[No text content]";
    const authorId = message.user || message.bot_id || "unknown";

    // Sanitize Slack mrkdwn for Notion compatibility
    const sanitizedText = rawText
      .replace(/<@([A-Z0-9]+)>/g, "@user")
      .replace(/<#([A-Z0-9]+)\|([^>]+)>/g, "#$2")
      .replace(/<([^|>]+)\|([^>]+)>/g, "$2")
      .replace(/<([^>]+)>/g, "$1");

    // 2. Fetch thread replies if this message has a thread
    let threadReplies = [];
    if (message.thread_ts && message.reply_count > 0) {
      const repliesResp = await axios($, {
        method: "GET",
        url: "https://slack.com/api/conversations.replies",
        params: { channel, ts: message.thread_ts, limit: 100 },
        headers: { Authorization: `Bearer ${token}` },
      });
      if (repliesResp.ok) {
        // Skip the first item — it's the parent message repeated
        threadReplies = repliesResp.messages
          .slice(1)
          .map((r) => ({
            text: r.text?.replace(/<[^>]+>/g, "") || "",
            user: r.user || r.bot_id || "unknown",
            ts: r.ts,
          }));
      }
    }

    // 3. Fetch permalink
    const permalinkResp = await axios($, {
      method: "GET",
      url: "https://slack.com/api/chat.getPermalink",
      params: { channel, message_ts: messageTs },
      headers: { Authorization: `Bearer ${token}` },
    });

    if (!permalinkResp.ok) {
      throw new Error(`chat.getPermalink failed: ${permalinkResp.error}`);
    }

    // 4. Resolve author name
    let authorName = authorId;
    if (authorId.startsWith("U")) {
      const userResp = await axios($, {
        method: "GET",
        url: "https://slack.com/api/users.info",
        params: { user: authorId },
        headers: { Authorization: `Bearer ${token}` },
      });
      if (userResp.ok) {
        authorName = userResp.user.profile.display_name || userResp.user.real_name;
      }
    } else if (authorId.startsWith("B")) {
      const botResp = await axios($, {
        method: "GET",
        url: "https://slack.com/api/bots.info",
        params: { bot: authorId },
        headers: { Authorization: `Bearer ${token}` },
      });
      if (botResp.ok) authorName = botResp.bot.name + " (bot)";
    }

    // 5. Resolve archiving user name
    let archivedByName = reactingUserId;
    const archiverResp = await axios($, {
      method: "GET",
      url: "https://slack.com/api/users.info",
      params: { user: reactingUserId },
      headers: { Authorization: `Bearer ${token}` },
    });
    if (archiverResp.ok) {
      archivedByName = archiverResp.user.profile.display_name || archiverResp.user.real_name;
    }

    return {
      message_text: sanitizedText,
      message_text_truncated: sanitizedText.slice(0, 100),
      author_id: authorId,
      author_name: authorName,
      archived_by: archivedByName,
      channel_id: channel,
      message_ts: messageTs,
      permalink: permalinkResp.permalink,
      thread_replies: threadReplies,
      archived_at: new Date(parseFloat(steps.trigger.event.event_ts) * 1000).toISOString(),
    };
  },
});
6

Workflow Canvas > + > Run Node.js code

Fetch the Slack permalink for the message

Call Slack's chat.getPermalink endpoint using the channel ID and message timestamp from the trigger. This gives you a permanent URL that deep-links directly to the original message in Slack — critical for the Notion page so readers can jump back to context. Store the permalink in this step's exports so the Notion step can reference it as steps.get_permalink.permalink.

  1. 1Click '+' below the user resolution step
  2. 2Select 'Run Node.js code'
  3. 3Call chat.getPermalink with channel and message_ts from the trigger event
  4. 4Export { permalink } from the step
What you should see: The exports object contains a permalink field with a URL in the format https://yourworkspace.slack.com/archives/C0XXXXXX/pXXXXXXXXXXXXXX.
Common mistake — chat.getPermalink uses the message timestamp as a float string (e.g. '1700000000.123456'). The trigger gives you item.ts in this format already — don't convert it to a Date object or it will break the API call.
7

Workflow Canvas > + > Notion > Create Page

Connect your Notion account

Click '+' below the permalink step and search for 'Notion'. Select the 'Create Page' action. Click 'Connect Notion' and complete the OAuth flow — Notion will ask which pages and databases your integration can access. You must explicitly grant access to the specific database you want to archive into. If you skip granting access to the right database, Notion's API returns a 404 even though the database exists.

  1. 1Click '+' and search for 'Notion'
  2. 2Select 'Notion' from the app list
  3. 3Choose 'Create Page' as the action
  4. 4Click 'Connect Notion account'
  5. 5In the Notion OAuth dialog, check the box next to your 'Archived Decisions' database before clicking Allow
What you should see: Pipedream shows a green 'Connected' badge for Notion and the action configuration panel displays a 'Parent Database ID' field ready for input.
Common mistake — Notion's OAuth scope is set at connection time. If you later create a new database you want to archive into, you must reconnect the Notion account in Pipedream's Connected Accounts and re-grant access — there is no way to add database access without re-authorizing.
Pipedream settings
Connection
Choose a connection…Add
click Add
Slack
Log in to authorize
Authorize Pipedream
popup window
Connected
green checkmark
8

Workflow Canvas > Notion Create Page > Configure

Configure the Notion page fields

In the 'Create Page' action, set the Parent Database ID to your archive database's ID (found in the Notion URL after the workspace name and before the '?v=' parameter). Map each Notion database property to the corresponding Pipedream step export. Set the page title to the first 100 characters of the message text, the Author property to the resolved display name, Channel to the channel name, and the Slack Link URL property to the permalink.

  1. 1Paste your Notion database ID into the 'Parent Database ID' field
  2. 2Set 'Title' to: {{steps.fetch_message.message_text.slice(0, 100)}}
  3. 3Set 'Author' property to: {{steps.resolve_user.real_name}}
  4. 4Set 'Slack Link' URL property to: {{steps.get_permalink.permalink}}
  5. 5Set 'Archived At' date property to: {{new Date(steps.trigger.event.event_ts * 1000).toISOString()}}
What you should see: Each field in the Notion action panel shows a dynamic reference (the double-curly expression) rather than a static string, and the step validates without red error borders on any required field.
Slack fields
text
user
channel
ts
thread_ts
available as variables:
1.props.text
1.props.user
1.props.channel
1.props.ts
1.props.thread_ts
9

Workflow Canvas > + > Notion > Append Block Children

Add the full message as Notion page body

The Notion 'Create Page' action creates the page record but leaves the body empty. Add a second Notion step — 'Append Block Children' — immediately after. Use the page ID output from the previous step (steps.create_page.$return_value.id) as the Block ID. Add a paragraph block containing the full message text and, if thread replies exist, append each reply as a separate quote block. This makes the Notion page readable without having to go back to Slack.

  1. 1Click '+' below the Create Page step
  2. 2Search for 'Notion' and select 'Append Block Children'
  3. 3Set Block ID to: {{steps.create_notion_page.$return_value.id}}
  4. 4Add a paragraph block with content: {{steps.fetch_message.message_text}}
  5. 5Add a loop over steps.fetch_message.thread_replies to append each reply as a quote block
What you should see: After testing, opening the newly created Notion page shows the full message text in the page body, with thread replies indented beneath as quote blocks.
Common mistake — Notion's Append Block Children endpoint accepts a maximum of 100 blocks per request. Threads longer than 100 replies will be silently truncated. For threads that long, split the replies array into chunks of 100 and call the endpoint multiple times.
message template
🔔 New Record: {{text}} {{user}}
channel: {{channel}}
ts: {{ts}}
#sales
🔔 New Record: Jane Smith
Company: Acme Corp
10

pipedream.com > Workflows > [Your Workflow] > Event History

Test end-to-end with a real Slack message

In Slack, go to a channel your bot is in and react to any message with your archive emoji. Return to Pipedream and click the workflow's 'Event History' tab — you should see a new execution appear within 5-10 seconds. Click the execution to expand each step and confirm green checkmarks on the Filter, all code steps, and both Notion steps. Then open Notion and verify the page was created in the correct database with all fields populated.

  1. 1React to a Slack message with your archive emoji (e.g. 🔖)
  2. 2Click the 'Event History' tab in your Pipedream workflow
  3. 3Click the newest execution entry to expand the step-by-step results
  4. 4Confirm all steps show green checkmarks
  5. 5Open Notion and verify the new page exists in your archive database
What you should see: Notion shows a new page titled with the first 100 characters of the archived message, the Author field contains the user's real name, and clicking the Slack Link URL opens the original message in Slack.
Common mistake — Map fields using the variable picker — don't type field names manually. Hand-typed variable names often have invisible spacing errors that produce blank output.
Pipedream
▶ Deploy & test
executed
Slack
Notion
Notion
🔔 notification
received
11

Workflow Canvas > Deploy > Settings > Notifications

Deploy and set error alerting

Click the grey 'Deploy' button in the top right to activate the workflow. Pipedream will now process every matching Slack reaction event in production. Go to Settings > Notifications inside the workflow and enable email alerts on workflow errors — this way you'll know immediately if a Slack API rate limit or Notion auth failure causes a run to fail silently. Set the notification threshold to 1 error (not the default 10) for a workflow this small.

  1. 1Click the 'Deploy' button in the top right corner
  2. 2Confirm the deployment dialog
  3. 3Click 'Settings' in the left panel of the workflow
  4. 4Toggle on 'Email on error'
  5. 5Set error threshold to 1 and save
What you should see: The workflow status badge changes from grey 'Inactive' to green 'Active', and the Notifications section shows your email address with a threshold of 1 error per run.

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

VerdictWhy n8n for this workflow

Use Pipedream for this if your team has any technical comfort at all. The webhook from Slack lands in under 2 seconds, and the Node.js code step means you can handle the four API calls this workflow needs (conversations.history, users.info, chat.getPermalink, Notion create) in a single step with real error handling — not four separate no-code blocks with no visibility into what failed. The other scenario where Pipedream wins hard here: you want to parse Slack's mrkdwn syntax before it hits Notion. That text sanitization is three lines of regex in Node.js. In Zapier, it's a Formatter step that doesn't handle all the edge cases. Pick a different platform only if your whole team is non-technical and nobody will maintain the code step — in that case Make is more accessible.

Cost

On the free tier, Pipedream gives you 10,000 credits per month. This workflow uses 3 credits per run (roughly one credit per outbound API call). At 3,333 archives per month you hit the ceiling — that's about 110 archived messages per day. Most teams land well under that. If you're over it, Pipedream's Basic plan is $29/month for 100,000 credits, which supports 33,000 archives/month. Zapier's comparable plan for real-time triggers starts at $49/month and caps at 2,000 tasks — which at one task per Zap step means this 4-step workflow consumes 4 tasks per run, capping you at 500 archives/month. Pipedream is cheaper and higher-volume by a significant margin here.

Tradeoffs

Zapier has a cleaner UI for connecting Slack and Notion without writing code — if you can live with its Notion integration's limited block type support (it won't append quote blocks for thread replies, only plain paragraphs). Make handles the message text transformation better than Zapier using its text parsing modules, and its Notion module has slightly richer block type options. n8n gives you the same Node.js flexibility as Pipedream but requires self-hosting or their cloud plan — for a workflow this simple, that overhead isn't worth it unless you're already running n8n. Power Automate has no native Slack trigger worth using; you'd need a webhook and a custom connector, which is more work than just using Pipedream. Pipedream is still the right call because the Slack webhook delivery is instant, the code step handles the mrkdwn sanitization cleanly, and the connected accounts system means you don't manage OAuth tokens manually.

Three things you'll run into after the first week: First, Slack's reaction_added event fires even when someone removes and re-adds their reaction — you'll see duplicate archives from the same person on the same message if they toggle the emoji. Build the deduplication query into your workflow before you think you need it. Second, the Notion API's rate limit is 3 requests per second per integration. This workflow makes two Notion calls (create page + append blocks) in quick succession — under normal load it's fine, but if multiple people are archiving messages simultaneously you'll hit 429 errors. Add a 400ms delay between the two Notion steps. Third, Slack's conversations.history only returns messages from public channels and private channels where the bot is invited — it will not return messages from channels where the bot was added after the message was posted, even if the message timestamp is recent. This burns people who add the bot to an existing channel and expect to retroactively archive old messages by reacting to them.

Ideas for what to build next

  • Tag Notion pages by reaction typeUse different emojis (🔖 for decisions, ✅ for action items, 💡 for ideas) and map each to a different Notion database or a 'Category' select property, so archived messages are automatically sorted by type.
  • Add a daily digest notificationBuild a second Pipedream workflow on a scheduled trigger that queries your Notion archive database for pages created in the last 24 hours and posts a summary back to a Slack channel — closes the loop so the team sees what was archived.
  • Attach file previews from Slack messagesExtend the message fetch step to check for files[] in the Slack message payload, download the file using the Slack Files API, and upload it to Notion as an attachment on the archived page — useful for capturing screenshots and diagrams posted alongside decisions.

Related guides

Was this guide helpful?
Slack + Notion overviewPipedream profile →