WorkflowsMCP

Weekly workload digest: Todoist + Calendar → Slack

Task load meets meeting load: one ranked digest per teammate, posted to Slack every Friday.

Task counts alone don't show who's overloaded — three tasks and twelve hours of meetings is worse than ten tasks and an open calendar. This pulls each teammate's overdue-and-due-this-week count from Todoist, cross-references it with busy hours from Google Calendar, and pushes one ranked digest to Slack every Friday, before the team plans next week. Todoist's own People Tab shows per-person counts on free and paid plans, but it's a view you open, with no meeting data or push; this adds the calendar half and lands it in Slack automatically.

How it flows

  1. 01

    Weekly trigger fires

    Every Friday at 16:00 in the team's timezone, the digest job kicks off. Nothing app-specific here — wire it to whatever scheduler drives this workflow.

  2. 02

    Team roster pulled from Todoist

    Collaborators on the shared project returns every teammate's email and user_id — the one authoritative roster, with nothing to maintain separately.

  3. 03

    Per-person task load pulled

    For each person on the roster, the filter query returns their overdue-plus-due-this-week count.

  4. 04

    Per-person busy hours pulled

    The same roster's emails go into one freeBusy.query call; each person's busy[] intervals are summed into hours busy for the coming week.

  5. 05

    Rows ranked and assembled into Block Kit

    People are ordered by whatever mix of overdue count and busy hours the team cares about most — this workflow leaves the exact weighting to the team — into one header plus one section-with-fields block per person, closed with a footer noting the data cutoff.

  6. 06

    Digest posted to Slack

    chat.postMessage delivers the finished digest to the target channel once, well under its per-channel rate limit.

Set up each app

Work through these in order — later apps usually need a token or an id from an earlier one.

Todoist

Per-person task load: overdue plus due this week

  1. 01

    Get a read-only API token

    Settings → Integrations → Developer → copy your personal API token, or register an OAuth app and request only `data:read` if this should run under a service account rather than your own login. `data:read` covers both tasks and collaborators, and this workflow never writes back to Todoist. (Todoist's own People Tab — available on Beginner and Business team plans, not Business-exclusive — already shows per-person task/overdue counts, but only as something you open inside the app: no meeting data, no push. See the workflow description for the full boundary.)

  2. 02

    Find the shared project and pull the roster

    Grab the project_id of the team's shared project from its URL, or from GET /api/v1/projects. Calling collaborators on that project id returns every teammate's email and user_id in one shot — that email is the join key against Google Calendar's freebusy response later, so there is no separate roster to keep in sync by hand.

    GET /api/v1/projects/{project_id}/collaborators
    curl -s "https://api.todoist.com/api/v1/projects/6X6WMMqgq2PWxjCX/collaborators" \
      -H "Authorization: Bearer $TODOIST_TOKEN"
  3. 03

    Build the per-person filter query

    For each collaborator, query overdue-plus-due-this-week tasks with Todoist's own filter language — no client-side date math needed. `od` covers everything already overdue, `7 days` covers what's due before next Friday; `&`/`|` combine them. Use `/api/v1/` only: REST v2 and Sync v9 both return 410 Gone now, so there is nothing to fall back to on those paths.

    GET /api/v1/tasks?filter=…
    GET https://api.todoist.com/api/v1/tasks?filter=assigned%20to%3A%20jane%40example.com%20%26%20(od%20%7C%207%20days)
    Authorization: Bearer $TODOIST_TOKEN

Google Calendar

Per-person busy hours for the coming week

  1. 01

    Request the freebusy-only scope

    Create an OAuth client and request only `https://www.googleapis.com/auth/calendar.freebusy`. It returns busy/free blocks and nothing else — no event titles, no attendee lists — which keeps this workflow from needing every teammate's full calendar-read consent just to produce an hours-busy total.

  2. 02

    Confirm internal sharing is at least free/busy

    freeBusy.query only sees a calendar that is actually shared with the requester. Workspace admins check Admin console → Apps → Google Workspace → Calendar → Sharing settings and confirm internal sharing isn't set to "No sharing" — Google does not document what a newly created org defaults to, so check rather than assume. Anyone outside that org default (or not on a Workspace domain at all) has to share individually: My calendars → the calendar's ⋮ → Settings and sharing → Shared with → add the requesting account at "See only free/busy". Do this for every teammate before the first run, or their row in the digest comes back empty with no error to explain why.

  3. 03

    Query the week's busy blocks for the whole roster

    One freeBusy.query call covers up to 50 calendars (the calendarExpansionMax ceiling), so a starter-sized team never needs to paginate this. Sum each person's busy[] intervals into total hours. Note that "busy" includes anything marked opaque — Focus time and out-of-office blocks count the same as meetings — so label the digest column "busy hours," not "meeting hours," unless the team is prepared to switch to events.list with an attendee-count filter instead, which needs the broader calendar.events.readonly scope this workflow deliberately avoids.

    POST /calendar/v3/freeBusy
    {
      "timeMin": "2026-08-10T00:00:00-04:00",
      "timeMax": "2026-08-17T00:00:00-04:00",
      "timeZone": "America/New_York",
      "items": [
        { "id": "jane@example.com" },
        { "id": "bob@example.com" }
      ]
    }

Slack

Delivers the ranked digest to the team channel

  1. 01

    Create the app and add posting scopes

    api.slack.com/apps → Create New App → From scratch → OAuth & Permissions → Bot Token Scopes: add `chat:write` (also add `chat:write.public` if the digest needs to post to a public channel the bot hasn't been invited to yet). Install/reinstall the app to the workspace.

  2. 02

    Invite the bot to the target channel

    /invite @<bot-name> in the channel the digest should land in, e.g. #team-ops. Slack's API reference now lives at docs.slack.dev — api.slack.com/methods and api.slack.com/block-kit redirect there as of this check, so bookmark the new domain for anything you look up later.

  3. 03

    Resolve emails to Slack user ids (optional, for @-mentions)

    Add `users:read.email` only if the digest should @-mention each person by name rather than post a channel-wide summary; skip it entirely if a plain roster post is enough, which keeps setup shorter for a starter workflow.

    GET users.lookupByEmail
    curl -s -G "https://slack.com/api/users.lookupByEmail" \
      -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
      -d email=jane@example.com
  4. 04

    Send a test digest to check the layout

    Post a one-person sample before wiring up the real weekly job, to confirm the format survives Block Kit's limits: 50 blocks per message, 10 fields per section, 2000 characters per field. A starter-sized team fits comfortably in one message — a header, one section-with-fields block per person, a closing divider, and a context footer citing the data cutoff.

    POST /api/chat.postMessage
    {
      "channel": "#team-ops",
      "text": "Weekly Capacity Digest",
      "blocks": [
        { "type": "header", "text": { "type": "plain_text", "text": "Weekly Capacity Digest — week of Aug 10" } },
        {
          "type": "section",
          "fields": [
            { "type": "mrkdwn", "text": "*Jane Doe*" },
            { "type": "mrkdwn", "text": "3 overdue · 6 due this week · 14h busy" }
          ]
        },
        { "type": "divider" },
        { "type": "context", "elements": [{ "type": "mrkdwn", "text": "Task counts via Todoist · busy hours via Google Calendar freebusy · generated Fri 16:00" }] }
      ]
    }