

How to Turn Slack Messages into Todoist Tasks with n8n
When a Slack message is reacted to with a specific emoji or contains a trigger keyword, n8n creates a Todoist task with the message content, sender, and channel as structured task data.
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 surface action items in Slack discussions and need them captured in Todoist without manual copy-paste.
Not ideal for
Teams that need two-way sync — if tasks need to update back in Slack when completed, use a dedicated Slack-Todoist integration app instead.
Sync type
real-timeUse case type
routingReal-World Example
A 12-person product team uses a white checkmark emoji reaction on Slack messages to flag action items during sprint planning calls. Before this workflow, someone had to manually copy each action item into Todoist after the meeting — things were regularly missed or duplicated. Now, reacting with ✅ on any message creates a Todoist task within 10 seconds, assigned to the inbox with the original message text and a link back to the Slack thread.
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.
Optional
Field Mapping
Map these fields between your apps.
| Field | API Name | |
|---|---|---|
| Required | ||
| Task Content | content | |
7 optional fields▸ show
| Project ID | project_id |
| Due String | due_string |
| Priority | priority |
| Description | description |
| Labels | label_ids |
| Assignee ID | assignee_id |
| Section ID | section_id |
Step-by-Step Setup
api.slack.com/apps > Create New App > From Scratch
Create a Slack App and Enable Event Subscriptions
Go to api.slack.com/apps and click 'Create New App'. Choose 'From scratch', name it something like 'n8n Task Bot', and select your workspace. You need your own Slack app because n8n's built-in Slack OAuth credentials don't include the reaction_added event scope by default. This step gives you a bot token with the exact scopes this workflow needs.
- 1Go to api.slack.com/apps and click the green 'Create New App' button
- 2Select 'From scratch' in the modal
- 3Enter a name like 'n8n Task Bot' and select your Slack workspace
- 4Click 'Create App'
Paste this into the first Code node (after the Webhook trigger) in n8n. It filters by emoji, extracts the Slack permalink format, strips user mention syntax, and attempts to parse a due date from natural language keywords in the message text — all before the HTTP Request and Todoist nodes run.
JavaScript — Code Node// n8n Code Node — Filter emoji reaction and extract task data▸ Show code
// n8n Code Node — Filter emoji reaction and extract task data // Place this as the first Code node after the Webhook trigger const TRIGGER_EMOJI = 'white_check_mark'; // Change to your chosen emoji name
... expand to see full code
// n8n Code Node — Filter emoji reaction and extract task data
// Place this as the first Code node after the Webhook trigger
const TRIGGER_EMOJI = 'white_check_mark'; // Change to your chosen emoji name
const SLACK_WORKSPACE = 'yourteam'; // Replace with your Slack workspace subdomain
const body = $input.first().json.body;
const event = body?.event;
// Stop processing if this isn't the right emoji
if (!event || event.type !== 'reaction_added' || event.reaction !== TRIGGER_EMOJI) {
return []; // Return empty array — n8n stops execution silently
}
const channelId = event.item?.channel;
const messageTs = event.item?.ts;
const reactingUser = event.user;
if (!channelId || !messageTs) {
throw new Error(`Missing channel or timestamp in Slack event: ${JSON.stringify(event)}`);
}
// Build the Slack permalink from channel ID and timestamp
// Format: archives/CHANNEL/pTIMESTAMP (remove the dot from ts)
const tsFormatted = messageTs.replace('.', '');
const slackLink = `https://${SLACK_WORKSPACE}.slack.com/archives/${channelId}/p${tsFormatted}`;
// Try to extract a due date keyword from the message (will be refined in step 7)
const dueDateKeywords = ['today', 'tomorrow', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'eod', 'next week'];
return [{
json: {
channel: channelId,
ts: messageTs,
reacting_user: reactingUser,
slack_link: slackLink,
emoji: event.reaction,
due_keywords: dueDateKeywords // passed to formatting node for text matching
}
}];Slack App Dashboard > OAuth & Permissions > Bot Token Scopes
Add Required Bot Token Scopes
In your Slack app dashboard, navigate to OAuth & Permissions in the left sidebar. Scroll to the 'Scopes' section and add bot token scopes. You need reactions:read to detect emoji reactions, channels:history to read message content, and channels:read to identify channel names. Without reactions:read, the event subscription won't fire at all.
- 1Click 'OAuth & Permissions' in the left sidebar
- 2Scroll down to the 'Bot Token Scopes' section
- 3Click 'Add an OAuth Scope'
- 4Add reactions:read, channels:history, and channels:read one at a time
- 5Click 'Install to Workspace' at the top of the page and authorize
n8n > New Workflow > + Node > Webhook
Create an n8n Webhook Node as the Trigger
Open your n8n instance and create a new workflow. Add a Webhook node as the first node — this is what Slack will POST events to. Set the HTTP Method to POST and note the webhook URL n8n generates. You'll paste this URL into Slack's Event Subscriptions page in the next step. Keep the path as the default random UUID or set a memorable path like /slack-task-trigger.
- 1Click the '+' button on the canvas to open the node picker
- 2Search for 'Webhook' and select the Webhook trigger node
- 3Set HTTP Method to POST
- 4Copy the 'Test URL' shown — you'll use this during setup, then switch to Production URL before go-live
Slack App Dashboard > Event Subscriptions > Enable Events
Register the Webhook URL in Slack Event Subscriptions
Back in the Slack API dashboard, click 'Event Subscriptions' in the left sidebar and toggle 'Enable Events' to On. Paste your n8n webhook URL into the Request URL field. Slack will immediately send a challenge request — n8n's Webhook node handles this automatically by echoing back the challenge parameter. Once verified, you'll see a green 'Verified' checkmark.
- 1Click 'Event Subscriptions' in the left sidebar
- 2Toggle 'Enable Events' to On
- 3Paste your n8n webhook URL into the 'Request URL' field
- 4Wait for Slack to show 'Verified' next to the URL
- 5Under 'Subscribe to bot events', click 'Add Bot User Event' and select reaction_added
- 6Click 'Save Changes' at the bottom of the page
Paste this into the first Code node (after the Webhook trigger) in n8n. It filters by emoji, extracts the Slack permalink format, strips user mention syntax, and attempts to parse a due date from natural language keywords in the message text — all before the HTTP Request and Todoist nodes run.
JavaScript — Code Node// n8n Code Node — Filter emoji reaction and extract task data▸ Show code
// n8n Code Node — Filter emoji reaction and extract task data // Place this as the first Code node after the Webhook trigger const TRIGGER_EMOJI = 'white_check_mark'; // Change to your chosen emoji name
... expand to see full code
// n8n Code Node — Filter emoji reaction and extract task data
// Place this as the first Code node after the Webhook trigger
const TRIGGER_EMOJI = 'white_check_mark'; // Change to your chosen emoji name
const SLACK_WORKSPACE = 'yourteam'; // Replace with your Slack workspace subdomain
const body = $input.first().json.body;
const event = body?.event;
// Stop processing if this isn't the right emoji
if (!event || event.type !== 'reaction_added' || event.reaction !== TRIGGER_EMOJI) {
return []; // Return empty array — n8n stops execution silently
}
const channelId = event.item?.channel;
const messageTs = event.item?.ts;
const reactingUser = event.user;
if (!channelId || !messageTs) {
throw new Error(`Missing channel or timestamp in Slack event: ${JSON.stringify(event)}`);
}
// Build the Slack permalink from channel ID and timestamp
// Format: archives/CHANNEL/pTIMESTAMP (remove the dot from ts)
const tsFormatted = messageTs.replace('.', '');
const slackLink = `https://${SLACK_WORKSPACE}.slack.com/archives/${channelId}/p${tsFormatted}`;
// Try to extract a due date keyword from the message (will be refined in step 7)
const dueDateKeywords = ['today', 'tomorrow', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'eod', 'next week'];
return [{
json: {
channel: channelId,
ts: messageTs,
reacting_user: reactingUser,
slack_link: slackLink,
emoji: event.reaction,
due_keywords: dueDateKeywords // passed to formatting node for text matching
}
}];n8n Canvas > + Node > Code
Add an n8n Code Node to Filter by Emoji and Extract Message Text
Connect a Code node after the Webhook node. This node does two things: it filters for only your chosen trigger emoji (e.g. ✅) and it extracts the message text from the Slack event payload. The reaction_added event includes the emoji name, the channel ID, the message timestamp, and the user who reacted — but not the message text itself. You'll fetch that in the next step using the timestamp.
- 1Click the '+' connector after the Webhook node
- 2Search for 'Code' and select the Code node
- 3Set the language to JavaScript
- 4Paste the filtering and extraction logic into the editor (see pro tip code below)
- 5Click 'Execute Node' and verify the output shows your filtered event data
Paste this into the first Code node (after the Webhook trigger) in n8n. It filters by emoji, extracts the Slack permalink format, strips user mention syntax, and attempts to parse a due date from natural language keywords in the message text — all before the HTTP Request and Todoist nodes run.
JavaScript — Code Node// n8n Code Node — Filter emoji reaction and extract task data▸ Show code
// n8n Code Node — Filter emoji reaction and extract task data // Place this as the first Code node after the Webhook trigger const TRIGGER_EMOJI = 'white_check_mark'; // Change to your chosen emoji name
... expand to see full code
// n8n Code Node — Filter emoji reaction and extract task data
// Place this as the first Code node after the Webhook trigger
const TRIGGER_EMOJI = 'white_check_mark'; // Change to your chosen emoji name
const SLACK_WORKSPACE = 'yourteam'; // Replace with your Slack workspace subdomain
const body = $input.first().json.body;
const event = body?.event;
// Stop processing if this isn't the right emoji
if (!event || event.type !== 'reaction_added' || event.reaction !== TRIGGER_EMOJI) {
return []; // Return empty array — n8n stops execution silently
}
const channelId = event.item?.channel;
const messageTs = event.item?.ts;
const reactingUser = event.user;
if (!channelId || !messageTs) {
throw new Error(`Missing channel or timestamp in Slack event: ${JSON.stringify(event)}`);
}
// Build the Slack permalink from channel ID and timestamp
// Format: archives/CHANNEL/pTIMESTAMP (remove the dot from ts)
const tsFormatted = messageTs.replace('.', '');
const slackLink = `https://${SLACK_WORKSPACE}.slack.com/archives/${channelId}/p${tsFormatted}`;
// Try to extract a due date keyword from the message (will be refined in step 7)
const dueDateKeywords = ['today', 'tomorrow', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'eod', 'next week'];
return [{
json: {
channel: channelId,
ts: messageTs,
reacting_user: reactingUser,
slack_link: slackLink,
emoji: event.reaction,
due_keywords: dueDateKeywords // passed to formatting node for text matching
}
}];n8n Canvas > + Node > HTTP Request
Fetch the Original Slack Message Text
Add an HTTP Request node to call Slack's conversations.history API and retrieve the actual message text using the channel ID and message timestamp from the previous step. The reaction_added event payload only gives you the timestamp — not the text. You need a GET request to https://slack.com/api/conversations.history with the channel, latest, limit, and inclusive parameters set. Pass your Bot Token as a Bearer token in the Authorization header.
- 1Add an HTTP Request node after the Code node
- 2Set Method to GET
- 3Set URL to https://slack.com/api/conversations.history
- 4Under 'Query Parameters', add: channel = {{ $json.channel }}, latest = {{ $json.ts }}, limit = 1, inclusive = true
- 5Under 'Authentication', select 'Header Auth' and set Authorization to Bearer xoxb-your-bot-token
n8n Canvas > + Node > Code
Add a Second Code Node to Format the Task Data
Connect another Code node to clean up the message text and build the Todoist task payload. This is where you strip Slack's user mention syntax (e.g. <@U012AB3CD>) and channel mention syntax into readable text. You also construct the task content string and set a due date if the message contains a date keyword. This node outputs a clean object ready for the Todoist node.
- 1Add a second Code node after the HTTP Request node
- 2Paste the text cleaning and payload formatting logic
- 3Add a field for task_content combining the message text with a Slack link
- 4Add a field for due_string if the text contains keywords like 'tomorrow' or 'Friday'
- 5Click 'Execute Node' and confirm task_content looks clean and readable
Paste this into the first Code node (after the Webhook trigger) in n8n. It filters by emoji, extracts the Slack permalink format, strips user mention syntax, and attempts to parse a due date from natural language keywords in the message text — all before the HTTP Request and Todoist nodes run.
JavaScript — Code Node// n8n Code Node — Filter emoji reaction and extract task data▸ Show code
// n8n Code Node — Filter emoji reaction and extract task data // Place this as the first Code node after the Webhook trigger const TRIGGER_EMOJI = 'white_check_mark'; // Change to your chosen emoji name
... expand to see full code
// n8n Code Node — Filter emoji reaction and extract task data
// Place this as the first Code node after the Webhook trigger
const TRIGGER_EMOJI = 'white_check_mark'; // Change to your chosen emoji name
const SLACK_WORKSPACE = 'yourteam'; // Replace with your Slack workspace subdomain
const body = $input.first().json.body;
const event = body?.event;
// Stop processing if this isn't the right emoji
if (!event || event.type !== 'reaction_added' || event.reaction !== TRIGGER_EMOJI) {
return []; // Return empty array — n8n stops execution silently
}
const channelId = event.item?.channel;
const messageTs = event.item?.ts;
const reactingUser = event.user;
if (!channelId || !messageTs) {
throw new Error(`Missing channel or timestamp in Slack event: ${JSON.stringify(event)}`);
}
// Build the Slack permalink from channel ID and timestamp
// Format: archives/CHANNEL/pTIMESTAMP (remove the dot from ts)
const tsFormatted = messageTs.replace('.', '');
const slackLink = `https://${SLACK_WORKSPACE}.slack.com/archives/${channelId}/p${tsFormatted}`;
// Try to extract a due date keyword from the message (will be refined in step 7)
const dueDateKeywords = ['today', 'tomorrow', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'eod', 'next week'];
return [{
json: {
channel: channelId,
ts: messageTs,
reacting_user: reactingUser,
slack_link: slackLink,
emoji: event.reaction,
due_keywords: dueDateKeywords // passed to formatting node for text matching
}
}];n8n Canvas > Todoist Node > Credentials > Create New
Connect Todoist Credentials in n8n
Add a Todoist node and configure authentication. In n8n, click 'Credential' in the Todoist node and select 'Create New'. You'll need a Todoist API token — find it at todoist.com/prefs/integrations under 'API token'. Paste the token into n8n and click Save. This token gives n8n full access to create tasks in your account, so use a dedicated Todoist account or a bot user account if this is a shared team setup.
- 1Add a Todoist node after the second Code node
- 2Click the 'Credential' dropdown and select 'Create New Credential'
- 3Go to todoist.com/prefs/integrations and copy your API token
- 4Paste the token into the 'API Key' field in n8n
- 5Click 'Save' and confirm the credential name appears in the dropdown
n8n Canvas > Todoist Node > Operation: Create Task
Configure the Todoist Task Creation Node
In the Todoist node, set the Operation to 'Create Task'. Map task_content from the previous Code node to the Content field. Set the Project to your target project (use the dropdown to pick it — n8n fetches your project list automatically). Optionally map due_string to the Due String field so Todoist parses natural language dates. Set Priority to 2 (high) for emoji-triggered tasks since they're already pre-filtered as important.
- 1Set 'Operation' to 'Create Task'
- 2Map 'Content' to {{ $json.task_content }} from the Code node
- 3Select a Project from the dropdown or use {{ $json.project_id }} for dynamic routing
- 4Set 'Due String' to {{ $json.due_string }} (leave blank if null)
- 5Set 'Priority' to 2
n8n Canvas > Todoist Node > Error Output > HTTP Request
Add an Error Handling Node for Failed Task Creation
Add a second output branch from the Todoist node to handle failures. Connect an HTTP Request node that posts a Slack message back to the original channel when task creation fails. Without this, failed creations are invisible — the emoji stays on the message but no task was made. Use Slack's chat.postMessage API to send a brief error notice to the channel where the reaction happened.
- 1Click the Todoist node to select it
- 2Drag from the red error output connector (bottom of node) to a new HTTP Request node
- 3Set the URL to https://slack.com/api/chat.postMessage
- 4Set the body to include channel: {{ $node['Webhook'].json.body.event.item.channel }} and text: '⚠️ Task creation failed for this message. Please add it to Todoist manually.'
- 5Set Authorization header to Bearer with your bot token
n8n Canvas > Webhook Node > Production URL | Slack App Dashboard > Event Subscriptions
Switch from Test Webhook to Production and Activate
Open the Webhook node and copy the Production URL (not the Test URL). Go back to the Slack API dashboard, navigate to Event Subscriptions, and replace the existing URL with the Production URL. Then click 'Activate' in the top-right of your n8n workflow. Test by reacting with your trigger emoji in a real Slack channel and confirming the task appears in Todoist within 10 seconds.
- 1In the Webhook node, copy the Production URL
- 2Go to the Slack API dashboard > Event Subscriptions
- 3Replace the Test URL with the Production URL and save
- 4Click the 'Activate' toggle in the top-right of your n8n workflow
- 5React to a Slack message with your trigger emoji and check Todoist
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 need full control over the message parsing logic or you're running the workflow on your own infrastructure. The emoji-to-task pattern requires stripping Slack's raw mention syntax, conditionally routing to different Todoist projects, and potentially calling three separate APIs in sequence — that's exactly the kind of multi-step logic n8n handles well with Code nodes. The one scenario where you'd skip n8n: if your team is non-technical and needs someone else to maintain this. In that case, use Zapier's Slack + Todoist integration — setup is 8 minutes, no code, and Zapier's Slack trigger includes cleaned message text out of the box.
n8n Cloud pricing starts at $20/month for 2,500 executions. This workflow uses 1 execution per reaction event — at 50 flagged messages per day across your team, that's 1,500 executions/month, well within the base plan. The HTTP Request node to fetch message text counts as part of the same execution in n8n, not a separate one. Compare that to Zapier, where the same workflow costs $49/month on the Team plan (required for multi-step Zaps), and Make, where the equivalent scenario uses roughly 3 operations per run — at 1,500 runs/month that's 4,500 operations, covered by Make's free tier (10,000 ops/month). n8n Cloud is cheaper than Zapier for this volume, and self-hosted n8n costs nothing beyond your server.
Zapier's Slack trigger labeled 'New Reaction Added' is simpler to configure — no custom app setup, no manual scope management. But it doesn't give you the raw message timestamp to fetch context, and you can't write custom parsing logic without Zapier's Code by Zapier, which requires the $49/month plan. Make has a cleaner visual flow for the three-API-call sequence and better error handling UI, but its Slack module doesn't expose reaction events cleanly — you end up needing a custom webhook anyway, which removes Make's visual advantage. Power Automate has a Slack connector but it's limited to posting messages, not reading reactions — you'd need to build the same custom webhook approach as n8n, without n8n's Code node flexibility. Pipedream's Slack source handles reaction_added natively with zero config, and the step-based editor is faster to wire up than n8n for simple cases. For this specific workflow, n8n is the right call if you're already self-hosting it — otherwise Pipedream gets you to the same result faster.
Three things you'll run into after setup. First: Slack's retry mechanism. If your n8n workflow takes more than 3 seconds to respond, Slack resends the event. Add a Respond to Webhook node at the top of your workflow that immediately returns 200 OK before doing any processing — this is non-obvious in n8n because the default Webhook node holds the response until the workflow completes. Second: the conversations.history API requires the bot to be in the channel, and it fails silently for private channels you forgot to invite the bot to — the API returns channel_not_found, not a permission error, which is confusing. Third: Todoist's natural language due date parsing (due_string) works well for English but behaves unpredictably for abbreviations like 'EOD' or 'COB' — these return no due date rather than an error. Parse those manually in your Code node and convert them to explicit dates before passing to Todoist.
Ideas for what to build next
- →Add Todoist Project Routing by Channel — Use a Switch node to map specific Slack channels to different Todoist projects — #engineering reactions go to the Engineering project, #marketing reactions go to the Marketing project. This takes about 20 minutes to configure and eliminates manual task sorting.
- →Post Confirmation Back to Slack Thread — After successful Todoist task creation, add an HTTP Request node that posts a threaded reply in Slack with the task link and due date. Team members get immediate confirmation without opening Todoist, and the Slack thread becomes a record of what was captured.
- →Daily Digest of Slack-Sourced Tasks — Add a second workflow on a cron schedule that queries Todoist's API for tasks with your 'from-slack' label created in the last 24 hours and posts a digest to a #daily-tasks Slack channel each morning. This gives teams visibility into everything flagged without checking Todoist constantly.
Related guides
How to Send Weekly Todoist Reports to Slack with Pipedream
~15 min setup
How to Send Weekly Todoist Reports to Slack with Power Automate
~15 min setup
How to Send Weekly Todoist Reports to Slack with n8n
~20 min setup
How to Send Weekly Todoist Reports to Slack with Zapier
~8 min setup
How to Send Weekly Todoist Reports to Slack with Make
~12 min setup
How to Assign Todoist Tasks from Slack Mentions with Pipedream
~15 min setup