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

How to Send Weekly Todoist Reports to Slack with Pipedream

Every Monday morning, Pipedream queries the Todoist API for the past 7 days of completed tasks, aggregates counts by project, and posts a formatted summary message to a Slack channel for leadership review.

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 track work in Todoist and need a consistent Monday morning status post in Slack without someone manually compiling it.

Not ideal for

Teams needing real-time task alerts — use a webhook-based Todoist-to-Slack trigger instead of this scheduled digest approach.

Sync type

scheduled

Use case type

reporting

Real-World Example

💡

A 12-person product team at a B2B SaaS company tracks sprint tasks in Todoist across 4 projects. Before this workflow, the team lead spent 20-30 minutes every Monday pulling completion numbers and writing a Slack update by hand. Now Pipedream runs at 8:00 AM every Monday, pulls the previous week's completed tasks from the Todoist API, groups them by project, and posts a structured summary to #leadership-updates automatically.

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

Todoist Premium or Business account — the completed tasks API endpoint is not available on free plans
Todoist API access: ensure your account has 'data:read' scope available via OAuth
Slack bot or app with 'chat:write' scope installed in your workspace — user tokens work but produce messages from your personal account
The Slack bot must be invited to the target channel before posting — use /invite @botname in the channel
A Pipedream account — free tier supports scheduled workflows but limits to 10,000 credits/month

Field Mapping

Map these fields between your apps.

FieldAPI Name
Required
Task Completion Datecompleted_at
Project IDproject_id
Slack Channel ID
Slack Block Kit Payload
Report Date Range (Since / Until)
3 optional fields▸ show
Task Contentcontent
Task IDid
Assignee User IDuser_id

Step-by-Step Setup

1

pipedream.com > Workflows > New Workflow

Create a new Pipedream Workflow

Go to pipedream.com and click 'Workflows' in the left sidebar. Click the green 'New Workflow' button in the top right corner. Pipedream will open a blank workflow canvas with an empty trigger slot at the top. Give the workflow a name — something like 'Weekly Todoist Report to Slack' — by clicking the default name at the top of the canvas.

  1. 1Log in at pipedream.com
  2. 2Click 'Workflows' in the left navigation
  3. 3Click the green 'New Workflow' button
  4. 4Click the workflow name at the top and rename it to 'Weekly Todoist Report to Slack'
What you should see: You should see a blank workflow canvas with a single empty trigger block labeled 'Add Trigger' at the top.
2

Workflow Canvas > Add Trigger > Schedule > Cron Scheduler

Set a Schedule Trigger

Click the 'Add Trigger' block. In the trigger selector panel that slides in from the right, search for 'Schedule' and select it. Choose 'Cron Scheduler' as the trigger type. Enter the cron expression '0 8 * * 1' — this fires every Monday at 8:00 AM UTC. Adjust the timezone field if your leadership team is not on UTC.

  1. 1Click the 'Add Trigger' block
  2. 2Type 'Schedule' in the search box
  3. 3Select 'Schedule' from the results
  4. 4Choose 'Cron Scheduler' as the schedule type
  5. 5Enter '0 8 * * 1' in the cron expression field
What you should see: The trigger block should show 'Cron Scheduler' with your expression '0 8 * * 1' and a human-readable label like 'Every Monday at 8:00 AM'.
Common mistake — Pipedream executes cron triggers in UTC by default. If your team is in EST (UTC-5), enter '0 13 * * 1' to hit 8:00 AM local time. There is no built-in timezone picker for cron — you must do the offset math yourself.
Pipedream
+
click +
search apps
Slack
SL
Slack
Set a Schedule Trigger
Slack
SL
module added
3

Workflow Canvas > + Add Step > App > Todoist > Get Completed Tasks

Connect Your Todoist Account

Click '+ Add Step' below the trigger and select 'App' from the step type options. Search for 'Todoist' in the app selector. Choose the action 'Get Completed Tasks' (also listed as 'Get all completed items' depending on the component version). In the step configuration panel, click 'Connect Todoist Account' and follow the OAuth flow. Todoist will ask you to authorize Pipedream — accept all requested scopes.

  1. 1Click '+ Add Step' below the Schedule trigger
  2. 2Select 'App' as the step type
  3. 3Search for 'Todoist' and select it
  4. 4Choose the action 'Get Completed Tasks'
  5. 5Click 'Connect Todoist Account' and complete the OAuth flow in the popup
