Intermediate~20 min setupCommunication & Project ManagementVerified April 2026
Slack logo
Asana logo

How to Send Asana Milestone Updates to Slack with n8n

Polls Asana every 5 minutes for completed milestones or approaching deadlines and posts a formatted status message to the relevant Slack channel automatically.

Steps and UI details are based on platform versions at time of writing — check each platform for the latest interface.

Best for

Engineering or product teams running multiple Asana projects who want channel-specific Slack alerts when milestones complete or deadlines are 24–48 hours out — without anyone writing a status update manually.

Not ideal for

Teams that need sub-minute Slack alerts — Asana's API does not support webhooks for task completion events in free plans, so if you need true real-time you'll need Asana Business or Enterprise with webhook support and a self-hosted n8n instance with a public URL.

Sync type

scheduled

Use case type

notification

Real-World Example

💡

A 22-person product team at a B2B SaaS company runs 6 active projects in Asana simultaneously. Before this workflow, the PM posted manual Slack summaries every Friday — milestone completions from Tuesday went unnoticed until the weekend update. Now n8n polls Asana every 5 minutes and drops a formatted message into #product-updates the moment a milestone is marked complete, including the assignee name and due date.

What Will This Cost?

Drag the slider to your expected monthly volume.

/mo
505005K50K

Each platform counts differently — Zapier: 1 task per trigger. Make: 1 operation per module per record. n8n: 1 execution per run.

Prices shown for annual billing. Based on published pricing as of April 2026.

Estimated ROI

1000

min saved/mo

$583

labor value/mo

Free

no platform cost

Based on ~2 min manual effort per operation at $35/hr fully loaded labor cost.

Implementation

Skip the setup

Import this workflow directly into n8n

Copy the pre-built n8n blueprint and paste it straight into n8n. All modules, filters, and field mappings are already configured — you just need to connect your accounts.

Before You Start

Make sure you have everything ready.

Asana Personal Access Token with access to the projects you want to monitor — generate at app.asana.com > Profile > Apps > Manage Developer Apps
Slack Bot Token (xoxb-) with scopes: chat:write, chat:write.public, channels:read, groups:read — create at api.slack.com/apps
n8n instance running v1.0 or later — self-hosted or n8n Cloud both work; self-hosted requires the Schedule Trigger to have a running queue
Asana project GID for each project you want to monitor — found in the URL at app.asana.com/0/PROJECT_GID/list
Slack channel IDs (not names) for each destination channel — run /invite @YourBot in any private channel before the workflow activates

Field Mapping

Map these fields between your apps.

FieldAPI Name
Required
Task Namename
Task GIDgid
Resource Subtyperesource_subtype
Completedcompleted
Due Datedue_on
Project Namememberships[0].project.name
Slack Channel ID
3 optional fields▸ show
Assignee Nameassignee.name
Completion Datecompleted_at
Notes / Descriptionnotes

Step-by-Step Setup

1

n8n Dashboard > Workflows > + New Workflow

Create a new n8n workflow

Open your n8n instance and click the '+ New Workflow' button in the top-left of the Workflows dashboard. Give it a clear name like 'Asana Milestones → Slack'. You'll land on a blank canvas with a single 'Add first step' node in the center. This is where you'll drop the trigger.

  1. 1Click '+ New Workflow' in the top-left corner
  2. 2Click the pencil icon next to 'My workflow' and rename it 'Asana Milestones → Slack'
  3. 3Click the large '+ Add first step' node on the canvas
What you should see: You should see a blank canvas with the node selection panel open on the right side of the screen.
2

Canvas > Add first step > Search 'Schedule' > Schedule Trigger

Add the Schedule trigger

In the node search panel, type 'Schedule' and select 'Schedule Trigger'. Set the interval to 'Every 5 Minutes'. This is your polling clock — every 5 minutes n8n will run the rest of the workflow to check Asana for changes. Asana's REST API does not push events to n8n without a publicly reachable webhook endpoint, so polling is the practical default for most self-hosted setups.

  1. 1Type 'Schedule' in the node search box
  2. 2Click 'Schedule Trigger' from the results
  3. 3Set 'Trigger Interval' to 'Minutes'
  4. 4Set 'Minutes Between Triggers' to 5
  5. 5Click 'Save' in the top-right
