Skip to content

Workflow 002: Task Automation

Fresh Last Updated: January 2025
FieldValue
Workflow IDWF-002
Version1.0
ComplexityMedium
Estimated Setup1-3 hours

Overview

Automate task management in Notion databases with status updates, due date reminders, assignments, and external notifications. Build a complete task management workflow with Slack/email integrations.

Use Cases

  • Auto-assign tasks based on category or workload
  • Send Slack notifications when tasks are assigned
  • Email reminders for upcoming due dates
  • Auto-archive completed tasks after X days
  • Generate weekly task reports
  • Track task metrics and SLAs

Architecture

Prerequisites

  • [ ] Notion database with task schema (below)
  • [ ] Completed SOP 002: Authentication
  • [ ] Optional: Slack webhook URL
  • [ ] Optional: Email service (SendGrid, etc.)
PropertyTypePurpose
TaskTitleTask name
StatusSelectTo Do, In Progress, Review, Done, Archived
PrioritySelectLow, Medium, High, Urgent
AssigneePeopleWho's responsible
Due DateDateWhen it's due
CategorySelectEngineering, Design, Marketing, etc.
ProjectRelationLink to projects database
Estimated HoursNumberTime estimate
Actual HoursNumberTime spent
CreatedCreated timeAuto timestamp
Last UpdatedLast edited timeAuto timestamp

Implementation

Step 1: Project Setup

bash
mkdir notion-task-automation
cd notion-task-automation
npm init -y
npm install @notionhq/client dotenv node-cron axios

Create .env:

env
NOTION_API_KEY=secret_xxxxx
NOTION_DATABASE_ID=xxxxx
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxxxx
SENDGRID_API_KEY=xxxxx
FROM_EMAIL=notifications@yourcompany.com

Step 2: Task Manager Class

javascript
// src/task-manager.js
const { Client, isNotionClientError, APIErrorCode } = require('@notionhq/client');
require('dotenv').config();

class TaskManager {
  constructor() {
    this.notion = new Client({ auth: process.env.NOTION_API_KEY });
    this.databaseId = process.env.NOTION_DATABASE_ID;
  }

  async queryTasks(filter = {}, sorts = []) {
    const tasks = [];
    let cursor = undefined;

    do {
      const response = await this.notion.databases.query({
        database_id: this.databaseId,
        filter,
        sorts,
        start_cursor: cursor,
        page_size: 100
      });

      tasks.push(...response.results.map(this.extractTaskData));
      cursor = response.has_more ? response.next_cursor : undefined;
    } while (cursor);

    return tasks;
  }

  extractTaskData(page) {
    const props = page.properties;

    return {
      id: page.id,
      url: page.url,
      task: props.Task?.title?.[0]?.plain_text || 'Untitled',
      status: props.Status?.select?.name || null,
      priority: props.Priority?.select?.name || null,
      assignee: props.Assignee?.people?.[0] || null,
      assigneeName: props.Assignee?.people?.[0]?.name || null,
      assigneeEmail: props.Assignee?.people?.[0]?.person?.email || null,
      dueDate: props['Due Date']?.date?.start || null,
      category: props.Category?.select?.name || null,
      estimatedHours: props['Estimated Hours']?.number || null,
      actualHours: props['Actual Hours']?.number || null,
      created: page.created_time,
      lastUpdated: page.last_edited_time
    };
  }

  async updateTask(taskId, properties) {
    return this.notion.pages.update({
      page_id: taskId,
      properties
    });
  }

  async createTask(taskData) {
    return this.notion.pages.create({
      parent: { database_id: this.databaseId },
      properties: {
        Task: {
          title: [{ text: { content: taskData.task } }]
        },
        Status: taskData.status ? { select: { name: taskData.status } } : undefined,
        Priority: taskData.priority ? { select: { name: taskData.priority } } : undefined,
        'Due Date': taskData.dueDate ? { date: { start: taskData.dueDate } } : undefined,
        Category: taskData.category ? { select: { name: taskData.category } } : undefined
      }
    });
  }

  // Query helpers
  async getTasksDueSoon(days = 3) {
    const futureDate = new Date();
    futureDate.setDate(futureDate.getDate() + days);

    return this.queryTasks({
      and: [
        { property: 'Status', select: { does_not_equal: 'Done' } },
        { property: 'Status', select: { does_not_equal: 'Archived' } },
        { property: 'Due Date', date: { on_or_before: futureDate.toISOString() } },
        { property: 'Due Date', date: { is_not_empty: true } }
      ]
    }, [
      { property: 'Due Date', direction: 'ascending' }
    ]);
  }