What you should see: The Todoist step should show your account name in the 'Account' dropdown with a green connected indicator. The step configuration fields for 'Project ID', 'Since', and 'Until' will appear below.
Common mistake — The Todoist 'Get Completed Tasks' action only works with Todoist Premium or Business accounts. Free Todoist accounts do not have API access to completed task history — the endpoint will return a 403 error.
Pipedream settings
Connection
Choose a connection…Add
click Add
Slack
Log in to authorize
Authorize Pipedream
popup window
Connected
green checkmark
4

Workflow Canvas > Todoist Step > Configure > Since / Until fields

Configure the Todoist Date Range Parameters

In the Todoist step, you need to pass dynamic date values for 'Since' and 'Until' so the query always covers the last 7 days. Click the 'Since' field and switch to expression mode (click the '{{ }}' toggle). Enter a JavaScript expression to calculate last Monday's date. Leave 'Project ID' blank to fetch completions across all projects, or enter a specific project ID to narrow the scope. Set 'Limit' to 200 to avoid missing tasks in busy weeks.

  1. 1Click the 'Since' field in the Todoist step
  2. 2Click the '{{ }}' expression toggle to enable dynamic values
  3. 3Enter the expression for 7 days ago (handled in the code step next — leave this as a static value for now: the prior Monday's ISO date)
  4. 4Leave 'Project ID' blank for all projects or paste a specific project ID
  5. 5Set 'Limit' to 200
What you should see: The Todoist step configuration should show the Since date, an empty Project ID field (or your specific ID), and Limit set to 200.
Common mistake — The Todoist completed tasks endpoint returns a maximum of 200 items per call. If your team completes more than 200 tasks in a week, you will silently miss items — the API does not paginate automatically in this Pipedream component. For very active teams, use the code step approach in the Pro Tip instead.
5

Workflow Canvas > + Add Step > Run Node.js code

Add a Node.js Code Step to Aggregate Task Data

Click '+ Add Step' and select 'Run Node.js code'. This is where you transform the raw Todoist API response into a structured summary grouped by project. Paste the code from the Pro Tip section into the code editor. The script pulls the completed task array from the previous step, groups tasks by project name, counts completions per project, calculates a total, and assembles the Slack message payload. Click 'Test' to run it against real data before moving on.

  1. 1Click '+ Add Step' below the Todoist step
  2. 2Select 'Run Node.js code'
  3. 3Delete the placeholder code in the editor
  4. 4Paste the aggregation code from the Pro Tip section
  5. 5Click 'Test' to validate the output
What you should see: After clicking Test, the step's Return Value panel should show a JSON object containing 'totalCompleted', 'byProject' (an array of project names with counts), and 'slackBlocks' (the formatted Slack Block Kit payload).

Paste this into the Node.js code step (Step 3 in your workflow). It pulls the completed tasks array from the Todoist step output, resolves project IDs to names using a second Todoist API call, groups and counts by project, then builds a Slack Block Kit payload ready to drop into the Slack step's Blocks field.

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, $ }) {
    // Pull completed tasks from previous Todoist step
    const completedItems = steps.todoist.$return_value?.items || [];

    if (completedItems.length === 0) {
      // Post a fallback message rather than an empty report
      return {
        totalCompleted: 0,
        byProject: [],
        slackBlocks: [
          {
            type: 'header',
            text: { type: 'plain_text', text: '📋 Weekly Progress Report', emoji: true }
          },
          {
            type: 'section',
            text: { type: 'mrkdwn', text: 'No tasks were completed in Todoist this week.' }
          }
        ]
      };
    }

    // Fetch project names from Todoist API to resolve IDs to labels
    const todoist_token = process.env.TODOIST_API_TOKEN;
    let projectMap = {};
    try {
      const projectsResp = await axios.get('https://api.todoist.com/rest/v2/projects', {
        headers: { Authorization: `Bearer ${todoist_token}` }
      });
      for (const project of projectsResp.data) {
        projectMap[project.id] = project.name;
      }
    } catch (err) {
      console.error('Failed to fetch Todoist projects:', err.message);
      // Continue with raw IDs if lookup fails
    }

    // Group completions by project
    const projectCounts = {};
    for (const item of completedItems) {
      const projectName = projectMap[item.project_id] || `Project ${item.project_id}`;
      projectCounts[projectName] = (projectCounts[projectName] || 0) + 1;
    }

    const totalCompleted = completedItems.length;
    const byProject = Object.entries(projectCounts)
      .sort((a, b) => b[1] - a[1])
      .map(([name, count]) => ({ name, count }));

    // Build date range label
    const now = new Date();
    const lastMonday = new Date(now);
    lastMonday.setDate(now.getDate() - 7);
    const dateLabel = `${lastMonday.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}–${now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;

    // Build Slack Block Kit payload
    const projectLines = byProject
      .map(p => `• *${p.name}* — ${p.count} task${p.count !== 1 ? 's' : ''} completed`)
      .join('\n');

    const slackBlocks = [
      {
        type: 'header',
        text: { type: 'plain_text', text: `📋 Weekly Progress Report — ${dateLabel}`, emoji: true }
      },
      { type: 'divider' },
      {
        type: 'section',
        text: { type: 'mrkdwn', text: projectLines }
      },
      { type: 'divider' },
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `*Total completed this week: ${totalCompleted} task${totalCompleted !== 1 ? 's' : ''}*` }
      },
      {
        type: 'context',
        elements: [{ type: 'mrkdwn', text: `Generated automatically by Pipedream · ${new Date().toISOString()}` }]
      }
    ];

    return { totalCompleted, byProject, slackBlocks };
  }
});
6

Workflow Canvas > + Add Step > App > Slack > Send Message

Connect Your Slack Account

Click '+ Add Step' and search for 'Slack'. Select the 'Send Message' action (or 'Send Block Kit Message' if you want rich formatting). Click 'Connect Slack Account' — this opens a Slack OAuth window asking you to select the workspace and approve the bot's permissions. You need at minimum the 'chat:write' scope. If you want the bot to post to private channels, you must invite it to those channels manually in Slack first.

  1. 1Click '+ Add Step' below the code step
  2. 2Search for 'Slack' and select it
  3. 3Choose 'Send Message (Block Kit)' for formatted output
  4. 4Click 'Connect Slack Account'
  5. 5Select your workspace in the OAuth popup and click Allow
What you should see: The Slack step should display your workspace name in the Account field with a green connected indicator. The 'Channel', 'Text', and 'Blocks' fields will appear below.
Common mistake — If you authenticate as yourself (user token) instead of installing a Slack bot, the message will appear to come from your personal account. Use a dedicated Slack app/bot token so reports appear from a recognizable bot name like 'Weekly Reporter'.
7

Workflow Canvas > Slack Step > Configure > Channel / Blocks fields

Configure the Slack Message Destination

In the Channel field, type the name of your target channel — for example '#leadership-updates'. You can also paste a channel ID (starts with 'C') for more reliability. In the Blocks field, reference the output from your code step using the expression '{{steps.code.$return_value.slackBlocks}}'. Set the 'Text' fallback field to something like 'Weekly Todoist Progress Report' for clients that don't render Block Kit.

  1. 1Type your channel name (e.g. '#leadership-updates') in the Channel field
  2. 2Click the Blocks field and switch to expression mode with '{{ }}'
  3. 3Enter '{{steps.code.$return_value.slackBlocks}}' to reference the formatted payload
  4. 4Set the Text fallback field to 'Weekly Todoist Progress Report'
What you should see: The Slack step configuration should show your channel name, the dynamic Blocks expression referencing your code step output, and the fallback text filled in.
Common mistake — Channel names work for public channels. For private channels, paste the channel ID instead of the name — the Slack API will reject private channel names with a 'channel_not_found' error even when the channel exists.
8

Workflow Canvas > Run Now button (top right)

Test the Full Workflow End-to-End

Click 'Run Now' at the top of the workflow canvas to trigger a manual test run (this bypasses the Monday schedule and fires immediately). Watch each step turn green in sequence. Click the Todoist step to inspect the raw API response — confirm you see completed tasks in the 'items' array. Click the code step to verify the aggregated output. Click the Slack step to confirm it shows a 200 OK response from Slack.

  1. 1Click the 'Run Now' button at the top right of the canvas
  2. 2Watch each step indicator — green means success, red means error
  3. 3Click the Todoist step result to inspect the raw 'items' array
  4. 4Click the code step result to verify 'totalCompleted' and 'byProject' values
  5. 5Check your Slack channel to confirm the message appeared
What you should see: All steps should show green checkmarks. Your Slack channel should contain a formatted message with project-by-project completion counts and a weekly total within 30 seconds of clicking Run Now.
Pipedream
▶ Deploy & test
executed
Slack
Todoist
Todoist
🔔 notification
received
9

Workflow Canvas > Node.js Code Step > Edit code

Add Error Handling to the Code Step

Go back to your Node.js code step. Wrap the main logic in a try/catch block. In the catch block, use $.send.http or log the error — but more practically, add a fallback Slack notification so leadership knows the report failed instead of just silently not appearing. Update the step to export an error flag if the Todoist response is empty or malformed. This prevents silent failures on weeks where the API is slow.

  1. 1Click the code step to open the editor
  2. 2Wrap your main logic in a try/catch block
  3. 3In the catch block, throw a descriptive error with $.flow.exit() or re-throw to halt the workflow
  4. 4Add a check for empty 'items' array and return an early 'No completed tasks this week' message
  5. 5Click 'Test' again to confirm the updated code still passes
What you should see: The code step should now handle empty results gracefully — posting a 'No completed tasks found this week' Slack message rather than failing or posting a blank report.

Paste this into the Node.js code step (Step 3 in your workflow). It pulls the completed tasks array from the Todoist step output, resolves project IDs to names using a second Todoist API call, groups and counts by project, then builds a Slack Block Kit payload ready to drop into the Slack step's Blocks field.

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, $ }) {
    // Pull completed tasks from previous Todoist step
    const completedItems = steps.todoist.$return_value?.items || [];

    if (completedItems.length === 0) {
      // Post a fallback message rather than an empty report
      return {
        totalCompleted: 0,
        byProject: [],
        slackBlocks: [
          {
            type: 'header',
            text: { type: 'plain_text', text: '📋 Weekly Progress Report', emoji: true }
          },
          {
            type: 'section',
            text: { type: 'mrkdwn', text: 'No tasks were completed in Todoist this week.' }
          }
        ]
      };
    }

    // Fetch project names from Todoist API to resolve IDs to labels
    const todoist_token = process.env.TODOIST_API_TOKEN;
    let projectMap = {};
    try {
      const projectsResp = await axios.get('https://api.todoist.com/rest/v2/projects', {
        headers: { Authorization: `Bearer ${todoist_token}` }
      });
      for (const project of projectsResp.data) {
        projectMap[project.id] = project.name;
      }
    } catch (err) {
      console.error('Failed to fetch Todoist projects:', err.message);
      // Continue with raw IDs if lookup fails
    }

    // Group completions by project
    const projectCounts = {};
    for (const item of completedItems) {
      const projectName = projectMap[item.project_id] || `Project ${item.project_id}`;
      projectCounts[projectName] = (projectCounts[projectName] || 0) + 1;
    }

    const totalCompleted = completedItems.length;
    const byProject = Object.entries(projectCounts)
      .sort((a, b) => b[1] - a[1])
      .map(([name, count]) => ({ name, count }));

    // Build date range label
    const now = new Date();
    const lastMonday = new Date(now);
    lastMonday.setDate(now.getDate() - 7);
    const dateLabel = `${lastMonday.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}–${now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;

    // Build Slack Block Kit payload
    const projectLines = byProject
      .map(p => `• *${p.name}* — ${p.count} task${p.count !== 1 ? 's' : ''} completed`)
      .join('\n');

    const slackBlocks = [
      {
        type: 'header',
        text: { type: 'plain_text', text: `📋 Weekly Progress Report — ${dateLabel}`, emoji: true }
      },
      { type: 'divider' },
      {
        type: 'section',
        text: { type: 'mrkdwn', text: projectLines }
      },
      { type: 'divider' },
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `*Total completed this week: ${totalCompleted} task${totalCompleted !== 1 ? 's' : ''}*` }
      },
      {
        type: 'context',
        elements: [{ type: 'mrkdwn', text: `Generated automatically by Pipedream · ${new Date().toISOString()}` }]
      }
    ];

    return { totalCompleted, byProject, slackBlocks };
  }
});
10