What you should see: The Schedule Trigger node appears on the canvas with a clock icon. The node panel shows 'Every 5 minutes' as the configured interval.
Common mistake — Do not set this lower than 5 minutes. Asana's API rate limit is 1,500 requests per minute across your entire token — but if you're running multiple workflows against the same Asana credentials, aggressive polling intervals will eat that budget fast.
n8n
+
click +
search apps
Slack
SL
Slack
Add the Schedule trigger
Slack
SL
module added
3

Canvas > + > Asana > Credentials > Create New > Personal Access Token

Connect your Asana account

Click the '+' button after the Schedule Trigger to add a new node. Search for 'Asana' and select it. Click 'Create New Credential' in the node panel. You'll be prompted to choose between OAuth2 and Personal Access Token — use Personal Access Token for self-hosted n8n unless you have a public callback URL configured for OAuth. Copy your Asana PAT from Asana > My Profile Settings > Apps > Manage Developer Apps.

  1. 1Click the '+' connector after the Schedule Trigger node
  2. 2Search 'Asana' and select the Asana node
  3. 3In the node panel, click the 'Credential for Asana API' dropdown
  4. 4Click 'Create New Credential'
  5. 5Paste your Asana Personal Access Token and click 'Save'
What you should see: The credential dropdown shows your new credential name with a green checkmark. The Asana node is now authorized.
Common mistake — If you're using a service account token instead of your personal token, make sure that service account is a member of every Asana project you want to monitor — otherwise those tasks simply won't appear in API responses.
n8n settings
Connection
Choose a connection…Add
click Add
Slack
Log in to authorize
Authorize n8n
popup window
Connected
green checkmark
4

Asana Node > Resource: Task > Operation: Get Many > Filters

Configure the Asana node to fetch tasks

In the Asana node, set Resource to 'Task' and Operation to 'Get Many'. Set 'Return All' to true only if your projects have fewer than 500 tasks — above that, enable pagination manually. Under Filters, add 'Project' and enter your Asana project GID (found in the URL of the project: app.asana.com/0/PROJECT_GID/list). Add a second filter: 'Completed Since' set to a dynamic expression that looks back 6 minutes so you catch anything completed since the last poll.

  1. 1Set 'Resource' to 'Task'
  2. 2Set 'Operation' to 'Get Many'
  3. 3Toggle 'Return All' to true
  4. 4Under 'Additional Fields', click 'Add Field' and select 'Project'
  5. 5Enter your Asana project GID in the Project field
  6. 6Click 'Add Field' again and select 'Completed Since', then enter the expression: {{ new Date(Date.now() - 6 * 60 * 1000).toISOString() }}
What you should see: When you click 'Test Step', you should see a list of Asana task objects returned in the output panel. Each task shows fields like gid, name, completed, due_on, assignee, and memberships.
Common mistake — The 'Completed Since' filter only returns tasks marked complete within that window. If your polling interval drifts or n8n restarts, you may miss completions that happened during downtime. Store the last successful run timestamp in a static data node or external store to avoid this.
5

Canvas > + after Asana node > Filter

Filter for milestone tasks only

Asana milestones are tasks where 'resource_subtype' equals 'milestone'. Add a Filter node after the Asana node to keep only those records. Without this filter, every completed task — subtasks, regular tasks, all of them — will post to Slack, which gets noisy fast. You can also add a second condition here for deadline proximity if you want 'approaching deadline' alerts alongside completion alerts.

  1. 1Click '+' after the Asana node and search 'Filter'
  2. 2Select 'Filter' from the list
  3. 3Click 'Add Condition'
  4. 4Set Field to {{ $json.resource_subtype }}
  5. 5Set Condition to 'equals'
  6. 6Set Value to 'milestone'
  7. 7Click 'Add Condition' again, set Field to {{ $json.completed }}, Condition to 'equals', Value to true
What you should see: After clicking 'Test Step', only milestone tasks marked complete should pass through. You'll see a count of matching items in the green output badge on the Filter node.
Common mistake — Filters are the most common place setups break. Double-check the field name and value exactly match what your app sends — a single capital letter difference will block everything.
Slack
SL
trigger
filter
Condition
matches criteria?
yes — passes through
no — skipped
Asana
AS
notified
6

Canvas > + parallel from Asana node > Filter > Merge node

Add a deadline proximity branch