  async getOverdueTasks() {
    return this.queryTasks({
      and: [
        { property: 'Status', select: { does_not_equal: 'Done' } },
        { property: 'Status', select: { does_not_equal: 'Archived' } },
        { property: 'Due Date', date: { before: new Date().toISOString() } }
      ]
    });
  }

  async getUnassignedTasks() {
    return this.queryTasks({
      and: [
        { property: 'Status', select: { does_not_equal: 'Done' } },
        { property: 'Assignee', people: { is_empty: true } }
      ]
    });
  }

  async getCompletedTasksOlderThan(days) {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - days);

    return this.queryTasks({
      and: [
        { property: 'Status', select: { equals: 'Done' } },
        { timestamp: 'last_edited_time', last_edited_time: { before: cutoffDate.toISOString() } }
      ]
    });
  }

  async getTasksByAssignee(userId) {
    return this.queryTasks({
      and: [
        { property: 'Assignee', people: { contains: userId } },
        { property: 'Status', select: { does_not_equal: 'Done' } }
      ]
    }, [
      { property: 'Priority', direction: 'descending' },
      { property: 'Due Date', direction: 'ascending' }
    ]);
  }
}

module.exports = TaskManager;

Step 3: Notification Services

javascript
// src/notifications.js
const axios = require('axios');

class NotificationService {
  constructor(config) {
    this.slackWebhookUrl = config.slackWebhookUrl;
    this.sendgridApiKey = config.sendgridApiKey;
    this.fromEmail = config.fromEmail;
  }

  async sendSlack(message, options = {}) {
    if (!this.slackWebhookUrl) {
      console.log('[Slack] No webhook configured, skipping');
      return;
    }

    try {
      await axios.post(this.slackWebhookUrl, {
        text: message,
        ...options
      });
      console.log('[Slack] Message sent');
    } catch (error) {
      console.error('[Slack] Error:', error.message);
    }
  }

  async sendSlackBlocks(blocks, text = '') {
    if (!this.slackWebhookUrl) return;

    try {
      await axios.post(this.slackWebhookUrl, {
        text,
        blocks
      });
    } catch (error) {
      console.error('[Slack] Error:', error.message);
    }
  }

  async sendEmail(to, subject, htmlContent) {
    if (!this.sendgridApiKey) {
      console.log('[Email] No API key configured, skipping');
      return;
    }

    try {
      await axios.post('https://api.sendgrid.com/v3/mail/send', {
        personalizations: [{ to: [{ email: to }] }],
        from: { email: this.fromEmail },
        subject,
        content: [{ type: 'text/html', value: htmlContent }]
      }, {
        headers: {
          'Authorization': `Bearer ${this.sendgridApiKey}`,
          'Content-Type': 'application/json'
        }
      });
      console.log(`[Email] Sent to ${to}`);
    } catch (error) {
      console.error('[Email] Error:', error.message);
    }
  }

  formatTaskForSlack(task) {
    const priority = {
      'Urgent': '🔴',
      'High': '🟠',
      'Medium': '🟡',
      'Low': '🟢'
    }[task.priority] || '⚪';

    const dueText = task.dueDate
      ? new Date(task.dueDate).toLocaleDateString()
      : 'No due date';

    return {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `${priority} *<${task.url}|${task.task}>*\nDue: ${dueText} | Assignee: ${task.assigneeName || 'Unassigned'}`
      }
    };
  }
}

module.exports = NotificationService;

Step 4: Automation Rules Engine

javascript
// src/automations.js
const TaskManager = require('./task-manager');
const NotificationService = require('./notifications');
require('dotenv').config();

const taskManager = new TaskManager();
const notifications = new NotificationService({
  slackWebhookUrl: process.env.SLACK_WEBHOOK_URL,
  sendgridApiKey: process.env.SENDGRID_API_KEY,
  fromEmail: process.env.FROM_EMAIL
});