Workflow Canvas > Active/Inactive toggle (top right)

Activate the Workflow

Toggle the workflow from 'Inactive' to 'Active' using the switch in the top right of the canvas. Pipedream will confirm activation and show the next scheduled run time. Verify the next run shows the upcoming Monday at your configured time. You can also check the 'Runs' tab to see historical executions once the first scheduled run fires.

  1. 1Click the toggle switch at the top right of the canvas from 'Inactive' to 'Active'
  2. 2Confirm the dialog if prompted
  3. 3Check the trigger block — it should display the next scheduled run time
  4. 4Navigate to the 'Runs' tab to monitor future executions
What you should see: The workflow status should show 'Active' in green. The Schedule trigger block should display the next run timestamp, e.g. 'Next run: Mon Jan 13 at 08:00 UTC'.

Scaling Beyond 200+ tasks completed per week across all Todoist projects+ Records

If your volume exceeds 200+ tasks completed per week across all Todoist projects records, apply these adjustments.

1

Paginate the Todoist API Manually

The Todoist completed tasks endpoint returns at most 200 items per call. Replace the single Todoist step with a Node.js code step that loops — fetching 200 items, incrementing an offset, and continuing until the response returns fewer than 200. This is the only reliable way to capture full week data for active teams.

2

Filter by Project Before Aggregating