To also catch milestones with deadlines approaching in the next 48 hours, add an 'OR' branch to your filter — or add a second Filter node connected in parallel from the Asana node. Set the condition to check if 'due_on' is within 2 days of today. Use the expression: {{ Math.ceil((new Date($json.due_on) - new Date()) / (1000 * 60 * 60 * 24)) }} and check if it is less than or equal to 2. Merge both branches with a Merge node before the Slack step.

  1. 1Click the '+' on the Asana node again to add a second parallel path
  2. 2Add a second Filter node
  3. 3Set condition: {{ $json.resource_subtype }} equals 'milestone'
  4. 4Add condition: {{ $json.completed }} equals false
  5. 5Add condition: {{ Math.ceil((new Date($json.due_on) - new Date()) / (1000*60*60*24)) }} less than or equal to 2
  6. 6Add a 'Merge' node after both Filter nodes and connect both branches to it
What you should see: The Merge node should show two input connectors — one from the completed milestone branch and one from the approaching deadline branch.
Common mistake — The Merge node in 'Append' mode will combine both streams. If the same milestone is both completed AND within 2 days of due date, it will appear twice in the output and post twice to Slack. Add a deduplication step using a Code node checking $json.gid if this matters for your team.
7

Canvas > + after Merge node > Code

Add a Code node to format the Slack message

Add a Code node after the Merge node. This is where you construct the Slack Block Kit message payload. You have access to all Asana task fields here: name, due_on, assignee, completed status, and the project name from the memberships array. The Code node runs once per item, so each milestone gets its own formatted message. Paste the code from the pro tip section below.

  1. 1Click '+' after the Merge node and search 'Code'
  2. 2Select the 'Code' node
  3. 3Set 'Mode' to 'Run Once For Each Item'
  4. 4Paste the JavaScript from the pro tip section into the code editor
  5. 5Click 'Test Step' to verify the output contains a formatted 'blocks' array
What you should see: Each item in the output panel should have a 'blocks' key containing a valid Slack Block Kit array, plus a 'channel' key with the target Slack channel ID.
Common mistake — The assignee field in Asana returns an object with 'gid' and 'name'. Use $json.assignee.name directly — don't try to use the gid as a display value or your Slack message will show a raw ID string.

Paste this into the Code node in Step 7. Set Mode to 'Run Once For Each Item'. It builds the full Slack Block Kit payload, resolves the correct Slack channel from a project-to-channel map, handles null assignees, formats dates, and flags whether this is a completion alert or a deadline warning — so your Slack messages are immediately readable without any extra config.

JavaScript — Code Node// n8n Code Node — Run Once For Each Item
▸ Show code
// n8n Code Node — Run Once For Each Item
// Maps Asana milestone data to a formatted Slack Block Kit message
const task = $input.item.json;

... expand to see full code

// n8n Code Node — Run Once For Each Item
// Maps Asana milestone data to a formatted Slack Block Kit message

const task = $input.item.json;

// Map Asana project GIDs to Slack channel IDs
// Update these with your actual project GIDs and channel IDs
const PROJECT_CHANNEL_MAP = {
  '1199000000001234': 'C045ABCDE12',  // Q1 Product Launch → #product-updates
  '1199000000005678': 'C067FGHIJ34',  // Platform Reliability → #eng-reliability
  'default': 'C089KLMNO56'             // #general-updates fallback
};

// Resolve project info from memberships array
const membership = task.memberships && task.memberships.length > 0
  ? task.memberships[0]
  : null;
const projectName = membership?.project?.name || 'Unknown Project';
const projectGid = membership?.project?.gid || 'default';

// Resolve Slack channel
const channel = PROJECT_CHANNEL_MAP[projectGid] || PROJECT_CHANNEL_MAP['default'];

// Resolve assignee safely
const assigneeName = task.assignee?.name || 'Unassigned';

// Format dates
const formatDate = (dateStr) => {
  if (!dateStr) return 'No date set';
  const d = new Date(dateStr);
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
};

// Calculate days until due
const daysUntilDue = task.due_on
  ? Math.ceil((new Date(task.due_on) - new Date()) / (1000 * 60 * 60 * 24))
  : null;

// Determine alert type
const isComplete = task.completed === true;
const headerText = isComplete ? '✅ Milestone Complete' : `⚠️ Milestone Due in ${daysUntilDue} Day${daysUntilDue === 1 ? '' : 's'}`;
const dateLabel = isComplete
  ? `📅 Completed: ${formatDate(task.completed_at)}`
  : `📅 Due: ${formatDate(task.due_on)}`;

