

How to Convert Slack Messages to Todoist Tasks with Pipedream
Watches for a specific emoji reaction on Slack messages and instantly creates a Todoist task with the message text, sender, 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
Developer-led teams who want custom parsing logic — stripping @mentions, extracting due dates from message text, or routing tasks to different Todoist projects by channel.
Not ideal for
Non-technical teams who just need a simple reaction-to-task trigger — Zapier's native Slack + Todoist integration sets that up in under 10 minutes with no code.
Sync type
real-timeUse case type
routingReal-World Example
A 12-person product team at a B2B SaaS company uses this workflow so that any message reacted with ✅ in #product-feedback instantly becomes a Todoist task assigned to the PM inbox. Before this, action items buried in Slack threads were manually copy-pasted into Todoist hours later — or not at all. Now, tasks appear in Todoist within 3 seconds of the reaction, with a direct 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 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 | ||
| Task Content (Title) | content | |
7 optional fields▸ show
| Task Description | description |
| Project ID | project_id |
| Due Date | due_date |
| Priority | priority |
| Assignee ID | assignee_id |
| Labels | labels |
| Slack Message Timestamp |
Step-by-Step Setup
pipedream.com > Dashboard > New Workflow
Create a new Pipedream Workflow
Go to pipedream.com and log in. Click 'New Workflow' from your dashboard. You'll land on the workflow canvas with an empty trigger slot at the top. Give the workflow a name like 'Slack Reaction → Todoist Task' in the title bar at the top left — this label appears in your logs, so make it descriptive.
- 1Click the blue 'New Workflow' button in the top right of your dashboard
- 2Click the workflow title field at the top and rename it to 'Slack Reaction → Todoist Task'
- 3Click 'Add a trigger' in the empty trigger block
Workflow Canvas > Add a trigger > Slack > New Reaction Added
Set the Slack trigger to 'New Reaction Added'
In the trigger selector, search for 'Slack' and select it. Scroll through the trigger list until you find 'New Reaction Added' — this fires every time any user adds an emoji reaction to any message in channels your bot can see. This is the most reliable trigger for this workflow because it's specific and doesn't create noise from every message sent.
- 1Type 'Slack' in the trigger search box
- 2Click 'Slack' in the results
- 3Scroll to find 'New Reaction Added' and click it
- 4Click 'Connect Slack' and authorize your workspace via OAuth
Trigger Config > Reaction > Channel (optional)
Configure the reaction filter
In the trigger config panel, set the Reaction field to the specific emoji name you want to use — enter it without colons, e.g. 'white_check_mark' for ✅. Leave the Channel field blank if you want this to work across all channels, or enter a specific channel ID to scope it. Setting a specific reaction is critical: without it, every single emoji added by anyone will trigger a Todoist task.
- 1Click the 'Reaction' field and type the emoji name without colons, e.g. 'white_check_mark'
- 2Optionally click 'Channel' and paste your channel ID (find it in Slack under channel settings > copy channel ID)
- 3Click 'Generate Test Event' to pull a real sample reaction event from your workspace
Workflow Canvas > + > Run Node.js code
Add a code step to fetch the original message text
The reaction event tells you which message was reacted to (via item.ts and item.channel) but does not include the message text itself. You need to call conversations.history to retrieve the actual message content. Click the '+' button below the trigger to add a new step, then select 'Run Node.js code'. This step will use the Slack API with your connected account to fetch the message.
- 1Click the '+' icon below the trigger block
- 2Select 'Run Node.js code' from the step type list
- 3Rename the step to 'fetch_message' by clicking the step name at the top
- 4Paste the code from the pro tip section below into the code editor
Paste this into two separate Node.js code steps in Pipedream: the first step (fetch_and_parse) fetches the message text via Slack API and cleans it, the second step creates the Todoist task with full error handling. The deduplication check uses Pipedream's built-in $.data key-value store so you don't need an external database.
JavaScript — Code Step// ── STEP 1: fetch_and_parse ──────────────────────────────────────────▸ Show code
// ── STEP 1: fetch_and_parse ──────────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({... expand to see full code
// ── STEP 1: fetch_and_parse ──────────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({
props: {
slack: {
type: "app",
app: "slack",
},
},
async run({ steps, $ }) {
const { channel, ts } = steps.trigger.event.item;
const reactionUser = steps.trigger.event.user;
// Deduplication: skip if we've already processed this message+reaction
const dedupKey = `slack-task-${channel}-${ts}`;
const alreadySeen = await $.data.get(dedupKey);
if (alreadySeen) {
$.flow.exit("Duplicate reaction — task already created for this message.");
}
// Fetch the original message text from Slack
const historyResp = await axios($, {
url: "https://slack.com/api/conversations.history",
headers: {
Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`,
},
params: {
channel,
latest: ts,
inclusive: true,
limit: 1,
},
});
if (!historyResp.ok) {
throw new Error(`Slack API error: ${historyResp.error}`);
}
const message = historyResp.messages?.[0];
if (!message) throw new Error("Message not found at timestamp.");
// Resolve the reacting user's display name
const userResp = await axios($, {
url: "https://slack.com/api/users.info",
headers: {
Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`,
},
params: { user: message.user || reactionUser },
});
const senderName = userResp.user?.profile?.display_name || "Unknown";
// Clean Slack mention syntax: <@U012AB3CD> → @username
let cleanText = message.text.replace(/<@([A-Z0-9]+)>/g, (match, uid) => `@${uid}`);
// Strip channel references like <#C012ABC|general> → #general
cleanText = cleanText.replace(/<#[A-Z0-9]+\|([^>]+)>/g, "#$1");
// Strip URLs wrapped in angle brackets
cleanText = cleanText.replace(/<(https?:\/\/[^|>]+)(?:\|[^>]+)?>/g, "$1");
// Extract due date if message contains 'due: <day>'
const dueDateMatch = cleanText.match(/due:\s*(\w+)/i);
let dueString = null;
if (dueDateMatch) {
// Basic day-name to date — extend this for production
const days = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];
const targetDay = days.indexOf(dueDateMatch[1].toLowerCase());
if (targetDay !== -1) {
const now = new Date();
const diff = (targetDay - now.getDay() + 7) % 7 || 7;
const due = new Date(now);
due.setDate(now.getDate() + diff);
dueString = due.toISOString().split("T")[0];
}
// Remove the 'due: Thursday' fragment from task title
cleanText = cleanText.replace(/due:\s*\w+/i, "").trim();
}
// Build Slack permalink
const tsForUrl = ts.replace(".", "");
const threadUrl = `https://slack.com/archives/${channel}/p${tsForUrl}`;
// Limit task title to 140 chars
const taskTitle = cleanText.length > 140 ? cleanText.substring(0, 137) + "..." : cleanText;
// Mark as processed to prevent duplicate tasks
await $.data.set(dedupKey, true);
return { taskTitle, dueString, senderName, threadUrl, channel };
},
});
// ── STEP 2: create_todoist_task ──────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({
props: {
todoist: {
type: "app",
app: "todoist",
},
},
async run({ steps, $ }) {
const { taskTitle, dueString, senderName, threadUrl, channel } = steps.fetch_and_parse.$return_value;
// Route to different Todoist projects based on Slack channel
const channelProjectMap = {
"C05XABCDE": 2312345678, // #product-feedback → Product project
"C02XBUGFIX": 2398765432, // #bugs → Engineering project
};
const projectId = channelProjectMap[channel] || null; // null = Inbox
const payload = {
content: taskTitle,
description: `Source: ${threadUrl}\nFrom: ${senderName}`,
priority: 2,
...(dueString && { due_date: dueString }),
...(projectId && { project_id: projectId }),
labels: ["slack"],
};
let taskResponse;
try {
taskResponse = await axios($, {
method: "POST",
url: "https://api.todoist.com/rest/v2/tasks",
headers: {
Authorization: `Bearer ${this.todoist.$auth.oauth_access_token}`,
"Content-Type": "application/json",
},
data: payload,
});
} catch (err) {
// Log full error for debugging, then rethrow so Pipedream marks the run failed
console.error("Todoist API error:", err.response?.data || err.message);
throw new Error(`Failed to create Todoist task: ${err.response?.data?.error || err.message}`);
}
console.log(`Task created: ${taskResponse.id} — "${taskTitle}"`);
return { taskId: taskResponse.id, taskUrl: taskResponse.url };
},
});channel: {{channel}}
ts: {{ts}}
Workflow Canvas > + > Run Node.js code
Add a second code step to parse the message
This step strips Slack's user mention formatting (e.g. <@U012AB3CD> → @username), removes channel references, and optionally extracts a due date if someone wrote 'due: Friday' in the message. This is where Pipedream earns its place — you can write real logic here that no-code tools cannot handle without multiple workaround steps.
- 1Click '+' below the fetch_message step
- 2Select 'Run Node.js code'
- 3Rename this step to 'parse_message'
- 4Add your parsing logic to clean the message text and extract any metadata
Paste this into two separate Node.js code steps in Pipedream: the first step (fetch_and_parse) fetches the message text via Slack API and cleans it, the second step creates the Todoist task with full error handling. The deduplication check uses Pipedream's built-in $.data key-value store so you don't need an external database.
JavaScript — Code Step// ── STEP 1: fetch_and_parse ──────────────────────────────────────────▸ Show code
// ── STEP 1: fetch_and_parse ──────────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({... expand to see full code
// ── STEP 1: fetch_and_parse ──────────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({
props: {
slack: {
type: "app",
app: "slack",
},
},
async run({ steps, $ }) {
const { channel, ts } = steps.trigger.event.item;
const reactionUser = steps.trigger.event.user;
// Deduplication: skip if we've already processed this message+reaction
const dedupKey = `slack-task-${channel}-${ts}`;
const alreadySeen = await $.data.get(dedupKey);
if (alreadySeen) {
$.flow.exit("Duplicate reaction — task already created for this message.");
}
// Fetch the original message text from Slack
const historyResp = await axios($, {
url: "https://slack.com/api/conversations.history",
headers: {
Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`,
},
params: {
channel,
latest: ts,
inclusive: true,
limit: 1,
},
});
if (!historyResp.ok) {
throw new Error(`Slack API error: ${historyResp.error}`);
}
const message = historyResp.messages?.[0];
if (!message) throw new Error("Message not found at timestamp.");
// Resolve the reacting user's display name
const userResp = await axios($, {
url: "https://slack.com/api/users.info",
headers: {
Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`,
},
params: { user: message.user || reactionUser },
});
const senderName = userResp.user?.profile?.display_name || "Unknown";
// Clean Slack mention syntax: <@U012AB3CD> → @username
let cleanText = message.text.replace(/<@([A-Z0-9]+)>/g, (match, uid) => `@${uid}`);
// Strip channel references like <#C012ABC|general> → #general
cleanText = cleanText.replace(/<#[A-Z0-9]+\|([^>]+)>/g, "#$1");
// Strip URLs wrapped in angle brackets
cleanText = cleanText.replace(/<(https?:\/\/[^|>]+)(?:\|[^>]+)?>/g, "$1");
// Extract due date if message contains 'due: <day>'
const dueDateMatch = cleanText.match(/due:\s*(\w+)/i);
let dueString = null;
if (dueDateMatch) {
// Basic day-name to date — extend this for production
const days = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];
const targetDay = days.indexOf(dueDateMatch[1].toLowerCase());
if (targetDay !== -1) {
const now = new Date();
const diff = (targetDay - now.getDay() + 7) % 7 || 7;
const due = new Date(now);
due.setDate(now.getDate() + diff);
dueString = due.toISOString().split("T")[0];
}
// Remove the 'due: Thursday' fragment from task title
cleanText = cleanText.replace(/due:\s*\w+/i, "").trim();
}
// Build Slack permalink
const tsForUrl = ts.replace(".", "");
const threadUrl = `https://slack.com/archives/${channel}/p${tsForUrl}`;
// Limit task title to 140 chars
const taskTitle = cleanText.length > 140 ? cleanText.substring(0, 137) + "..." : cleanText;
// Mark as processed to prevent duplicate tasks
await $.data.set(dedupKey, true);
return { taskTitle, dueString, senderName, threadUrl, channel };
},
});
// ── STEP 2: create_todoist_task ──────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({
props: {
todoist: {
type: "app",
app: "todoist",
},
},
async run({ steps, $ }) {
const { taskTitle, dueString, senderName, threadUrl, channel } = steps.fetch_and_parse.$return_value;
// Route to different Todoist projects based on Slack channel
const channelProjectMap = {
"C05XABCDE": 2312345678, // #product-feedback → Product project
"C02XBUGFIX": 2398765432, // #bugs → Engineering project
};
const projectId = channelProjectMap[channel] || null; // null = Inbox
const payload = {
content: taskTitle,
description: `Source: ${threadUrl}\nFrom: ${senderName}`,
priority: 2,
...(dueString && { due_date: dueString }),
...(projectId && { project_id: projectId }),
labels: ["slack"],
};
let taskResponse;
try {
taskResponse = await axios($, {
method: "POST",
url: "https://api.todoist.com/rest/v2/tasks",
headers: {
Authorization: `Bearer ${this.todoist.$auth.oauth_access_token}`,
"Content-Type": "application/json",
},
data: payload,
});
} catch (err) {
// Log full error for debugging, then rethrow so Pipedream marks the run failed
console.error("Todoist API error:", err.response?.data || err.message);
throw new Error(`Failed to create Todoist task: ${err.response?.data?.error || err.message}`);
}
console.log(`Task created: ${taskResponse.id} — "${taskTitle}"`);
return { taskId: taskResponse.id, taskUrl: taskResponse.url };
},
});Workflow Canvas > + > Todoist > Create Task
Add a Todoist step to create the task
Click '+' below your parse_message step and search for 'Todoist'. Select the 'Create Task' action. Connect your Todoist account via OAuth when prompted. You'll see fields for Content, Due Date, Project, Priority, and Description. Map these from your previous code step outputs using the reference syntax {{steps.parse_message.$return_value.taskTitle}}.
- 1Click '+' and search for 'Todoist'
- 2Select 'Create Task' from the action list
- 3Click 'Connect Todoist' and authorize via OAuth
- 4In the Content field, click the reference icon and select steps.parse_message.$return_value.taskTitle
- 5In the Description field, paste the Slack thread URL using steps.fetch_message.$return_value.threadUrl
Todoist Step Config > Description field
Map the Slack thread URL into the task description
Todoist tasks are useless without context. Construct a direct Slack message link using the channel ID and timestamp from the trigger event. The URL format is https://yourworkspace.slack.com/archives/{channel_id}/p{timestamp_without_dot}. Include this in the Todoist Description field so anyone clicking the task can jump straight to the Slack thread.
- 1In the Todoist Create Task step, click the Description field
- 2Type 'Source: ' then click the reference icon
- 3Navigate to steps.fetch_message.$return_value.threadUrl
- 4Also add the original sender's Slack display name for attribution
Paste this into two separate Node.js code steps in Pipedream: the first step (fetch_and_parse) fetches the message text via Slack API and cleans it, the second step creates the Todoist task with full error handling. The deduplication check uses Pipedream's built-in $.data key-value store so you don't need an external database.
JavaScript — Code Step// ── STEP 1: fetch_and_parse ──────────────────────────────────────────▸ Show code
// ── STEP 1: fetch_and_parse ──────────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({... expand to see full code
// ── STEP 1: fetch_and_parse ──────────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({
props: {
slack: {
type: "app",
app: "slack",
},
},
async run({ steps, $ }) {
const { channel, ts } = steps.trigger.event.item;
const reactionUser = steps.trigger.event.user;
// Deduplication: skip if we've already processed this message+reaction
const dedupKey = `slack-task-${channel}-${ts}`;
const alreadySeen = await $.data.get(dedupKey);
if (alreadySeen) {
$.flow.exit("Duplicate reaction — task already created for this message.");
}
// Fetch the original message text from Slack
const historyResp = await axios($, {
url: "https://slack.com/api/conversations.history",
headers: {
Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`,
},
params: {
channel,
latest: ts,
inclusive: true,
limit: 1,
},
});
if (!historyResp.ok) {
throw new Error(`Slack API error: ${historyResp.error}`);
}
const message = historyResp.messages?.[0];
if (!message) throw new Error("Message not found at timestamp.");
// Resolve the reacting user's display name
const userResp = await axios($, {
url: "https://slack.com/api/users.info",
headers: {
Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`,
},
params: { user: message.user || reactionUser },
});
const senderName = userResp.user?.profile?.display_name || "Unknown";
// Clean Slack mention syntax: <@U012AB3CD> → @username
let cleanText = message.text.replace(/<@([A-Z0-9]+)>/g, (match, uid) => `@${uid}`);
// Strip channel references like <#C012ABC|general> → #general
cleanText = cleanText.replace(/<#[A-Z0-9]+\|([^>]+)>/g, "#$1");
// Strip URLs wrapped in angle brackets
cleanText = cleanText.replace(/<(https?:\/\/[^|>]+)(?:\|[^>]+)?>/g, "$1");
// Extract due date if message contains 'due: <day>'
const dueDateMatch = cleanText.match(/due:\s*(\w+)/i);
let dueString = null;
if (dueDateMatch) {
// Basic day-name to date — extend this for production
const days = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];
const targetDay = days.indexOf(dueDateMatch[1].toLowerCase());
if (targetDay !== -1) {
const now = new Date();
const diff = (targetDay - now.getDay() + 7) % 7 || 7;
const due = new Date(now);
due.setDate(now.getDate() + diff);
dueString = due.toISOString().split("T")[0];
}
// Remove the 'due: Thursday' fragment from task title
cleanText = cleanText.replace(/due:\s*\w+/i, "").trim();
}
// Build Slack permalink
const tsForUrl = ts.replace(".", "");
const threadUrl = `https://slack.com/archives/${channel}/p${tsForUrl}`;
// Limit task title to 140 chars
const taskTitle = cleanText.length > 140 ? cleanText.substring(0, 137) + "..." : cleanText;
// Mark as processed to prevent duplicate tasks
await $.data.set(dedupKey, true);
return { taskTitle, dueString, senderName, threadUrl, channel };
},
});
// ── STEP 2: create_todoist_task ──────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({
props: {
todoist: {
type: "app",
app: "todoist",
},
},
async run({ steps, $ }) {
const { taskTitle, dueString, senderName, threadUrl, channel } = steps.fetch_and_parse.$return_value;
// Route to different Todoist projects based on Slack channel
const channelProjectMap = {
"C05XABCDE": 2312345678, // #product-feedback → Product project
"C02XBUGFIX": 2398765432, // #bugs → Engineering project
};
const projectId = channelProjectMap[channel] || null; // null = Inbox
const payload = {
content: taskTitle,
description: `Source: ${threadUrl}\nFrom: ${senderName}`,
priority: 2,
...(dueString && { due_date: dueString }),
...(projectId && { project_id: projectId }),
labels: ["slack"],
};
let taskResponse;
try {
taskResponse = await axios($, {
method: "POST",
url: "https://api.todoist.com/rest/v2/tasks",
headers: {
Authorization: `Bearer ${this.todoist.$auth.oauth_access_token}`,
"Content-Type": "application/json",
},
data: payload,
});
} catch (err) {
// Log full error for debugging, then rethrow so Pipedream marks the run failed
console.error("Todoist API error:", err.response?.data || err.message);
throw new Error(`Failed to create Todoist task: ${err.response?.data?.error || err.message}`);
}
console.log(`Task created: ${taskResponse.id} — "${taskTitle}"`);
return { taskId: taskResponse.id, taskUrl: taskResponse.url };
},
});Workflow Canvas > + > Run Node.js code (error handler)
Add error handling with a code step
Wrap your Todoist step in a try/catch or add a dedicated error step using Pipedream's $.flow.exit() helper. If the Todoist API call fails (rate limit, bad token), you want the workflow to log the failure and optionally send a Slack DM to the person who added the reaction so they know the task wasn't created. Without this, failures are silent.
- 1Click '+' after the Todoist step
- 2Select 'Run Node.js code'
- 3Rename the step to 'error_handler'
- 4Add logic to check if the Todoist step succeeded and send a Slack DM on failure
Paste this into two separate Node.js code steps in Pipedream: the first step (fetch_and_parse) fetches the message text via Slack API and cleans it, the second step creates the Todoist task with full error handling. The deduplication check uses Pipedream's built-in $.data key-value store so you don't need an external database.
JavaScript — Code Step// ── STEP 1: fetch_and_parse ──────────────────────────────────────────▸ Show code
// ── STEP 1: fetch_and_parse ──────────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({... expand to see full code
// ── STEP 1: fetch_and_parse ──────────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({
props: {
slack: {
type: "app",
app: "slack",
},
},
async run({ steps, $ }) {
const { channel, ts } = steps.trigger.event.item;
const reactionUser = steps.trigger.event.user;
// Deduplication: skip if we've already processed this message+reaction
const dedupKey = `slack-task-${channel}-${ts}`;
const alreadySeen = await $.data.get(dedupKey);
if (alreadySeen) {
$.flow.exit("Duplicate reaction — task already created for this message.");
}
// Fetch the original message text from Slack
const historyResp = await axios($, {
url: "https://slack.com/api/conversations.history",
headers: {
Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`,
},
params: {
channel,
latest: ts,
inclusive: true,
limit: 1,
},
});
if (!historyResp.ok) {
throw new Error(`Slack API error: ${historyResp.error}`);
}
const message = historyResp.messages?.[0];
if (!message) throw new Error("Message not found at timestamp.");
// Resolve the reacting user's display name
const userResp = await axios($, {
url: "https://slack.com/api/users.info",
headers: {
Authorization: `Bearer ${this.slack.$auth.oauth_access_token}`,
},
params: { user: message.user || reactionUser },
});
const senderName = userResp.user?.profile?.display_name || "Unknown";
// Clean Slack mention syntax: <@U012AB3CD> → @username
let cleanText = message.text.replace(/<@([A-Z0-9]+)>/g, (match, uid) => `@${uid}`);
// Strip channel references like <#C012ABC|general> → #general
cleanText = cleanText.replace(/<#[A-Z0-9]+\|([^>]+)>/g, "#$1");
// Strip URLs wrapped in angle brackets
cleanText = cleanText.replace(/<(https?:\/\/[^|>]+)(?:\|[^>]+)?>/g, "$1");
// Extract due date if message contains 'due: <day>'
const dueDateMatch = cleanText.match(/due:\s*(\w+)/i);
let dueString = null;
if (dueDateMatch) {
// Basic day-name to date — extend this for production
const days = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];
const targetDay = days.indexOf(dueDateMatch[1].toLowerCase());
if (targetDay !== -1) {
const now = new Date();
const diff = (targetDay - now.getDay() + 7) % 7 || 7;
const due = new Date(now);
due.setDate(now.getDate() + diff);
dueString = due.toISOString().split("T")[0];
}
// Remove the 'due: Thursday' fragment from task title
cleanText = cleanText.replace(/due:\s*\w+/i, "").trim();
}
// Build Slack permalink
const tsForUrl = ts.replace(".", "");
const threadUrl = `https://slack.com/archives/${channel}/p${tsForUrl}`;
// Limit task title to 140 chars
const taskTitle = cleanText.length > 140 ? cleanText.substring(0, 137) + "..." : cleanText;
// Mark as processed to prevent duplicate tasks
await $.data.set(dedupKey, true);
return { taskTitle, dueString, senderName, threadUrl, channel };
},
});
// ── STEP 2: create_todoist_task ──────────────────────────────────────
import { axios } from "@pipedream/platform";
export default defineComponent({
props: {
todoist: {
type: "app",
app: "todoist",
},
},
async run({ steps, $ }) {
const { taskTitle, dueString, senderName, threadUrl, channel } = steps.fetch_and_parse.$return_value;
// Route to different Todoist projects based on Slack channel
const channelProjectMap = {
"C05XABCDE": 2312345678, // #product-feedback → Product project
"C02XBUGFIX": 2398765432, // #bugs → Engineering project
};
const projectId = channelProjectMap[channel] || null; // null = Inbox
const payload = {
content: taskTitle,
description: `Source: ${threadUrl}\nFrom: ${senderName}`,
priority: 2,
...(dueString && { due_date: dueString }),
...(projectId && { project_id: projectId }),
labels: ["slack"],
};
let taskResponse;
try {
taskResponse = await axios($, {
method: "POST",
url: "https://api.todoist.com/rest/v2/tasks",
headers: {
Authorization: `Bearer ${this.todoist.$auth.oauth_access_token}`,
"Content-Type": "application/json",
},
data: payload,
});
} catch (err) {
// Log full error for debugging, then rethrow so Pipedream marks the run failed
console.error("Todoist API error:", err.response?.data || err.message);
throw new Error(`Failed to create Todoist task: ${err.response?.data?.error || err.message}`);
}
console.log(`Task created: ${taskResponse.id} — "${taskTitle}"`);
return { taskId: taskResponse.id, taskUrl: taskResponse.url };
},
});pipedream.com > Workflow > Event History
Test end-to-end with a real Slack message
Go to your Slack workspace and react to any message in the monitored channel with your chosen emoji. Switch back to Pipedream and click 'Event History' in the left sidebar to watch for the incoming event. You should see the workflow trigger, the fetch_message step fetch the text, and the Todoist step return a task ID within 3-5 seconds.
- 1Open Slack and react to a test message with your configured emoji (e.g. ✅)
- 2Switch to Pipedream and click 'Event History' in the left sidebar
- 3Click the incoming event to inspect each step's input and output
- 4Open Todoist and verify the task appears in the correct project with the Slack link in the description
Workflow Canvas > Deploy button (top right)
Deploy the workflow
Click the blue 'Deploy' button in the top right of the workflow canvas. Pipedream will activate the Slack event source and start listening for reactions in real time via webhook. The workflow status will change from 'Development' to 'Active'. Any reaction matching your filter from this point forward will trigger the workflow automatically.
- 1Click the blue 'Deploy' button in the top right
- 2Confirm the deployment in the dialog that appears
- 3Check the workflow status badge — it should read 'Active' in green
- 4Add a final test reaction in Slack to confirm the live workflow fires correctly
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 needs custom logic that no-code tools can't handle: parsing due dates out of message text, routing to different Todoist projects based on channel, resolving Slack user IDs to names, or deduplicating reactions from multiple users. Pipedream processes the Slack webhook in under a second and lets you write that logic in real Node.js without workarounds. The one case where you'd skip Pipedream: if your team is non-technical and just needs emoji → task with no customization. Zapier's native Slack + Todoist integration does that in 10 minutes flat.
Pipedream's free tier gives you 100 credits/day. This workflow runs 3-4 steps per trigger, costing roughly 3-4 credits per run. That's about 25-33 task creations per day before you hit the free limit. At the Basic paid plan ($19/month), you get 10,000 credits/month — enough for roughly 2,500-3,300 task creations per month, which covers most teams comfortably. Zapier's equivalent workflow costs $19.99/month for 750 tasks. Make's free tier handles 1,000 operations/month with the same workflow logic, which beats Pipedream's free tier if volume is your only concern.
Zapier handles the basic reaction-to-task trigger faster to set up — their native integration has both apps pre-connected and the field mapping is visual. Make has better conditional branching in the UI if you want to route to different projects without writing code. n8n's self-hosted version has zero per-run cost if you're running it on your own server, which matters at scale. Power Automate's Slack connector is still rough — reaction triggers are unreliable as of early 2024 and not worth the debugging time. Pipedream wins here specifically because the Slack event source is solid, the webhook fires instantly, and the Node.js steps mean you can handle every edge case (thread replies, bot messages, mention resolution) in one place without stitching together five workaround steps.
Three things you'll run into after you deploy. First, Slack's conversations.history API has a rate limit of 50 requests per minute per token — in a busy workspace where many reactions fire at once, you'll hit it. Add exponential backoff in your fetch step or the whole workflow fails silently. Second, Todoist's REST API v2 returns a 429 Too Many Requests if you fire more than 450 requests per 15 minutes — this rarely happens for task creation specifically, but if you're also fetching collaborators for assignee lookup, the calls add up. Third, Slack message text with block kit formatting (the newer rich text format) doesn't come through as plain text in the message.text field — it comes in blocks[]. If your team uses newer Slack clients, your text extraction needs to handle both formats or you'll get empty task titles.
Ideas for what to build next
- →Add a Todoist comment with the full Slack thread — After task creation, fetch all replies in the Slack thread using conversations.replies and post them as a single Todoist task comment so assignees have full context without opening Slack.
- →Route tasks to different assignees by channel — Extend the channel-to-project map to also set a Todoist assignee_id based on which Slack channel the message came from — so #design reactions always assign to the design lead's Todoist account.
- →Post a Slack confirmation message when the task is created — After the Todoist task is created, use the Slack API to post a threaded reply on the original message confirming 'Task created in Todoist: [link]' — closes the loop for the team without anyone leaving Slack.
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