If your team uses 10+ projects but leadership only cares about 4, pass a project_id filter to each paginated API call. This reduces response size and speeds up the code step. Fetch each project in parallel using Promise.all() rather than sequential await calls.

3

Cache Project Name Lookups

For teams with many projects, hitting the /projects endpoint on every weekly run is wasteful. Store the project ID-to-name map in a Pipedream Data Store (pipedream.com > Data Stores) and refresh it only when a lookup fails. This cuts one full API call per run.

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 Pipedream for this if your team has someone who can write or read basic JavaScript. The scheduled trigger is reliable — Pipedream's cron infrastructure has better uptime than Make's scheduler in practice — and the Node.js code step lets you call the Todoist API directly, paginate if needed, and build a proper Block Kit payload without fighting a visual interface. The one case where you'd pick something else: if nobody on the team touches code at all, Make's visual aggregator modules are easier to reason about without writing a line.

Cost

Cost math: this workflow runs once per week = 52 times per year. Each run touches 3 steps (Schedule trigger + Todoist fetch + Slack post) plus your code step = roughly 4-5 credits per run. That's 260 credits per year. Pipedream's free tier gives you 10,000 credits per month. You will not pay a cent for this workflow on Pipedream's free plan. Make's free tier allows 1,000 operations per month and charges per operation, so the same workflow costs 4-5 operations/run × 52 runs = ~260 operations/year — also free, but Make's free tier caps at 1,000/month total across all workflows, so if you have other automations running, you'll hit that wall faster.

