Intermediate~20 min setupCommunication & Project ManagementVerified April 2026
Slack logo
Todoist logo

How to Post Todoist Standups to Slack with n8n

A scheduled n8n workflow fetches today's Todoist tasks for each team member each morning and posts a formatted summary to a designated Slack channel, replacing manual standup reporting.

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 of 3–20 people who track daily work in Todoist and run async or semi-sync standups in Slack.

Not ideal for

Teams that need two-way sync or want people to update their standup status directly inside Slack — use a dedicated standup bot like Geekbot for that.

Sync type

scheduled

Use case type

reporting

Real-World Example

💡

A 10-person product team tracks sprint tasks in Todoist with due dates set to each day. Before this workflow, the team lead spent 5–10 minutes each morning pinging people for updates and copying task names into Slack manually. Now at 9:00 AM the workflow pulls every task due today per assignee, builds a grouped summary, and posts it to #standup — the team lead opens Slack and the report is already there.

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 running (self-hosted or n8n Cloud) with access to create and activate workflows
Todoist account with tasks assigned due dates and, if using assignees, a shared project with collaborators added
Slack workspace with a dedicated standup channel created and the channel ID noted (right-click channel > View Details to find it)
Todoist OAuth2 or API token with task read permissions — generated in Todoist Settings > Integrations > Developer
Slack OAuth app with chat:write and channels:read scopes authorized for the workspace — n8n's built-in Slack OAuth credential handles this during setup

Field Mapping

Map these fields between your apps.

FieldAPI Name
Required
Task Contentcontent
Due Datedue.date
Slack Channel ID
Slack Message Text
5 optional fields▸ show
Assignee IDassignee_id
Project IDproject_id
Prioritypriority
Task URLurl
Task Labelslabels

Step-by-Step Setup

1

n8n Dashboard > Workflows > + New Workflow

Create a new n8n workflow

Open your n8n instance and click the '+ New Workflow' button in the top right of the Workflows dashboard. Give it a descriptive name like 'Daily Todoist Standup to Slack'. You'll land on the canvas with an empty workflow and a prompt to add your first node. This is where the scheduled trigger goes.

  1. 1Click '+ New Workflow' in the top right corner
  2. 2Click the workflow title at the top and rename it to 'Daily Todoist Standup to Slack'
  3. 3Click '+ Add first step' in the center of the canvas
What you should see: You should see an empty canvas with the workflow name saved and the node picker drawer open on the right side.
Common mistake — If you're on n8n Cloud, your timezone defaults to UTC. A 9:00 AM cron fires at 9:00 AM UTC — which could be 1:00 AM or 5:00 PM local time depending on your region. Fix this in the workflow settings before anything else.
2

Canvas > Node Picker > Schedule Trigger

Add a Schedule trigger node

Search for 'Schedule Trigger' in the node picker and select it. In the node configuration panel, set the trigger interval to 'Cron Expression' for precise control. Enter the cron expression for your standup time — for 9:00 AM Monday through Friday, use '0 9 * * 1-5'. Click 'Save' on the node.

  1. 1Type 'Schedule' in the node search box
  2. 2Click 'Schedule Trigger' from the results
  3. 3Set 'Trigger Interval' dropdown to 'Custom (Cron)'
  4. 4Enter '0 9 * * 1-5' in the Cron Expression field
  5. 5Click 'Save' to close the node panel
What you should see: The Schedule Trigger node appears on the canvas labeled with your cron expression. Hovering it shows the next scheduled execution time.
Common mistake — Confirm your workflow timezone matches your business timezone — n8n uses the instance timezone by default. Also verify the workflow is saved and set to Active, since Schedule Triggers won't fire on inactive workflows.
n8n
+
click +
search apps
Slack
SL
Slack
Add a Schedule trigger node
Slack
SL
module added
3

Canvas > + Node > Todoist > Task > Get Many

Connect Todoist and fetch today's tasks

Add a Todoist node after the Schedule Trigger. In the node picker, search 'Todoist' and select the Todoist node. Set the operation to 'Get Many' under the Task resource. You need to create a Todoist credential first — click 'Create New Credential', which opens an OAuth2 flow. Authorize n8n in the Todoist browser window that pops up. Once connected, set the Filter to 'Today' to pull only tasks due today.

  1. 1Click the '+' connector on the Schedule Trigger node
  2. 2Search 'Todoist' in the node picker and select it
  3. 3Set Resource to 'Task' and Operation to 'Get Many'
  4. 4Click 'Credential for Todoist API' > 'Create New Credential'
  5. 5Complete the Todoist OAuth2 authorization in the popup window
  6. 6Under Filters, set 'Filter' to 'Today'
