n8n Workflow for Project Milestone Slack Notifications
Why This Automation Matters
Project milestone communication is critical for maintaining team alignment and stakeholder engagement. Manual milestone notifications often lead to delayed updates, inconsistent messaging, and missed celebrations of team achievements. This n8n workflow automates the entire process of tracking project milestones and sending formatted notifications to your Slack channels.
The automation ensures that every milestone completion is immediately communicated to relevant team members, includes progress visualization, and maintains a consistent communication standard across all projects. This reduces administrative overhead while improving transparency and team morale through timely recognition of achievements.
What You Need Before Starting
Required Integrations
- Slack App with Bot Token permissions for posting messages
- Project Management Tool (Airtable, Monday.com, or similar) with milestone data
- n8n Cloud or Self-hosted instance with webhook capabilities
Prerequisites
- Active Slack workspace with channel creation permissions
- Project management system with milestone tracking fields
- Basic understanding of Slack channel structure and mentions
- API credentials for your project management platform
Data Requirements
- Project name and ID
- Milestone title and description
- Completion percentage
- Due dates and completion dates
- Assigned team members and stakeholders
- Project priority level
Node-by-Node Build Instructions
Step 1: Set Up the Webhook Trigger
- Add a Webhook node as your starting trigger
- Set the HTTP Method to “POST”
- Configure the webhook URL path as “/project-milestone”
- Enable “Respond to Webhook” option
- Set response code to 200 for successful processing
Step 2: Parse and Validate Milestone Data
- Add a Code node after the webhook
- Name it “Process Milestone Data”
- Configure the following JavaScript code:
const milestoneData = $input.first().json.body;
// Validate required fields
const requiredFields = ['projectName', 'milestoneTitle', 'completionPercentage', 'dueDate'];
const missingFields = requiredFields.filter(field => !milestoneData[field]);
if (missingFields.length > 0) {
throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
}
// Format data for Slack message
return [{
json: {
projectName: milestoneData.projectName,
milestoneTitle: milestoneData.milestoneTitle,
description: milestoneData.description || '',
completionPercentage: parseInt(milestoneData.completionPercentage),
dueDate: milestoneData.dueDate,
completedDate: milestoneData.completedDate || new Date().toISOString(),
assignedTeam: milestoneData.assignedTeam || [],
priority: milestoneData.priority || 'medium',
projectId: milestoneData.projectId || 'unknown'
}
}];
Step 3: Determine Notification Type
- Add an IF node to check milestone status
- Set condition to check if
completionPercentageequals 100 - Label branches as “Milestone Completed” and “Progress Update”
Step 4: Format Completion Message (True Branch)
- Add a Code node on the True branch
- Name it “Format Completion Message”
- Add this code:
const data = $input.first().json;
const teamMentions = data.assignedTeam.map(member => `<@${member}>`).join(' ');
const progressBar = '🟢'.repeat(10); // Full green bar for completion
const priorityEmoji = {
'high': '🔴',
'medium': '🟡',
'low': '🟢'
}[data.priority] || '🟡';
return [{
json: {
channel: '#project-updates',
text: `🎉 Milestone Completed: ${data.milestoneTitle}`,
blocks: [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🎉 Milestone Completed!"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": `*Project:* ${data.projectName}`
},
{
"type": "mrkdwn",
"text": `*Milestone:* ${data.milestoneTitle}`
},
{
"type": "mrkdwn",
"text": `*Priority:* ${priorityEmoji} ${data.priority.toUpperCase()}`
},
{
"type": "mrkdwn",
"text": `*Completed:* ${new Date(data.completedDate).toLocaleDateString()}`
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `*Progress:* ${progressBar} 100%`
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `*Description:* ${data.description || 'No description provided'}`
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `*Team:* ${teamMentions || 'No team members assigned'}`
}
}
]
}
}];
Step 5: Format Progress Message (False Branch)
- Add a Code node on the False branch
- Name it “Format Progress Message”
- Add this code:
const data = $input.first().json;
const teamMentions = data.assignedTeam.map(member => `<@${member}>`).join(' ');
const completed = Math.floor(data.completionPercentage / 10);
const remaining = 10 - completed;
const progressBar = '🟢'.repeat(completed) + '⚪'.repeat(remaining);
const priorityEmoji = {
'high': '🔴',
'medium': '🟡',
'low': '🟢'
}[data.priority] || '🟡';
const daysUntilDue = Math.ceil((new Date(data.dueDate) - new Date()) / (1000 * 60 * 60 * 24));
const dueDateStatus = daysUntilDue < 0 ? '⚠️ OVERDUE' :
daysUntilDue <= 3 ? '🟡 Due Soon' : '✅ On Track';
return [{
json: {
channel: '#project-updates',
text: `📊 Milestone Progress Update: ${data.milestoneTitle}`,
blocks: [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "📊 Milestone Progress Update"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": `*Project:* ${data.projectName}`
},
{
"type": "mrkdwn",
"text": `*Milestone:* ${data.milestoneTitle}`
},
{
"type": "mrkdwn",
"text": `*Priority:* ${priorityEmoji} ${data.priority.toUpperCase()}`
},
{
"type": "mrkdwn",
"text": `*Due Date:* ${new Date(data.dueDate).toLocaleDateString()}`
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `*Progress:* ${progressBar} ${data.completionPercentage}%`
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `*Status:* ${dueDateStatus} (${daysUntilDue} days)`
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `*Team:* ${teamMentions || 'No team members assigned'}`
}
}
]
}
}];
Step 6: Send Slack Notification
- Add a Slack node after both message formatting nodes
- Set Resource to “Message” and Operation to “Post”
- Configure Slack credentials with your Bot Token
- Map the channel field:
{{ $json.channel }} - Map the text field:
{{ $json.text }} - Set “Send as” to “Bot”
- Enable “Blocks” and map:
{{ $json.blocks }}
Step 7: Log Notification Activity
- Add a Code node after Slack
- Name it “Log Activity”
- Configure logging code:
const slackResponse = $input.first().json;
const originalData = $('Process Milestone Data').first().json;
console.log(`Milestone notification sent successfully:
Project: ${originalData.projectName}
Milestone: ${originalData.milestoneTitle}
Progress: ${originalData.completionPercentage}%
Slack Message ID: ${slackResponse.ts}
Channel: ${slackResponse.channel}`);
return [{
json: {
status: 'success',
notificationSent: true,
timestamp: new Date().toISOString(),
projectId: originalData.projectId,
milestoneTitle: originalData.milestoneTitle,
slackMessageId: slackResponse.ts
}
}];
Step 8: Error Handling
- Add error handling to the webhook response
- Configure appropriate HTTP status codes
- Add retry logic for failed Slack messages
- Set up fallback notification methods
Complete Workflow JSON
{
"meta": {
"instanceId": "project-milestone-notifications"
},
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "project-milestone",
"responseMode": "responseNode",
"options": {}
},
"name": "Milestone Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [240, 300],
"id": "webhook-milestone"
},
{
"parameters": {
"jsCode": "const milestoneData = $input.first().json.body;\n\n// Validate required fields\nconst requiredFields = ['projectName', 'milestoneTitle', 'completionPercentage', 'dueDate'];\nconst missingFields = requiredFields.filter(field => !milestoneData[field]);\n\nif (missingFields.length > 0) {\n throw new Error(`Missing required fields: ${missingFields.join(', ')}`);\n}\n\n// Format data for Slack message\nreturn [{\n json: {\n projectName: milestoneData.projectName,\n milestoneTitle: milestoneData.milestoneTitle,\n description: milestoneData.description || '',\n completionPercentage: parseInt(milestoneData.completionPercentage),\n dueDate: milestoneData.dueDate,\n completedDate: milestoneData.completedDate || new Date().toISOString(),\n assignedTeam: milestoneData.assignedTeam || [],\n priority: milestoneData.priority || 'medium',\n projectId: milestoneData.projectId || 'unknown'\n }\n}];"
},
"name": "Process Milestone Data",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [460, 300],
"id": "process-data"
},
{
"parameters": {
"conditions": {
"number": [
{
"value1": "={{ $json.completionPercentage }}",
"operation": "equal",
"value2": 100
}
]
}
},
"name": "Check Completion Status",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [680, 300],
"id": "check-completion"
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst teamMentions = data.assignedTeam.map(member => `<@${member}>`).join(' ');\n\nconst progressBar = '🟢'.repeat(10);\nconst priorityEmoji = {\n 'high': '🔴',\n 'medium': '🟡',\n 'low': '🟢'\n}[data.priority] || '🟡';\n\nreturn [{\n json: {\n channel: '#project-updates',\n text: `🎉 Milestone Completed: ${data.milestoneTitle}`,\n blocks: [\n {\n \"type\": \"header\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \"🎉 Milestone Completed!\"\n }\n },\n {\n \"type\": \"section\",\n \"fields\": [\n {\n \"type\": \"mrkdwn\",\n \"text\": `*Project:* ${data.projectName}`\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": `*Milestone:* ${data.milestoneTitle}`\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": `*Priority:* ${priorityEmoji} ${data.priority.toUpperCase()}`\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": `*Completed:* ${new Date(data.completedDate).toLocaleDateString()}`\n }\n ]\n },\n {\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": `*Progress:* ${progressBar} 100%`\n }\n },\n {\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": `*Description:* ${data.description || 'No description provided'}`\n }\n },\n {\n \"type\": \"section\",\n \"text\": {\n \"type\": \"mrkdwn\",\n \"text\": `*Team:* ${teamMentions || 'No team members assigned'}`\n }\n }\n ]\n }\n}];"
},