Tradeoffs

Make handles this use case with a visual aggregator module that non-developers can configure — no code required to group tasks by project. Zapier has a 'Digest by Zapier' action that batches items, but it's clunky and limited to one digest per trigger, which makes cross-project aggregation painful. n8n's 'Summarize' node handles grouping cleanly and the self-hosted version is free at any volume, making it the better pick for privacy-conscious teams or those running 50+ workflows. Power Automate can do this but requires SharePoint or Dataverse for intermediate data storage, which adds unnecessary complexity. Pipedream wins here specifically because the code step handles the aggregation logic cleanly in one place, and the Slack Block Kit output is trivial to assemble in JavaScript compared to any visual tool.

Three things you'll hit after setup: First, Todoist's completed tasks API returns project_id as a numeric string, not a project name — if you forget to resolve it, your Slack report shows 'Project 2301847562' instead of 'Marketing'. Always make the secondary /projects call. Second, if your team uses Todoist's recurring tasks feature, completed recurring tasks appear in the API with the same task ID on each completion — so if someone completes the same recurring daily task 5 times in a week, it shows as one ID, not five. Use completed_at timestamps to count actual completions. Third, Slack's Block Kit has a 50-block limit per message. If you have more than ~20 projects, your blocks array will silently truncate — test with real data before going live.

Ideas for what to build next

  • Add Per-Person Task BreakdownExtend the code step to group completions by Todoist user ID, then resolve user IDs to names via the Todoist /users endpoint. This gives leadership a per-person completion count alongside the project breakdown.
  • Archive Reports to a Notion or Google SheetAdd a fourth step after the Slack post that writes the weekly totals (date range, project counts, total) to a Google Sheet or Notion database. After 12 weeks you'll have a trend dataset showing team velocity over time.
  • Send a Friday Reminder if the Week Looks LightAdd a second Pipedream workflow that runs every Friday afternoon, checks the week-to-date completion count, and posts a nudge to the team Slack channel if the count is below a threshold you define.

Related guides

Was this guide helpful?
Slack + Todoist overviewPipedream profile →