What you should see: After clicking 'Test Step', you should see a list of JSON task objects in the Output panel, each containing fields like content, due, assignee_id, project_id, and priority.
Common mistake — Todoist's 'Today' filter returns tasks due today across ALL projects, including personal ones. If team members use one Todoist account for personal and work tasks, those will appear in the standup. Scope this with a specific project_id filter if needed — add it in the Additional Fields section.
4

Canvas > + Node > Code

Add a Code node to group tasks by assignee

The Todoist node returns a flat array of tasks. You need to group them by assignee so each person's tasks appear together in the Slack message. Add a Code node after Todoist. Paste the JavaScript from the Pro Tip section below into the code editor. This node outputs one item per team member with their tasks bundled, which the Slack node will iterate over.

  1. 1Click '+' after the Todoist node
  2. 2Search 'Code' in the node picker and select it
  3. 3Set 'Mode' to 'Run Once for All Items'
  4. 4Paste the grouping script into the code editor
  5. 5Click 'Test Step' to verify the output structure
What you should see: The Code node output should show one item per unique assignee, each containing a 'name' field and a 'tasks' array with the task content strings.
Common mistake — Todoist returns assignee_id as a numeric ID, not a display name. If you want real names in the Slack message, you need to map IDs to names in this Code node or add a lookup step. Hardcode a small team's ID-to-name map inside the script, or call the Todoist Collaborators API endpoint in a separate HTTP Request node before this step.

Paste this into the first Code node (set to 'Run Once for All Items'). It pulls the collaborators list from the previous HTTP Request node, builds an ID-to-name map, groups today's tasks by assignee, and returns one item per person with a pre-formatted Slack message string including priority emoji and clickable task links. The second part of the script (the message formatter) replaces the need for a separate formatting Code node.

JavaScript — Code Node// Node: Code — 'Group and Format Standup Messages'
▸ Show code
// Node: Code — 'Group and Format Standup Messages'
// Mode: Run Once for All Items
// Expects: items from Todoist node, collaborators from HTTP Request node

... expand to see full code

// Node: Code — 'Group and Format Standup Messages'
// Mode: Run Once for All Items
// Expects: items from Todoist node, collaborators from HTTP Request node

const tasks = $('Todoist').all().map(item => item.json);
const collaborators = $('HTTP Request').all().map(item => item.json);

// Build ID → display name lookup
const nameMap = {};
for (const person of collaborators) {
  nameMap[String(person.id)] = person.name;
}

// Priority emoji map (Todoist: 4=urgent, 1=normal)
const priorityEmoji = { '4': '🔴', '3': '🟠', '2': '🟡', '1': '' };

// Group tasks by assignee_id
const grouped = {};
for (const task of tasks) {
  const assigneeKey = task.assignee_id ? String(task.assignee_id) : 'unassigned';
  if (!grouped[assigneeKey]) {
    grouped[assigneeKey] = [];
  }
  grouped[assigneeKey].push(task);
}

// Build one output item per assignee
const output = [];
for (const [assigneeId, assigneeTasks] of Object.entries(grouped)) {
  if (assigneeId === 'unassigned') continue; // skip unassigned tasks

  const displayName = nameMap[assigneeId] || `User ${assigneeId}`;

  // Sort by priority descending (4 first)
  const sorted = assigneeTasks.sort((a, b) => (b.priority || 1) - (a.priority || 1));

  // Format each task line
  const lines = sorted.map(task => {
    const emoji = priorityEmoji[String(task.priority)] || '';
    const link = task.url ? ` (<${task.url}|view>)` : '';
    const labels = task.labels && task.labels.length > 0
      ? ` [${task.labels.join(', ')}]`
      : '';
    return `• ${emoji} ${task.content}${labels}${link}`.trim();
  });

  const message = `*${displayName}*\n${lines.join('\n')}`;

  output.push({
    json: {
      assignee_id: assigneeId,
      assignee_name: displayName,
      task_count: assigneeTasks.length,
      message
    }
  });
}

// Sort output alphabetically by name for consistent Slack ordering
output.sort((a, b) => a.json.assignee_name.localeCompare(b.json.assignee_name));

return output;
5

Canvas > + Node > HTTP Request

Resolve assignee IDs to display names