// Build Block Kit blocks array
const blocks = [
  {
    type: 'header',
    text: { type: 'plain_text', text: headerText, emoji: true }
  },
  {
    type: 'section',
    text: {
      type: 'mrkdwn',
      text: `*${task.name}*\n📁 Project: ${projectName}\n👤 Owner: ${assigneeName}\n${dateLabel}`
    }
  }
];

// Add notes if present (truncate to 200 chars)
if (task.notes && task.notes.trim().length > 0) {
  const truncatedNotes = task.notes.length > 200
    ? task.notes.substring(0, 197) + '...'
    : task.notes;
  blocks.push({
    type: 'section',
    text: { type: 'mrkdwn', text: truncatedNotes }
  });
}

// Add divider
blocks.push({ type: 'divider' });

return { channel, blocks, taskGid: task.gid, projectName, isComplete };
message template
🔔 New Record: {{text}} {{user}}
channel: {{channel}}
ts: {{ts}}
#sales
🔔 New Record: Jane Smith
Company: Acme Corp
8

Canvas > + after Code node > Slack > Credentials > Create New > OAuth2

Connect your Slack account

Add a Slack node after the Code node. Click 'Create New Credential' and choose OAuth2. You'll need to create a Slack App at api.slack.com/apps with the scopes: chat:write, channels:read, and groups:read. Once your app is installed to your workspace, copy the Bot OAuth Token and paste it into n8n. The bot must be manually invited to any private channel you want to post to.

  1. 1Click '+' after the Code node and search 'Slack'
  2. 2Select the Slack node
  3. 3Click 'Credential for Slack API' dropdown and select 'Create New Credential'
  4. 4Paste your Slack Bot OAuth Token (starts with xoxb-)
  5. 5Click 'Save'
What you should see: The Slack credential shows a green checkmark. The Slack node panel is now active and ready to configure operations.
Common mistake — The bot token scope chat:write.public lets your bot post to public channels without being a member. Without it, you must /invite @YourBot to every channel first. Add this scope in your Slack App settings under OAuth & Permissions to save yourself repeated invitations.
9

Slack Node > Resource: Message > Operation: Post

Configure the Slack message action

In the Slack node, set Resource to 'Message' and Operation to 'Post'. For 'Channel', use the expression {{ $json.channel }} to pull the channel ID from your Code node output. Set 'Message Type' to 'Block'. In the 'Blocks' field, reference {{ $json.blocks }} from the Code node. This sends the fully formatted Block Kit message, including bold project name, milestone title, assignee, and due date.

  1. 1Set 'Resource' to 'Message'
  2. 2Set 'Operation' to 'Post'
  3. 3Set 'Channel' to expression mode and enter {{ $json.channel }}
  4. 4Set 'Message Type' to 'Block'
  5. 5Set 'Blocks' to expression mode and enter {{ JSON.stringify($json.blocks) }}
What you should see: When you click 'Test Step', a formatted Slack message should appear in your target channel within a few seconds. You'll also see a 200 OK response in the node output with the message timestamp (ts field).
Common mistake — Map fields using the variable picker — don't type field names manually. Hand-typed variable names often have invisible spacing errors that produce blank output.
10

Canvas > + after Schedule Trigger > Code (before Asana node)

Add deduplication with n8n static data

Without deduplication, every poll will re-fetch and re-post any milestone that was completed in the last 6 minutes AND is still within the polling overlap window. Add a Code node before your Filter nodes. Use n8n's built-in static workflow data to store GIDs of milestones already posted. On each run, skip any GID already in that store and add new ones after posting.

  1. 1Click '+' after the Schedule Trigger to insert a Code node before the Asana node
  2. 2Set Mode to 'Run Once For All Items'
  3. 3Paste the deduplication initialization code (see pro tip)
  4. 4After the Slack node, add another Code node to write the posted GID to static data
  5. 5Connect this final Code node back to nothing — it's a terminal write step
What you should see: After the first successful run, subsequent runs skip milestones already posted. You can verify by checking the static data store in n8n Settings > Static Data or by confirming no duplicate Slack messages appear.
Common mistake — n8n static workflow data persists across executions but resets if you delete and recreate the workflow. If you migrate or clone this workflow, your deduplication history goes with it only if you export the full workflow JSON including static data.
11

Canvas > Activate toggle (top right) > Left sidebar > Executions

Activate the workflow and verify

