Workflow 002: Task Automation
Fresh Last Updated: January 2025| Field | Value |
|---|---|
| Workflow ID | WF-002 |
| Version | 1.0 |
| Complexity | Medium |
| Estimated Setup | 1-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.)
Recommended Task Database Schema
| Property | Type | Purpose |
|---|---|---|
| Task | Title | Task name |
| Status | Select | To Do, In Progress, Review, Done, Archived |
| Priority | Select | Low, Medium, High, Urgent |
| Assignee | People | Who's responsible |
| Due Date | Date | When it's due |
| Category | Select | Engineering, Design, Marketing, etc. |
| Project | Relation | Link to projects database |
| Estimated Hours | Number | Time estimate |
| Actual Hours | Number | Time spent |
| Created | Created time | Auto timestamp |
| Last Updated | Last edited time | Auto 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 axiosCreate .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.comStep 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-stoppedPM2 (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 startupVerification 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
| Issue | Cause | Solution |
|---|---|---|
| No notifications | Missing webhook/API key | Check environment variables |
| Wrong tasks selected | Filter mismatch | Verify filter conditions |
| Rate limit errors | Too many requests | Add delays between operations |
| Cron not running | Timezone issues | Use explicit timezone in cron |