If your team uses assignee_id in Todoist, add an HTTP Request node before the Code node to fetch project collaborators. Call GET https://api.todoist.com/rest/v2/projects/{project_id}/collaborators with your Todoist API token in the Authorization header. This returns an array of objects with id and name. Pass this data into the Code node so you can build the ID-to-name lookup map there.

  1. 1Click '+' between the Todoist node and the Code node
  2. 2Search 'HTTP Request' and select it
  3. 3Set Method to 'GET'
  4. 4Set URL to 'https://api.todoist.com/rest/v2/projects/YOUR_PROJECT_ID/collaborators'
  5. 5Under Authentication, choose 'Header Auth' and add 'Authorization: Bearer YOUR_TODOIST_TOKEN'
  6. 6Click 'Test Step' to confirm you receive a list of collaborators
What you should see: The HTTP Request node output shows a JSON array where each entry has an 'id' (number) and 'name' (string) — e.g. {"id": 12345678, "name": "Maria Chen"}.
Common mistake — Replace YOUR_PROJECT_ID with your actual Todoist project ID. Find it by opening the project in Todoist web and reading the numeric ID from the URL bar. Using a wrong ID silently returns an empty array — no error, just no names.

Paste this into the first Code node (set to 'Run Once for All Items'). It pulls the collaborators list from the previous HTTP Request node, builds an ID-to-name map, groups today's tasks by assignee, and returns one item per person with a pre-formatted Slack message string including priority emoji and clickable task links. The second part of the script (the message formatter) replaces the need for a separate formatting Code node.

JavaScript — Code Node// Node: Code — 'Group and Format Standup Messages'
▸ Show code
// Node: Code — 'Group and Format Standup Messages'
// Mode: Run Once for All Items
// Expects: items from Todoist node, collaborators from HTTP Request node

... expand to see full code

// Node: Code — 'Group and Format Standup Messages'
// Mode: Run Once for All Items
// Expects: items from Todoist node, collaborators from HTTP Request node

const tasks = $('Todoist').all().map(item => item.json);
const collaborators = $('HTTP Request').all().map(item => item.json);

// Build ID → display name lookup
const nameMap = {};
for (const person of collaborators) {
  nameMap[String(person.id)] = person.name;
}

// Priority emoji map (Todoist: 4=urgent, 1=normal)
const priorityEmoji = { '4': '🔴', '3': '🟠', '2': '🟡', '1': '' };

// Group tasks by assignee_id
const grouped = {};
for (const task of tasks) {
  const assigneeKey = task.assignee_id ? String(task.assignee_id) : 'unassigned';
  if (!grouped[assigneeKey]) {
    grouped[assigneeKey] = [];
  }
  grouped[assigneeKey].push(task);
}

// Build one output item per assignee
const output = [];
for (const [assigneeId, assigneeTasks] of Object.entries(grouped)) {
  if (assigneeId === 'unassigned') continue; // skip unassigned tasks

  const displayName = nameMap[assigneeId] || `User ${assigneeId}`;

  // Sort by priority descending (4 first)
  const sorted = assigneeTasks.sort((a, b) => (b.priority || 1) - (a.priority || 1));

  // Format each task line
  const lines = sorted.map(task => {
    const emoji = priorityEmoji[String(task.priority)] || '';
    const link = task.url ? ` (<${task.url}|view>)` : '';
    const labels = task.labels && task.labels.length > 0
      ? ` [${task.labels.join(', ')}]`
      : '';
    return `• ${emoji} ${task.content}${labels}${link}`.trim();
  });

  const message = `*${displayName}*\n${lines.join('\n')}`;

  output.push({
    json: {
      assignee_id: assigneeId,
      assignee_name: displayName,
      task_count: assigneeTasks.length,
      message
    }
  });
}

// Sort output alphabetically by name for consistent Slack ordering
output.sort((a, b) => a.json.assignee_name.localeCompare(b.json.assignee_name));

return output;
6

Canvas > + Node > Code (second instance)

Format the Slack message with a Function node

After the Code node that groups tasks, add another Code node to build the final Slack Block Kit message payload. This step takes each grouped assignee item and converts it into a Slack-formatted string with bold names and bulleted task lists. Run this node in 'Run Once for Each Item' mode so it processes one assignee at a time and outputs one Slack message per person.

  1. 1Click '+' after the grouping Code node
  2. 2Add another Code node
  3. 3Set Mode to 'Run Once for Each Item'
  4. 4Write the message formatting logic (see Pro Tip code)
  5. 5Click 'Test Step' and inspect one output item to confirm the message string looks correct
What you should see: Each output item should contain a 'message' field with a readable standup block like '*Maria Chen*\n• Fix login bug\n• Review PR #42'.
Common mistake — Map fields using the variable picker — don't type field names manually. Hand-typed variable names often have invisible spacing errors that produce blank output.
7

Canvas > + Node > Slack > Message > Send

Add a Slack node to post each message

