Home/ Workflows/ Project Management/ Team Communication
Workflow · Project Management

n8n Workflow for Project Milestone Slack Notifications

Automate project milestone tracking and team notifications in Slack to keep stakeholders informed of project progress and achievements in real-time.

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

Prerequisites

Data Requirements

Node-by-Node Build Instructions

Step 1: Set Up the Webhook Trigger

  1. Add a Webhook node as your starting trigger
  2. Set the HTTP Method to “POST”
  3. Configure the webhook URL path as “/project-milestone”
  4. Enable “Respond to Webhook” option
  5. Set response code to 200 for successful processing

Step 2: Parse and Validate Milestone Data

  1. Add a Code node after the webhook
  2. Name it “Process Milestone Data”
  3. 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

  1. Add an IF node to check milestone status
  2. Set condition to check if completionPercentage equals 100
  3. Label branches as “Milestone Completed” and “Progress Update”

Step 4: Format Completion Message (True Branch)

  1. Add a Code node on the True branch
  2. Name it “Format Completion Message”
  3. 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)

  1. Add a Code node on the False branch
  2. Name it “Format Progress Message”
  3. 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

  1. Add a Slack node after both message formatting nodes
  2. Set Resource to “Message” and Operation to “Post”
  3. Configure Slack credentials with your Bot Token
  4. Map the channel field: {{ $json.channel }}
  5. Map the text field: {{ $json.text }}
  6. Set “Send as” to “Bot”
  7. Enable “Blocks” and map: {{ $json.blocks }}

Step 7: Log Notification Activity

  1. Add a Code node after Slack
  2. Name it “Log Activity”
  3. 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

  1. Add error handling to the webhook response
  2. Configure appropriate HTTP status codes
  3. Add retry logic for failed Slack messages
  4. 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}];"
      },