// Automation: Send due date reminders
async function sendDueDateReminders() {
  console.log('🔔 Running due date reminders...');

  const tasksDueSoon = await taskManager.getTasksDueSoon(3);

  if (tasksDueSoon.length === 0) {
    console.log('  No tasks due soon');
    return;
  }

  // Group by assignee
  const byAssignee = {};
  for (const task of tasksDueSoon) {
    const key = task.assigneeEmail || 'unassigned';
    if (!byAssignee[key]) {
      byAssignee[key] = [];
    }
    byAssignee[key].push(task);
  }

  // Send Slack summary
  const blocks = [
    {
      type: 'header',
      text: { type: 'plain_text', text: '📅 Tasks Due Soon' }
    },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*${tasksDueSoon.length} tasks* due in the next 3 days`
      }
    },
    { type: 'divider' },
    ...tasksDueSoon.slice(0, 10).map(t => notifications.formatTaskForSlack(t))
  ];

  await notifications.sendSlackBlocks(blocks, 'Tasks due soon');

  // Send individual emails
  for (const [email, tasks] of Object.entries(byAssignee)) {
    if (email === 'unassigned') continue;

    const html = `
      <h2>Your Tasks Due Soon</h2>
      <p>You have ${tasks.length} task(s) due in the next 3 days:</p>
      <ul>
        ${tasks.map(t => `
          <li>
            <a href="${t.url}">${t.task}</a>
            - Due: ${new Date(t.dueDate).toLocaleDateString()}
            (${t.priority || 'No priority'})
          </li>
        `).join('')}
      </ul>
    `;

    await notifications.sendEmail(email, 'Tasks Due Soon', html);
  }

  console.log(`  Sent reminders for ${tasksDueSoon.length} tasks`);
}

// Automation: Alert on overdue tasks
async function alertOverdueTasks() {
  console.log('⚠️ Checking overdue tasks...');

  const overdue = await taskManager.getOverdueTasks();

  if (overdue.length === 0) {
    console.log('  No overdue tasks');
    return;
  }

  const blocks = [
    {
      type: 'header',
      text: { type: 'plain_text', text: '🚨 Overdue Tasks Alert' }
    },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*${overdue.length} tasks* are overdue!`
      }
    },
    { type: 'divider' },
    ...overdue.slice(0, 10).map(t => notifications.formatTaskForSlack(t))
  ];

  await notifications.sendSlackBlocks(blocks, 'Overdue tasks alert');
  console.log(`  Alerted ${overdue.length} overdue tasks`);
}

// Automation: Auto-archive old completed tasks
async function archiveOldCompletedTasks(daysOld = 30) {
  console.log('🗄️ Archiving old completed tasks...');

  const oldTasks = await taskManager.getCompletedTasksOlderThan(daysOld);

  if (oldTasks.length === 0) {
    console.log('  No tasks to archive');
    return;
  }

  let archived = 0;
  for (const task of oldTasks) {
    try {
      await taskManager.updateTask(task.id, {
        Status: { select: { name: 'Archived' } }
      });
      archived++;
      await sleep(350); // Rate limit
    } catch (error) {
      console.error(`  Error archiving ${task.id}:`, error.message);
    }
  }

  console.log(`  Archived ${archived} tasks`);
}

// Automation: Auto-assign unassigned tasks
async function autoAssignTasks(assignmentRules) {
  console.log('👤 Auto-assigning tasks...');

  const unassigned = await taskManager.getUnassignedTasks();

  if (unassigned.length === 0) {
    console.log('  No unassigned tasks');
    return;
  }

  let assigned = 0;
  for (const task of unassigned) {
    // Find matching rule
    const rule = assignmentRules.find(r =>
      r.category === task.category ||
      r.priority === task.priority
    );

    if (rule && rule.assigneeId) {
      try {
        await taskManager.updateTask(task.id, {
          Assignee: { people: [{ id: rule.assigneeId }] }
        });
        assigned++;

        // Notify assignee
        await notifications.sendSlack(
          `📋 New task assigned: *<${task.url}|${task.task}>*`
        );

        await sleep(350);
      } catch (error) {
        console.error(`  Error assigning ${task.id}:`, error.message);
      }
    }
  }

  console.log(`  Assigned ${assigned} tasks`);
}

// Automation: Weekly report
async function generateWeeklyReport() {
  console.log('📊 Generating weekly report...');

  const oneWeekAgo = new Date();
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);

  // Get completed this week
  const completed = await taskManager.queryTasks({
    and: [
      { property: 'Status', select: { equals: 'Done' } },
      { timestamp: 'last_edited_time', last_edited_time: { after: oneWeekAgo.toISOString() } }
    ]
  });

  // Get all active tasks
  const active = await taskManager.queryTasks({
    and: [
      { property: 'Status', select: { does_not_equal: 'Done' } },
      { property: 'Status', select: { does_not_equal: 'Archived' } }
    ]
  });

  const overdue = active.filter(t => t.dueDate && new Date(t.dueDate) < new Date());

  const report = `
📊 *Weekly Task Report*
Period: ${oneWeekAgo.toLocaleDateString()} - ${new Date().toLocaleDateString()}

*Summary:*
• ✅ Completed: ${completed.length} tasks
• 📋 Active: ${active.length} tasks
• ⚠️ Overdue: ${overdue.length} tasks

*By Priority:*
• 🔴 Urgent: ${active.filter(t => t.priority === 'Urgent').length}
• 🟠 High: ${active.filter(t => t.priority === 'High').length}
• 🟡 Medium: ${active.filter(t => t.priority === 'Medium').length}
• 🟢 Low: ${active.filter(t => t.priority === 'Low').length}

*Top Completers:*
${getTopCompleters(completed).map(c => `• ${c.name}: ${c.count}`).join('\n')}
  `;

  await notifications.sendSlack(report);
  console.log('  Report sent');
}