Add a Slack node after the formatting Code node. Set the resource to 'Message' and operation to 'Send'. Create a Slack credential using OAuth2 — click 'Create New Credential' and authorize n8n inside your Slack workspace. Set the Channel field to your standup channel ID (not the display name — use the channel ID starting with 'C'). Map the 'Text' field to the 'message' output from the previous Code node using the expression editor.

  1. 1Click '+' after the second Code node
  2. 2Search 'Slack' and select it
  3. 3Set Resource to 'Message', Operation to 'Send'
  4. 4Click 'Credential for Slack API' > 'Create New Credential' and complete OAuth2
  5. 5In the Channel field, click the expression toggle (the '=' icon) and enter your channel ID
  6. 6In the Text field, click the expression toggle and select 'message' from the previous node output
What you should see: After 'Test Step', you should see the Slack API response with 'ok: true' and a ts timestamp. Check your Slack channel — the test message should appear there within 5 seconds.
Common mistake — Use the channel ID (e.g. C04XYZ1234) not the channel name (#standup). Channel names can change; IDs don't. Find the channel ID by right-clicking the channel in Slack > 'View channel details' > scroll to the bottom.
message template
🔔 New Record: {{text}} {{user}}
channel: {{channel}}
ts: {{ts}}
#sales
🔔 New Record: Jane Smith
Company: Acme Corp
8

Canvas > + Node > Slack > Message > Send (header)

Add a header message to open the standup thread

Before the per-person Slack messages, post a single header message to the channel to frame the standup. Add a Slack node immediately after the Schedule Trigger (parallel to the Todoist fetch, or in sequence before it). Set the text to something like ':clipboard: *Daily Standup — {{$now.toFormat('EEEE, MMMM d')}}*'. This gives the thread a clear timestamp and makes it easy to search in Slack history.

  1. 1Click '+' after the Schedule Trigger to insert a new node before Todoist
  2. 2Add a Slack node set to Message > Send
  3. 3In the Text field, enter ':clipboard: *Daily Standup — {{$now.toFormat('EEEE, MMMM d')}}*'
  4. 4Use the same channel ID as the per-person messages
  5. 5Connect the output of this Slack node to the Todoist node
What you should see: When you run the full workflow test, the header message posts first, then the individual task summaries follow in the channel in sequence.
Common mistake — n8n's $now uses the server timezone, not your local timezone. If the date in the header shows yesterday's date, your n8n server clock is offset. Set the workflow timezone explicitly in Workflow Settings > Timezone.
9

Canvas > + Node > IF

Handle the no-tasks edge case

If a team member has no tasks due today, the grouping Code node may produce an empty tasks array for them. Add an IF node after the grouping Code node to check whether tasks.length is greater than 0. Route the 'true' branch to the formatting and Slack nodes. Route the 'false' branch to a No-Op node or skip entirely. This prevents sending empty standup entries like '*Maria Chen*\n(no tasks today)' unless you intentionally want that.

  1. 1Click '+' after the grouping Code node
  2. 2Search 'IF' and select the IF node
  3. 3Set Condition: Value 1 = '{{$json.tasks.length}}', Operation = 'Greater Than', Value 2 = '0'
  4. 4Connect the 'true' output to the formatting Code node
  5. 5Connect the 'false' output to a No Operation node to terminate that branch cleanly
What you should see: Running the workflow when one team member has no tasks shows that branch correctly skipped in the execution log — the Slack node fires only for members with at least one task.
10

Canvas > Test Workflow > Active Toggle

Activate and test the full workflow

Click 'Test Workflow' at the bottom of the canvas to run the entire flow end-to-end with live data. Watch the execution log — each node should show a green checkmark and the execution count. Open Slack and verify the standup messages arrived in the correct channel. Once confirmed, toggle the workflow to 'Active' using the switch in the top right corner of the canvas. The workflow will now fire automatically every weekday at your configured time.

  1. 1Click 'Test Workflow' at the bottom toolbar
  2. 2Watch each node highlight green as it executes
  3. 3Open Slack and confirm messages appear in your standup channel
  4. 4Click the 'Inactive' toggle in the top right to switch it to 'Active'
  5. 5Confirm the toggle shows 'Active' in green
What you should see: The workflow status shows 'Active' in the top right. The Executions tab shows a successful run. Your Slack standup channel has the header message plus one message block per team member who had tasks due today.
Common mistake — Clicking 'Test Workflow' runs it immediately with real data — it will post real messages to your Slack channel. If you're testing, temporarily change the channel to a private test channel you control so you don't spam the team during setup.
n8n
▶ Run once
executed
Slack
Todoist
Todoist
🔔 notification
received

Scaling Beyond Teams with 50+ members or 500+ tasks per day+ Records

If your volume exceeds Teams with 50+ members or 500+ tasks per day records, apply these adjustments.

1

Batch Slack posts to avoid rate limits

Slack's chat.postMessage endpoint has a rate limit of 1 message per second per channel. For teams with 20+ members, posting one message per person back-to-back hits this limit. Add a 'Wait' node set to 1.1 seconds between Slack nodes, or consolidate all messages into a single multi-section message using Slack Block Kit.

2

Use Todoist's pagination for large task sets

The Todoist REST API returns up to 200 tasks per request. Teams with heavy task volumes may hit this silently — you'll get exactly 200 tasks with no indication more exist. Add an HTTP Request node using Todoist's sync API with incremental sync tokens to reliably fetch all tasks regardless of count.

3

Split large teams into sub-channels

A single Slack channel with 30+ standup messages becomes unreadable fast. At 15+ members, route task summaries into team-specific channels (#frontend-standup, #backend-standup) using a Switch node on project_id or a team membership lookup. Keeps each channel's standup under 8 messages.

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 your team self-hosts or has data residency requirements — all Todoist task data stays on your own infrastructure and never passes through a third-party automation vendor's servers. It's also the right call if you want to customize the standup format significantly: grouping by project, priority sorting, conditional sections for blockers, clickable links. The Code node gives you real JavaScript, not a watered-down expression language. The one scenario where you'd skip n8n: if your team is non-technical and nobody wants to maintain JavaScript in a workflow. Use Make instead — same logic, visual interface, no code required.

Cost

n8n Cloud starts at $20/month for 2,500 executions. This workflow uses 1 execution per run regardless of team size. At 5 days/week × 4 weeks = 20 executions/month, you're nowhere near that limit. Self-hosted n8n is free with no execution cap — your only cost is the server (~$5–10/month on a basic VPS). Compare that to Zapier, where each task-to-message pair counts as a separate Zap step. A 10-person team with 5 tasks each = 50 task items processed = 50 Zap tasks per morning run × 20 days = 1,000 Zap tasks/month. That's fine on Zapier's $19.99 Starter plan (750 tasks) — actually, you'd need the $49 Professional plan. n8n self-hosted costs you $5/month in server fees for the same output.

Tradeoffs

Make handles the grouping logic more visually — its Array Aggregator and Iterator modules avoid custom code for the task-grouping step, which is genuinely easier to configure than a Code node. Zapier has a native Todoist integration with a cleaner 'New Task Due Today' trigger, but you can't group tasks across multiple items without Code by Zapier, which requires their Professional plan. Power Automate has a Todoist connector but it's a premium connector requiring a Power Automate Per User license ($15/user/month), which makes it the most expensive option here for any team. Pipedream offers the closest feature parity to n8n at similar cost, but its Todoist trigger is event-based (new task created) rather than schedule-based, so you'd need extra logic to filter for today's tasks at standup time. n8n's Schedule Trigger plus Code node combination is the most direct path to exactly this output.

Three things you'll hit after setup. First: Todoist's 'Today' filter is timezone-sensitive. If your n8n server clock is in UTC and your team is in UTC-8, you'll get yesterday's tasks in the morning report for several months of the year — always set an explicit timezone in workflow settings and use a date expression filter instead of the built-in 'Today' filter. Second: Slack's chat.postMessage will silently truncate messages longer than 4,000 characters — if someone has 40 tasks due today, their standup block will be cut off with no error returned. Add a character count check in the Code node and split into multiple messages if needed. Third: Todoist doesn't fire a webhook when due dates are set or changed, so if someone reassigns a task to today at 8:55 AM, the 9:00 AM standup will pick it up only if the timing works out — there's no way to guarantee real-time accuracy with a scheduled pull approach.

Ideas for what to build next

  • Add a blockers section from Todoist labelsTag any task with a 'blocked' label in Todoist and extend the Code node to append a separate *Blockers* section to each person's standup message, making impediments immediately visible without extra reporting.
  • Post a weekly digest on FridaysAdd a second Schedule Trigger set to Friday at 4:00 PM that pulls all tasks completed this week using Todoist's completed tasks API endpoint and posts a weekly wins summary to a separate #team-wins Slack channel.
  • Route messages to per-project Slack channelsIf your team has multiple projects with separate Slack channels, extend the workflow with a Switch node that maps each Todoist project_id to a corresponding Slack channel ID, so frontend tasks go to #frontend-standup and backend tasks go to #backend-standup.

Related guides

Was this guide helpful?
Slack + Todoist overviewn8n profile →