Intermediate~20 min setupCommunication & CRMVerified April 2026
Slack logo
Copper logo

How to Log Slack Messages to Copper CRM with n8n

Watches a Slack channel for customer messages and writes a timestamped activity note to the matching Copper contact record automatically.

Steps and UI details are based on platform versions at time of writing — check each platform for the latest interface.

Best for

Small sales or CS teams who run customer conversations in Slack and need those interactions visible inside Copper without manual copy-paste.

Not ideal for

Teams logging hundreds of messages per hour — Copper's API rate limit of 1 request/second will cause queuing problems at that volume.

Sync type

real-time

Use case type

sync

Real-World Example

💡

A 12-person SaaS company uses a dedicated #customer-success Slack channel where reps post updates after calls, demos, and check-ins. Before this workflow, those updates lived only in Slack — Copper showed no activity for weeks at a time, making it impossible to prep for renewal conversations. Now every message posted to the channel creates a Copper activity note on the matching contact within 30 seconds.

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 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.

n8n instance accessible via a public URL (Slack's Events API cannot reach localhost)
Slack app with 'message.channels' event subscription and 'users:read.email' OAuth scope enabled
Copper API key — found in Copper under Settings > Integrations > API Keys
Slack bot token (xoxb-...) with 'users:read' and 'users:read.email' scopes
Copper contacts already exist with email addresses that match your Slack users' work emails

Field Mapping

Map these fields between your apps.

FieldAPI Name
Required
Contact IDparent_id
Parent Typeparent_type
Activity Typetype
Activity Detailsdetails
Slack Message Text
Slack User Email
Message Timestamp
2 optional fields▸ show
Channel Name
Sender Display Name

Step-by-Step Setup

1

n8n Dashboard > Workflows > + New Workflow

Create a new n8n workflow

Log into your n8n instance and click the '+ New Workflow' button in the top-right corner of the Workflows dashboard. Give it a clear name like 'Slack → Copper: Customer Message Log'. You'll land on the canvas with an empty trigger node waiting to be configured. This name shows up in execution logs, so make it descriptive.

  1. 1Click '+ New Workflow' in the top-right
  2. 2Click the workflow title field and type 'Slack → Copper: Customer Message Log'
  3. 3Press Enter to save the name
What you should see: You see a blank canvas with a single grey 'Add first step' node in the center.
2

Canvas > Add First Step > Search 'Slack' > Slack Trigger > Message Received

Add the Slack trigger node

Click the grey 'Add first step' node on the canvas. In the search bar that appears, type 'Slack' and select the Slack node from the results. Set the resource to 'Message' and the event to 'Message Received'. This uses Slack's Events API via webhook, which means n8n gets notified the moment a message is posted — no polling delay. You'll need a Slack app with the webhook URL configured in the next step.

  1. 1Click the grey 'Add first step' node
  2. 2Type 'Slack' in the search field
  3. 3Select 'Slack Trigger' from the list
  4. 4Set Event to 'Message Received'
  5. 5Click 'Add Credential' to connect your Slack app
What you should see: The Slack Trigger node opens with a webhook URL displayed in the 'Webhook URL' field — it looks like 'https://your-n8n-instance.com/webhook/abc123'.
Common mistake — The Slack node uses the Events API, not a bot token polling approach. If you paste the webhook URL into the wrong Slack app field (Interactivity & Shortcuts instead of Event Subscriptions), events will never arrive.
n8n
+
click +
search apps
Slack
SL
Slack
Add the Slack trigger node
Slack
SL
module added
3

api.slack.com/apps > [Your App] > Event Subscriptions > Subscribe to Bot Events

Configure the Slack app and subscribe to events

Open api.slack.com/apps, select your Slack app (or create one), and navigate to Event Subscriptions. Toggle 'Enable Events' to On, then paste the n8n webhook URL into the Request URL field. Slack will immediately send a challenge request — n8n auto-responds to verify the URL. Under 'Subscribe to bot events', add the 'message.channels' scope so the app receives messages from public channels. Save the changes and reinstall the app to your workspace.

  1. 1Go to api.slack.com/apps and select your app
  2. 2Click 'Event Subscriptions' in the left sidebar
  3. 3Toggle 'Enable Events' to On
  4. 4Paste the n8n webhook URL into the 'Request URL' field
  5. 5Click '+ Add Bot User Event' and add 'message.channels'
  6. 6Click 'Save Changes', then reinstall the app when prompted
What you should see: The Request URL field shows a green 'Verified' checkmark next to your n8n webhook URL.
Common mistake — If your n8n instance is behind a firewall or running on localhost, Slack cannot reach the webhook URL and verification will fail. Use ngrok or deploy n8n to a public URL before this step.
4

Canvas > + Node > IF > Conditions

Filter messages to the correct Slack channel

Add an IF node after the Slack trigger. You only want to log messages from your dedicated customer channel — not every channel in the workspace. Set the condition to check that the 'channel' field from the Slack output equals the channel ID of your target channel (e.g. C04ABCD1234). Channel IDs are found by right-clicking the channel name in Slack > View channel details > scroll to the bottom. Wire the 'true' branch to the next step; leave the 'false' branch empty.

  1. 1Click the '+' button to the right of the Slack Trigger node
  2. 2Search for and select 'IF'
  3. 3Set Condition 1 Value 1 to '{{ $json.channel }}'
  4. 4Set the operator to 'Equal'
  5. 5Set Value 2 to your channel ID (e.g. C04ABCD1234)
What you should see: The IF node shows two output branches: 'true' on the left and 'false' on the right. Only the 'true' branch will continue processing.
Common mistake — Slack channel IDs look like 'C04ABCD1234' — not the human-readable name like '#customer-success'. Using the channel name here will always evaluate to false and you'll get zero logs.
Slack
SL
trigger
filter
Condition
matches criteria?
yes — passes through
no — skipped
Copper
CO
notified
5

Canvas > + Node > HTTP Request

Extract the sender's email via Slack API

Copper matches contacts by email address. The Slack message payload gives you a user ID (like U05XYZ789), not an email. Add an HTTP Request node to call the Slack users.info API and resolve the user ID to a profile including their email. Set the method to GET, URL to 'https://slack.com/api/users.info', and add a query parameter 'user' set to '{{ $json.user }}'. Add the Authorization header with your Slack bot token as 'Bearer xoxb-...'.

  1. 1Add an HTTP Request node after the IF node's 'true' branch
  2. 2Set Method to 'GET'
  3. 3Set URL to 'https://slack.com/api/users.info'
  4. 4Under Query Parameters, add 'user' = '{{ $json.user }}'
  5. 5Under Headers, add 'Authorization' = 'Bearer xoxb-your-bot-token'
What you should see: Running a test shows the response body containing '{{ $json.user.profile.email }}' with the Slack user's real email address.
Common mistake — Slack only returns an email in users.info if your app has the 'users:read.email' OAuth scope. Without it, the email field is missing and Copper lookup will fail silently.
6

Canvas > + Node > Copper > Person > Search

Look up the matching Copper contact by email

Add a Copper node and set the resource to 'Person' and operation to 'Search'. Use the email resolved in step 5 as the search term: '{{ $json.user.profile.email }}'. Copper's search returns an array — if it returns an empty array, the contact doesn't exist yet and you'll need to decide whether to create a new one or skip. For now, wire a second IF node to check that the result array length is greater than 0.

  1. 1Add a Copper node after the HTTP Request node
  2. 2Set Resource to 'Person'
  3. 3Set Operation to 'Search'
  4. 4Set the 'Email' field to '{{ $json.user.profile.email }}'
  5. 5Click 'Execute Node' to test with a real email
What you should see: The node output shows a JSON array with at least one object containing the Copper contact's 'id', 'name', and 'emails' fields.
7

Canvas > + Node > IF

Guard against missing contacts

Add an IF node after the Copper search. Set the condition to check that '{{ $json.length }}' is greater than 0, or more reliably, check that '{{ $node["Copper"].json[0].id }}' exists. Wire the 'true' branch to the activity creation step. Wire the 'false' branch to a No-Op or an optional Slack notification back to the team that the contact wasn't found. This prevents failed Copper API calls from cluttering your execution log.

  1. 1Add an IF node after the Copper Search node
  2. 2Set Value 1 to '{{ $node["Copper Search"].json[0].id }}'
  3. 3Set operator to 'Is Not Empty'
  4. 4Leave the 'false' branch disconnected or connect a Slack message node to alert the team
What you should see: The IF node routes executions correctly — test with a known email to see the 'true' branch activate and an unknown email to confirm the 'false' branch fires.
Common mistake — If you skip this guard and the Copper search returns an empty array, the next node will throw 'Cannot read property id of undefined' and the execution will fail with no useful error message.
8

Canvas > + Node > Code

Build the activity note text

Add a Code node (JavaScript) to format the Slack message into a clean Copper activity note. This is where you combine the message text, timestamp, and channel context into a single readable string. Copper activity notes are plain text — there's no markdown rendering. Keep the format structured so it's scannable inside Copper's activity feed. Paste the code from the pro tip section below into this node.

  1. 1Click '+' after the IF node's 'true' branch
  2. 2Search for and select 'Code'
  3. 3Set Language to 'JavaScript'
  4. 4Paste the pro tip code into the editor
  5. 5Click 'Execute Node' to preview the formatted output
What you should see: The node output shows a 'noteText' field containing a formatted string like '[Slack | #customer-success | 2024-03-15 14:32 UTC] Jordan Lee: Thanks for the follow-up, we'll sign the contract Friday.'
Common mistake — Slack timestamps are Unix epoch in seconds with a decimal (e.g. '1710509520.123456'). Multiplying by 1000 converts to milliseconds for JavaScript Date — skipping this gives you a date in 1970.
9

Canvas > + Node > Copper > Activity > Create

Create the Copper activity note

Add a second Copper node and set the resource to 'Activity' and the operation to 'Create'. Set the 'Parent Type' to 'person' and 'Parent ID' to '{{ $node["Copper Search"].json[0].id }}'. Set 'Type' to 'note' and 'Details' to the formatted note text from the Code node: '{{ $json.noteText }}'. The activity will appear in the contact's timeline inside Copper immediately after the API call succeeds.

  1. 1Add a Copper node after the Code node
  2. 2Set Resource to 'Activity'
  3. 3Set Operation to 'Create'
  4. 4Set 'Parent Type' to 'person'
  5. 5Set 'Parent ID' to '{{ $node["Copper Search"].json[0].id }}'
  6. 6Set 'Details' to '{{ $json.noteText }}'
What you should see: The node returns a JSON response with an 'id' field — that's the new activity ID in Copper. Open the contact in Copper and you'll see the note in the Activity tab.
10

n8n Canvas > Toggle Workflow Active > Executions (left sidebar)

Test end-to-end with a real Slack message

Activate the workflow using the toggle in the top-right of the canvas. Go to the configured Slack channel and post a test message. Switch back to n8n and open the Executions panel from the left sidebar. You should see a new execution appear within 5-10 seconds. Click it to inspect each node's input and output. Then open Copper, find the contact whose email matches the Slack user, and confirm the activity note appears in their timeline.

  1. 1Click the 'Inactive' toggle in the top-right to activate the workflow
  2. 2Post a test message in the configured Slack channel
  3. 3Click 'Executions' in the left sidebar
  4. 4Click the latest execution to inspect node outputs
  5. 5Open Copper and navigate to the contact's Activity tab to verify the note
What you should see: The execution shows all green nodes with no errors, and the Copper contact has a new activity note timestamped within the last minute.
Common mistake — n8n's execution log only keeps the last 25 executions by default on self-hosted instances. If you're testing frequently, bump the retention limit in Settings > Execution Data before going to production.

This Code node runs between the Copper contact lookup and the Copper activity creation step. It formats the raw Slack data into a clean, timestamped note string and handles the Unix-to-readable timestamp conversion. Paste it into a Code node (JavaScript mode) immediately after the contact guard IF node.

JavaScript — Code Node// n8n Code Node — Format Slack message into Copper activity note
▸ Show code
// n8n Code Node — Format Slack message into Copper activity note
// Place this node between the contact lookup IF and the Copper Activity Create node
const items = $input.all();

... expand to see full code

// n8n Code Node — Format Slack message into Copper activity note
// Place this node between the contact lookup IF and the Copper Activity Create node

const items = $input.all();
const results = [];

for (const item of items) {
  // Slack timestamp is Unix seconds with decimal — convert to milliseconds
  const slackTs = item.json.ts || '';
  const tsMillis = parseFloat(slackTs) * 1000;
  const messageDate = new Date(tsMillis);

  // Format as readable UTC datetime string
  const formattedDate = messageDate.toISOString()
    .replace('T', ' ')
    .substring(0, 16) + ' UTC';

  // Pull sender display name from the upstream users.info HTTP node
  const senderName =
    $node['Get Slack User'].json?.user?.profile?.real_name ||
    $node['Get Slack User'].json?.user?.name ||
    'Unknown Sender';

  // Pull channel name — hardcode or resolve via conversations.info
  const channelLabel = '#customer-success';

  // Raw message text — strip Slack user mention formatting like <@U05XYZ789>
  const rawText = item.json.text || '';
  const cleanText = rawText.replace(/<@[A-Z0-9]+>/g, '').trim();

  // Compose the final note body
  const noteText = `[Slack | ${channelLabel} | ${formattedDate}] ${senderName}: ${cleanText}`;

  results.push({
    json: {
      noteText,
      formattedDate,
      senderName,
      cleanText,
      originalTs: slackTs
    }
  });
}

return results;
n8n
▶ Run once
executed
Slack
Copper
Copper
🔔 notification
received

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 n8n for this if you self-host, want full control over the code that formats notes, and need the users.info API call to resolve Slack IDs to emails without paying per task. The Code node here is the real reason n8n wins: you're doing a Unix timestamp conversion, stripping Slack mention syntax, and composing a formatted string — that's three operations that would cost you three separate steps in Zapier. The one scenario where you'd pick something else: if your team is non-technical and needs to maintain this workflow themselves, Make's visual interface is significantly easier to hand off.

Cost

n8n's self-hosted version costs you server time only — a $6/month DigitalOcean droplet handles this easily. Each Slack message triggers one execution with three API calls: Slack users.info, Copper search, and Copper activity create. At 200 customer messages per month, that's 200 executions and roughly $0 in platform cost. n8n Cloud starts at $20/month for 2,500 executions — at 200 messages/month you're well inside that. Zapier would charge you for each of those three Zap steps separately; at 200 messages that's 600 task uses, which exhausts the free tier (100 tasks) in days and pushes you to the $19.99/month Starter plan fast.

Tradeoffs

Make handles the timestamp formatting and text manipulation in a single module using its built-in text functions — no Code module needed, which makes it slightly easier for non-developers. Zapier has a 'Formatter' step that can do the timestamp conversion, but you'd need a separate Code by Zapier step for the mention-stripping regex, and each step costs a task. Power Automate has native connectors for neither Slack events (you'd use a webhook trigger manually) nor Copper (HTTP actions only), making the setup significantly more painful. Pipedream's code environment is as capable as n8n's and handles the Slack OAuth token management more gracefully, but it's cloud-only, which matters if you have data residency requirements. n8n is still the right call here because the self-hosted option, combined with the Code node, gives you the most control at the lowest recurring cost.