function getTopCompleters(tasks) {
  const counts = {};
  for (const task of tasks) {
    const name = task.assigneeName || 'Unassigned';
    counts[name] = (counts[name] || 0) + 1;
  }

  return Object.entries(counts)
    .map(([name, count]) => ({ name, count }))
    .sort((a, b) => b.count - a.count)
    .slice(0, 5);
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

module.exports = {
  sendDueDateReminders,
  alertOverdueTasks,
  archiveOldCompletedTasks,
  autoAssignTasks,
  generateWeeklyReport
};

Step 5: Scheduler Setup

javascript
// src/scheduler.js
const cron = require('node-cron');
const {
  sendDueDateReminders,
  alertOverdueTasks,
  archiveOldCompletedTasks,
  generateWeeklyReport
} = require('./automations');

console.log('🚀 Starting task automation scheduler...\n');

// Every day at 9 AM - Due date reminders
cron.schedule('0 9 * * *', async () => {
  console.log(`\n[${new Date().toISOString()}] Running daily reminders`);
  await sendDueDateReminders();
  await alertOverdueTasks();
});

// Every day at midnight - Archive old tasks
cron.schedule('0 0 * * *', async () => {
  console.log(`\n[${new Date().toISOString()}] Running nightly cleanup`);
  await archiveOldCompletedTasks(30);
});

// Every Monday at 9 AM - Weekly report
cron.schedule('0 9 * * 1', async () => {
  console.log(`\n[${new Date().toISOString()}] Running weekly report`);
  await generateWeeklyReport();
});

console.log('Scheduled jobs:');
console.log('  - Daily 9 AM: Due date reminders & overdue alerts');
console.log('  - Daily midnight: Archive old completed tasks');
console.log('  - Monday 9 AM: Weekly report');
console.log('\nWaiting for scheduled times...');

// Keep process running
process.on('SIGINT', () => {
  console.log('\nShutting down scheduler...');
  process.exit(0);
});

Step 6: CLI for Manual Triggers

javascript
// src/cli.js
const {
  sendDueDateReminders,
  alertOverdueTasks,
  archiveOldCompletedTasks,
  generateWeeklyReport
} = require('./automations');

const command = process.argv[2];

const commands = {
  reminders: sendDueDateReminders,
  overdue: alertOverdueTasks,
  archive: archiveOldCompletedTasks,
  report: generateWeeklyReport,
  all: async () => {
    await sendDueDateReminders();
    await alertOverdueTasks();
    await generateWeeklyReport();
  }
};

if (!command || !commands[command]) {
  console.log('Usage: node src/cli.js <command>');
  console.log('Commands: reminders, overdue, archive, report, all');
  process.exit(1);
}

commands[command]()
  .then(() => console.log('\n✅ Done'))
  .catch(err => {
    console.error('\n❌ Error:', err);
    process.exit(1);
  });

Deployment

Docker

dockerfile
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY src ./src

CMD ["node", "src/scheduler.js"]
yaml
# docker-compose.yml
version: '3.8'
services:
  task-automation:
    build: .
    env_file: .env
    restart: unless-stopped

PM2 (Process Manager)

javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'task-automation',
    script: 'src/scheduler.js',
    instances: 1,
    autorestart: true,
    max_memory_restart: '200M',
    env: {
      NODE_ENV: 'production'
    }
  }]
};
bash
pm2 start ecosystem.config.js
pm2 save
pm2 startup

Verification Checklist

  • [ ] Tasks query correctly from database
  • [ ] Due date reminders sent at scheduled time
  • [ ] Overdue alerts trigger correctly
  • [ ] Archive automation moves old tasks
  • [ ] Slack messages formatted properly
  • [ ] Email notifications delivered
  • [ ] Weekly reports accurate

Troubleshooting

IssueCauseSolution
No notificationsMissing webhook/API keyCheck environment variables
Wrong tasks selectedFilter mismatchVerify filter conditions
Rate limit errorsToo many requestsAdd delays between operations
Cron not runningTimezone issuesUse explicit timezone in cron

See Also

Built with VitePress | Powered by Claude AI