Click the toggle in the top-right of the canvas to activate the workflow. n8n will immediately begin polling on your 5-minute schedule. Watch the 'Executions' tab in the left sidebar — you should see a new execution entry every 5 minutes. Click any execution to inspect what data came through each node. Run a test by marking an Asana milestone complete and waiting up to 5 minutes for the Slack message to appear.

  1. 1Click the grey toggle switch in the top-right corner of the canvas
  2. 2Confirm the toggle turns green and shows 'Active'
  3. 3Click 'Executions' in the left sidebar
  4. 4Mark a test Asana milestone complete
  5. 5Wait 5 minutes and confirm the Slack message appears in the target channel
What you should see: The Executions tab shows a green 'Success' badge on each run. Your target Slack channel shows a formatted message with the milestone name, project, assignee, and due date.

Going live

Production Checklist

Before you turn this on for real, confirm each item.

Troubleshooting

Common errors and how to fix them.

Frequently Asked Questions

Common questions about this workflow.

Analysis

VerdictWhy n8n for this workflow

Use n8n for this if you are self-hosting your automation stack, want to avoid per-task pricing, or need to run custom JavaScript to handle Asana's nested data structures (memberships arrays, subtype filtering, null assignees). n8n handles all of that in Code nodes without workarounds. It is also the right call if you are monitoring 10+ projects simultaneously — you pay nothing extra per execution volume on self-hosted. The one scenario where you'd pick something else: if your company already runs Make and the whole team knows it, the Asana module in Make handles this same flow with less setup. Make's Asana integration includes built-in subtype filtering in the UI so you don't need to write filter expressions by hand.

Cost

n8n Cloud costs $20/month for the Starter plan, which includes 2,500 executions/month. This workflow fires once every 5 minutes — that's 288 executions per day, or roughly 8,640 per month. You'll hit the Starter limit in under 9 days. You need the Pro plan at $50/month (10,000 executions) or self-hosting, which runs on a $6/month VPS and has no execution cap. Make handles the equivalent workflow for $9/month on the Core plan, which gives 10,000 operations/month — and each Make scenario run counts as roughly 3-4 operations (watch, filter, HTTP, Slack), so you get about 2,500-3,000 effective runs per month. For pure cost at this polling frequency, self-hosted n8n wins by a wide margin.

Tradeoffs

Make has a native Asana 'Watch Tasks' module that uses long-polling under the hood — slightly faster response than n8n's schedule trigger and no code required for basic filtering. Zapier's Asana integration includes a 'New Completed Task' trigger but it does not natively filter by resource_subtype, so you'd need a Formatter step to filter out non-milestones, and at 5-minute polling intervals you burn through Zap tasks fast. Power Automate has an Asana connector but it is shallow — no milestone subtype filtering and limited to basic task fields. Pipedream's Asana source trigger is solid and supports true webhooks if you configure the Asana Events API, making it faster than n8n's polling approach. n8n is still the right call here if you need the Code node flexibility, want zero per-task costs, and are comfortable self-hosting — Pipedream edges it out only on real-time latency.

Three things you'll hit after going live: First, Asana's 'completed_at' timestamp is in UTC with millisecond precision, but 'due_on' is a plain date string with no timezone — your deadline proximity math will be off by up to 24 hours in certain timezones if you don't normalize both to the same reference. Set your date comparisons to use noon UTC on the due_on date. Second, the Asana API paginates at 100 tasks per request by default. If a project has 200+ tasks, 'Return All' in the n8n Asana node fires multiple API calls per execution — this counts against your rate limit faster than you expect. Third, Slack's Block Kit silently truncates section text at 3,000 characters. If your Asana task notes are long and you pass them raw, Slack drops the tail with no error — your messages just look truncated. Always cap notes at 200 characters in the Code node before sending.

Ideas for what to build next

  • Add a daily digest summaryInstead of (or alongside) per-milestone alerts, add a second workflow on a 9am daily schedule that fetches all milestones due this week and posts a single summarized digest to a leadership Slack channel — one message instead of 10 individual pings.
  • Route alerts by team using Asana custom fieldsIf your Asana projects use a custom 'Team' or 'Squad' field, update the Code node to read that field and extend the PROJECT_CHANNEL_MAP to route messages to squad-specific Slack channels like #squad-payments or #squad-growth automatically.
  • Log milestones to a Google Sheet for reportingAfter the Slack node, add a Google Sheets node that appends each posted milestone to a tracking sheet with columns: Project, Milestone, Completed Date, Assignee. Gives you a permanent audit trail and makes it easy to build a monthly milestone completion report.

Related guides

Was this guide helpful?
Slack + Asana overviewn8n profile →