Three things you'll hit after setup. First, Copper's search API does a substring match, not an exact match — searching for '[email protected]' might return contacts with similar emails if your data is messy. Always check the returned array length and validate the email field of result[0] before using the ID. Second, Slack timestamps have microsecond precision and come as strings like '1710509520.123456' — parseFloat() handles this, but if you try parseInt() you'll cut off the decimal and the Date object will be off by a fraction of a second, which causes subtle ordering issues in Copper's activity feed. Third, the Slack Events API will retry delivery up to 3 times if your n8n instance doesn't respond with a 200 within 3 seconds. If your Copper API call is slow (it can hit 800ms), you may get duplicate activity notes. Add a deduplication check using the Slack message 'ts' field stored in a simple n8n variable or a lightweight external key-value store.

Ideas for what to build next

  • Log to Copper Opportunity Instead of ContactIf conversations are deal-specific, modify the Copper lookup to search Opportunities by the contact's associated deals and attach the note there instead. This keeps activity tied to revenue context.
  • Add a Daily Digest SummaryBuild a second n8n workflow on a Schedule trigger that pulls all Copper activities created in the last 24 hours and posts a summary to a Slack channel — gives the team visibility without checking Copper directly.
  • Create Contact if Not Found in CopperWire the 'false' branch of the contact guard IF node to a Copper 'Create Person' node that creates a new contact using the email and display name from Slack, then continues to log the activity on the new record.

Related guides

Was this guide helpful?
Slack + Copper overviewn8n profile →