

How to Log Slack Messages to Copper CRM with Pipedream
When a Slack message is posted in a designated customer channel, Pipedream instantly logs it as an Activity on the matching Copper contact record.
Steps and UI details are based on platform versions at time of writing — check each platform for the latest interface.
Best for
Sales and CS teams using Slack for customer conversations who need every key message captured in Copper without copy-pasting manually
Not ideal for
Teams logging every single Slack message — this workflow works best when triggered by emoji reactions or specific channel naming conventions, not raw message volume
Sync type
real-timeUse case type
syncReal-World Example
A 12-person SaaS company runs dedicated Slack channels named after each customer (e.g. #client-acme, #client-globex). Their CS team tags messages with a 📋 emoji when something important is said — a complaint, a renewal signal, a feature request. Before this workflow, those insights lived only in Slack and were never visible in Copper. Now, each tagged message is logged as a Note activity on the right Copper contact within 30 seconds.
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 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.
Field Mapping
Map these fields between your apps.
| Field | API Name | |
|---|---|---|
| Required | ||
| Activity Details (Note Body) | details | |
| Activity Type ID | activity_type[id] | |
| Parent Type | parent[type] | |
| Parent ID | parent[id] | |
4 optional fields▸ show
| Slack Channel Name | |
| Slack User Display Name | |
| Message Timestamp | |
| Slack Message Permalink |
Step-by-Step Setup
pipedream.com > Workflows > New Workflow
Create a new Pipedream Workflow
Go to pipedream.com and sign in. Click 'New Workflow' in the top-right corner of the dashboard. Give your workflow a name like 'Slack → Copper: Log Customer Messages'. You'll land on the workflow builder canvas with an empty trigger slot at the top.
- 1Click 'New Workflow' in the top-right of the Pipedream dashboard
- 2Type a workflow name: 'Slack → Copper: Log Customer Messages'
- 3Click 'Create' to open the workflow canvas
Workflow Canvas > Add a trigger > Slack > New Reaction Added
Add the Slack trigger: New Reaction Added
Click the 'Add a trigger' block. Search for 'Slack' in the app list and select it. Choose the trigger 'New Reaction Added' — this fires whenever someone adds an emoji reaction to a message. Using a reaction (e.g. 📋) as the trigger gives your team control over exactly which messages get logged to Copper, rather than logging every message in a channel.
- 1Click the grey 'Add a trigger' block
- 2Type 'Slack' in the search box and select the Slack app
- 3Scroll to find 'New Reaction Added' and click it
- 4Click 'Connect Slack' and authenticate with your workspace via OAuth
- 5Set the 'Reaction' field to the emoji your team will use, e.g. 'clipboard' (without colons)
Workflow Canvas > Trigger Step > Generate Test Event
Test the Slack trigger to capture a sample event
Before building further steps, you need a real event payload to map fields against. Click 'Generate Test Event' in the trigger panel. Then go to your Slack workspace and add your chosen reaction to any message in a customer channel. Return to Pipedream — within 15 seconds you should see the event data populate in the trigger output panel on the right side of the screen.
- 1Click 'Generate Test Event' in the Slack trigger panel
- 2Switch to your Slack workspace and react to any message with your designated emoji
- 3Return to Pipedream and wait 10-15 seconds
- 4Click the test event that appears to expand the full JSON payload
Workflow Canvas > + Add a step > Run Node.js code
Fetch the original Slack message text via API
Click '+ Add a step' below the trigger, then select 'Run Node.js code'. This step calls the Slack conversations.history API with the channel ID and timestamp from the trigger to retrieve the actual message text. You'll use the Pipedream $ helper and your connected Slack account's OAuth token to make the authenticated request. Paste the code from the pro_tip_code section into this code step.
- 1Click '+ Add a step' below the Slack trigger
- 2Select 'Run Node.js code' from the step type list
- 3Rename the step to 'fetch_message' by clicking the step title
- 4Paste the Node.js code into the code editor
- 5Click 'Test' to run the step against your sample event
Workflow Canvas > + Add a step > Run Node.js code
Identify the Copper contact by email or name
Add another Node.js code step. This step calls the Copper People Search API to find the contact that matches the Slack channel name or a known email. The simplest approach: parse the channel name (e.g. #client-acme) to extract 'acme', then query Copper's /v1/people/search endpoint with that term. If your Slack channel names don't map to company names, you'll need a lookup table or to use a Slack user's email instead.
- 1Click '+ Add a step' below the fetch_message step
- 2Select 'Run Node.js code'
- 3Rename the step to 'find_copper_contact'
- 4Paste your contact lookup code referencing steps.trigger.event.item.channel
- 5Click 'Test' and verify the output includes a valid Copper contact ID
Workflow Canvas > + Add a step > Run Node.js code
Fetch the Slack user's display name
The reaction event gives you a Slack user ID (e.g. U04XYZABC), not a human name. Add a Node.js step that calls the Slack users.info API to resolve this to a real name. This matters because the Copper activity log will show who logged the message — 'sarah.jones' is more useful than 'U04XYZABC' when a manager reviews the contact history.
- 1Click '+ Add a step' below find_copper_contact
- 2Select 'Run Node.js code'
- 3Rename the step to 'resolve_slack_user'
- 4Call the Slack users.info API using the event.user field from the trigger
- 5Click 'Test' and confirm the output includes the user's real_name or display_name
Workflow Canvas > + Add a step > Run Node.js code
Create a Copper Activity (Note) via API
Add a final Node.js code step that POSTs to Copper's /v1/activities endpoint. Set the parent type to 'person', the parent ID to the contact ID found in step 5, and the details field to a formatted string combining the message text, the Slack user's name, the channel name, and the message timestamp. Copper's activity API uses an activity_type_id — the default 'Note' type ID is typically 0, but you should verify this in your Copper account under Settings > Activity Types.
- 1Click '+ Add a step' below resolve_slack_user
- 2Select 'Run Node.js code'
- 3Rename the step to 'create_copper_activity'
- 4Reference the contact ID from steps.find_copper_contact.$return_value
- 5POST to https://api.copper.com/developer_api/v1/activities with your API key and user email headers
- 6Click 'Test' and confirm the response includes a numeric activity ID
Workflow Canvas > find_copper_contact step > Settings > Error Handling
Add error handling for failed contact lookups
Not every Slack channel will have a matching Copper contact. Add a conditional check after the find_copper_contact step: if the contact ID is null or empty, route to a fallback step that sends a Slack DM to the user who added the reaction, telling them the contact wasn't found in Copper. This prevents silent failures and keeps your team informed. In Pipedream, use a 'Continue workflow on error' setting or an explicit null check in code.
- 1Click the find_copper_contact step to open its settings panel
- 2Scroll to the 'Error Handling' section
- 3Toggle on 'Continue workflow on error'
- 4Add an if/else check at the top of your create_copper_activity step: if no contact ID, call $.flow.exit('No matching Copper contact found')
- 5Optionally add a Slack 'Send a Direct Message' step to alert the user who reacted
Workflow Canvas > Deploy button (top right) > Executions tab
Deploy and test end-to-end
Click 'Deploy' in the top-right corner of the workflow canvas. The workflow is now live and listening for reactions. Go to Slack, find a real customer message in a customer channel, and add your designated reaction emoji. Watch the Pipedream workflow executions list — you should see a new run appear within 5 seconds. Click into the run to see each step's input/output and confirm all steps passed green.
- 1Click the blue 'Deploy' button in the top-right of the canvas
- 2Switch to your Slack workspace and add the designated emoji reaction to a customer message
- 3Return to Pipedream and click the 'Executions' tab in the left sidebar
- 4Click the most recent execution to open the run detail view
- 5Check each step's output for green status indicators
This single Node.js step handles all four API calls in sequence — fetch the Slack message, resolve the user's name, look up the Copper contact, and create the activity — with proper error handling at each stage. Paste this into a single 'Run Node.js code' step after your Slack trigger, replacing the four separate steps if you want a more compact workflow.
JavaScript — Code Stepimport axios from 'axios';▸ Show code
import axios from 'axios';
export default defineComponent({
async run({ steps, $ }) {... expand to see full code
import axios from 'axios';
export default defineComponent({
async run({ steps, $ }) {
const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN;
const COPPER_API_KEY = process.env.COPPER_API_KEY;
const COPPER_USER_EMAIL = process.env.COPPER_USER_EMAIL;
const CUSTOMER_CHANNEL_PREFIX = 'client-';
const { channel, ts, user } = steps.trigger.event.item;
// 1. Fetch the original message text
const historyRes = await axios.get('https://slack.com/api/conversations.history', {
headers: { Authorization: `Bearer ${SLACK_TOKEN}` },
params: { channel, latest: ts, inclusive: true, limit: 1 },
});
if (!historyRes.data.ok) throw new Error(`Slack history error: ${historyRes.data.error}`);
const message = historyRes.data.messages?.[0];
if (!message) $.flow.exit('Message not found in channel history');
const messageText = message.text;
// 2. Resolve Slack user display name
const userRes = await axios.get('https://slack.com/api/users.info', {
headers: { Authorization: `Bearer ${SLACK_TOKEN}` },
params: { user },
});
if (!userRes.data.ok) throw new Error(`Slack user error: ${userRes.data.error}`);
const displayName = userRes.data.user?.profile?.display_name || userRes.data.user?.real_name || 'Unknown';
// 3. Resolve channel name and guard against non-customer channels
const chanInfoRes = await axios.get('https://slack.com/api/conversations.info', {
headers: { Authorization: `Bearer ${SLACK_TOKEN}` },
params: { channel },
});
if (!chanInfoRes.data.ok) throw new Error(`Slack channel error: ${chanInfoRes.data.error}`);
const channelName = chanInfoRes.data.channel?.name || '';
if (!channelName.startsWith(CUSTOMER_CHANNEL_PREFIX)) {
$.flow.exit(`Skipped: #${channelName} is not a customer channel`);
}
const companySearchTerm = channelName.replace(CUSTOMER_CHANNEL_PREFIX, '').replace(/-/g, ' ');
// 4. Search for matching Copper contact
const copperHeaders = {
'X-PW-AccessToken': COPPER_API_KEY,
'X-PW-UserEmail': COPPER_USER_EMAIL,
'X-PW-Application': 'developer_api',
'Content-Type': 'application/json',
};
const searchRes = await axios.post(
'https://api.copper.com/developer_api/v1/people/search',
{ page_size: 5, name: companySearchTerm },
{ headers: copperHeaders }
);
const contacts = searchRes.data;
if (!contacts || contacts.length === 0) {
$.flow.exit(`No Copper contact found for search term: ${companySearchTerm}`);
}
if (contacts.length > 1) {
console.log(`Multiple contacts matched: ${contacts.map(c => c.name).join(', ')} — using first result`);
}
const contact = contacts[0];
// 5. Build the note body
const messageDate = new Date(parseFloat(ts) * 1000).toISOString().replace('T', ' ').substring(0, 16) + ' UTC';
const permalink = `https://slack.com/archives/${channel}/p${ts.replace('.', '')}`;
const noteBody = [
`[Slack Log] ${messageDate}`,
`Channel: #${channelName}`,
`Logged by: ${displayName}`,
'',
messageText,
'',
permalink,
].join('\n');
// 6. Create Copper Activity
const activityRes = await axios.post(
'https://api.copper.com/developer_api/v1/activities',
{
parent: { id: contact.id, type: 'person' },
type: { category: 'note' },
details: noteBody,
},
{ headers: copperHeaders }
);
return {
copper_activity_id: activityRes.data.id,
contact_name: contact.name,
contact_id: contact.id,
channel_name: channelName,
logged_by: displayName,
message_preview: messageText.substring(0, 100),
};
},
});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 Pipedream for this if your team has even minimal comfort reading Node.js — you don't need to write the code from scratch, but you do need to paste it and understand what the variables mean. Pipedream's webhook processing is genuinely fast here: the Slack reaction event hits Pipedream's endpoint, the four API calls run in under 3 seconds, and the Copper note appears before the reacting user has switched tabs. The other reason to pick Pipedream: you need four separate API calls in sequence (Slack history, Slack users.info, Copper search, Copper create), and chaining those in a no-code tool like Zapier requires four separate steps with fragile field mapping. In Pipedream, it's one code block with real error handling. The one scenario where you'd pick something else: if your entire team is non-technical and you need others to modify the workflow without touching code — use Make instead.
Pipedream's free tier gives you 10,000 workflow executions per month. Each Slack reaction that triggers this workflow costs exactly one execution. At 50 customer conversations logged per day, that's 1,500 executions per month — well inside the free tier. If you scale to 400+ logged messages per day, you'll cross into Pipedream's Basic plan at $19/month. Compare that to Zapier, where this workflow requires a multi-step Zap on the Professional plan at $49/month minimum. Make's free tier covers 1,000 operations per month, but this workflow uses roughly 8 operations per run (each API call counts), so you'd hit the ceiling at 125 reactions/month — faster than you'd expect.
Zapier can connect Slack reactions to Copper using their native app integrations, but Zapier has no native Copper 'Create Activity' action — you'd need to use Zapier's Webhooks step to hit the Copper API directly, which puts you in the same code-adjacent territory as Pipedream but with less flexibility and a higher price. Make handles the sequential API calls well using its HTTP modules and has a cleaner visual flow for non-developers, but Make's error handling for null contact lookups requires conditional routing that gets messy fast. n8n is a strong alternative if you self-host — the Copper and Slack nodes exist, and the workflow builder is visual enough for technical non-coders, plus self-hosting eliminates per-execution cost entirely. Power Automate has no Copper connector at all, making it a non-starter here. Pipedream wins because the code step is the right tool for this specific chain of API calls, and the execution speed on webhooks is the fastest of any platform tested.
Three things you'll hit after setup. First: Copper's People Search is not exact-match. If your Slack channel is #client-acme and Copper has the company listed as 'Acme Inc.' with the contact under a different name, the search returns zero results. Build your channel-to-contact-ID lookup table in Pipedream's built-in Data Store early, not after you've had three logging failures. Second: Slack's conversations.history API has a rate limit of 50 requests per minute per workspace. If your team all simultaneously reacts to a burst of messages, you'll hit 429 errors. Add a try/catch with a 1-second retry delay in your code step. Third: Copper's activity API requires the activity_type category OR the type ID — not both. If you pass both and they're inconsistent, the API returns a 422 with a vague 'unprocessable entity' message. Use category: 'note' and omit the numeric ID, or fetch the exact ID from /v1/activity_types first.
Ideas for what to build next
- →Log Copper to Slack: close the loop — Build a reverse workflow that posts to the customer's Slack channel whenever a Copper deal stage changes — so the team sees CRM updates without leaving Slack.
- →Add a daily digest to Copper — Create a scheduled Pipedream workflow that summarizes all Slack-logged activities from the past 24 hours and posts them as a single batch note on each contact, reducing notification noise.
- →Attach message to Copper Opportunity, not just Contact — Extend the find_copper_contact step to also search for open Opportunities linked to that contact, and log the Slack message against the active deal record for better pipeline visibility.
Related guides
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
How to Share Notion Meeting Notes to Slack with n8n
~20 min setup
How to Send Notion Meeting Notes to Slack with Zapier
~8 min setup
How to Share Notion Meeting Notes to Slack with Make
~12 min setup
How to Create Notion Tasks from Slack with Pipedream
~15 min setup