

How to Send Asana Milestone Updates to Slack with Pipedream
Automatically posts a formatted Slack message to a designated channel whenever an Asana task is marked complete or a deadline is approaching, so teams get status updates without anyone writing them manually.
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 who run projects in Asana and need the rest of the company to see milestone completions in Slack without someone manually writing status updates.
Not ideal for
Teams who need bidirectional sync — if you also want Slack replies to update Asana tasks, build a separate reverse workflow or look at a dedicated integration tool.
Sync type
real-timeUse case type
notificationReal-World Example
A 22-person product team at a B2B SaaS company tracks feature launches in Asana with milestones like 'Backend complete', 'QA sign-off', and 'Shipped to prod'. Before this workflow, the PM manually posted updates in #product-launches after each milestone — often hours late. Now, the moment an Asana task is marked complete, a formatted message fires to #product-launches within 15 seconds, including the task name, assignee, project name, and a direct link back to Asana.
What Will This Cost?
Drag the slider to your expected monthly volume.
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
Import this workflow directly into Pipedream
Copy the pre-built Pipedream blueprint and paste it straight into Pipedream. 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.
Field Mapping
Map these fields between your apps.
| Field | API Name | |
|---|---|---|
| Required | ||
| Task Name | resource.name | |
| Task GID | resource.gid | |
| Resource Subtype | resource.resource_subtype | |
| Completed | resource.completed | |
| Project Name | memberships[0].project.name | |
| Completed At | completed_at | |
| Permalink URL | permalink_url | |
3 optional fields▸ show
| Assignee Name | assignee.name |
| Due Date | due_on |
| Task Notes | notes |
Step-by-Step Setup
pipedream.com > Workflows > New Workflow
Create a new Pipedream Workflow
Go to pipedream.com and click 'New Workflow' in the top-right corner of the dashboard. You'll land on the workflow canvas with a prompt to select a trigger. This is where the entire automation starts — Pipedream will give this workflow a unique webhook URL once you configure the trigger source. Name the workflow something like 'Asana Milestones → Slack' so it's easy to find later.
- 1Log in at pipedream.com
- 2Click 'New Workflow' in the top-right corner
- 3When the canvas opens, click 'Add a trigger'
- 4Type 'Asana' in the search box and select the Asana app
Trigger Panel > App > Asana > Event > Connected Accounts
Configure the Asana Webhook Trigger
In the trigger panel, select 'New Completed Task' or 'New Event' from the event dropdown — choose 'New Event' if you also want to catch deadline-approaching scenarios via Asana's story events. Connect your Asana account using the 'Connect Account' button, which opens an OAuth flow in a new tab. After authenticating, select the specific Asana Project you want to monitor from the Project dropdown — this scopes the webhook to that project only.
- 1Select 'New Event (Instant)' from the Asana event list
- 2Click 'Connect Account' and complete the Asana OAuth flow in the pop-up window
- 3Once connected, select your target project from the 'Project' dropdown
- 4Click 'Save and Continue'
Trigger Panel > Test > Generate Test Event
Test the Trigger with a Real Asana Event
Click 'Generate Test Event' — Pipedream will poll Asana for a recent event in that project and load it as sample data into the trigger step. If no events have fired recently, go to Asana, mark any task in the project complete, then return to Pipedream and click 'Refresh' to pull the event. You need real event data here because downstream steps reference the exact field names Asana returns — dummy data will cause mapping errors later.
- 1Click 'Generate Test Event' in the trigger configuration panel
- 2If no event loads, open Asana in a new tab and mark a test task complete
- 3Return to Pipedream and click 'Refresh Events'
- 4Select the event from the list that appears
Workflow Canvas > + Add Step > Filter
Add a Filter Step to Target Milestone Tasks Only
Click the '+' button below the trigger to add a new step. Select 'Filter' from the built-in Pipedream helpers. You want this workflow to only continue when the completed task is actually a milestone — in Asana, milestones are tasks where resource.resource_subtype equals 'milestone'. Set the filter condition: resource.resource_subtype === 'milestone' AND resource.completed === true. This prevents the workflow from posting to Slack every time any task in the project is completed.
- 1Click the '+' icon below the trigger step
- 2Search for and select 'Filter' from the built-in helpers
- 3Set Condition 1: '{{steps.trigger.event.resource.resource_subtype}}' equals 'milestone'
- 4Set Condition 2: '{{steps.trigger.event.resource.completed}}' is true
- 5Set the logic to 'AND' and click 'Continue'
Workflow Canvas > + Add Step > Run Node.js code
Add a Node.js Code Step to Fetch Full Task Details
The Asana webhook payload is sparse — it includes the task GID but not the full task details like assignee name, project name, or notes. Add a code step after the filter to call the Asana API and pull the complete task record. Click '+', select 'Run Node.js code', and paste the fetch code shown in the pro tip section below. You'll reference the task GID from the trigger event as steps.trigger.event.resource.gid.
- 1Click the '+' icon below the filter step
- 2Select 'Run Node.js code' from the step list
- 3Paste the enrichment code from the pro tip section into the code editor
- 4Click 'Test' to run the step against the sample event
This code handles both the Asana API fetch and Slack Block Kit formatting in a single exportable module. Paste the fetch section into your fetch_task Node.js step and the formatter section into your build_message step — both steps are on the Pipedream canvas after the filter step.
JavaScript — Code Step// ── Step: fetch_task ──────────────────────────────────────────────▸ Show code
// ── Step: fetch_task ──────────────────────────────────────────────
// Paste this into the first Node.js code step (fetch_task)
import { axios } from '@pipedream/platform';... expand to see full code
// ── Step: fetch_task ──────────────────────────────────────────────
// Paste this into the first Node.js code step (fetch_task)
import { axios } from '@pipedream/platform';
export default defineComponent({
async run({ steps, $ }) {
const taskGid = steps.trigger.event.resource.gid;
if (!taskGid) {
$.flow.exit('No task GID found in trigger event');
}
let task;
try {
const response = await axios($, {
url: `https://app.asana.com/api/1.0/tasks/${taskGid}`,
headers: {
Authorization: `Bearer ${process.env.ASANA_ACCESS_TOKEN}`,
},
params: {
opt_fields: 'name,completed,completed_at,due_on,assignee.name,memberships.project.name,permalink_url,notes,resource_subtype',
},
});
task = response.data;
} catch (error) {
$.flow.exit(`Asana API fetch failed: ${error.message}`);
}
return {
gid: task.gid,
name: task.name,
completed: task.completed,
completedAt: task.completed_at,
dueOn: task.due_on,
assigneeName: task.assignee ? task.assignee.name : 'Unassigned',
projectName: task.memberships?.[0]?.project?.name ?? 'Unknown Project',
url: task.permalink_url,
notes: task.notes ? task.notes.substring(0, 250) + (task.notes.length > 250 ? '...' : '') : null,
};
},
});
// ── Step: build_message ───────────────────────────────────────────
// Paste this into the second Node.js code step (build_message)
export default defineComponent({
async run({ steps, $ }) {
const task = steps.fetch_task.$return_value;
const formatDate = (iso) => {
if (!iso) return 'No date set';
return new Date(iso).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
});
};
const completedDate = task.completedAt
? new Date(task.completedAt).toLocaleString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
})
: 'Unknown';
const onTime = task.dueOn
? new Date(task.completedAt) <= new Date(task.dueOn)
? '✅ Completed on time'
: '⚠️ Completed after due date'
: '';
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: `✅ Milestone Complete: ${task.name}`, emoji: true },
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Project:*\n${task.projectName}` },
{ type: 'mrkdwn', text: `*Assignee:*\n${task.assigneeName}` },
{ type: 'mrkdwn', text: `*Due Date:*\n${formatDate(task.dueOn)}` },
{ type: 'mrkdwn', text: `*Completed At:*\n${completedDate}` },
],
},
onTime ? { type: 'section', text: { type: 'mrkdwn', text: onTime } } : null,
task.notes
? { type: 'section', text: { type: 'mrkdwn', text: `*Notes:*\n${task.notes}` } }
: null,
{ type: 'divider' },
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'View in Asana', emoji: true },
url: task.url,
style: 'primary',
},
],
},
].filter(Boolean);
return { blocks };
},
});Workflow Canvas > + Add Step > Run Node.js code
Add Another Code Step to Build the Slack Message
Add a second Node.js code step after the Asana API call. This step formats the task data into a Slack Block Kit message. Using Block Kit instead of plain text gives you structured sections, bold headers, and a clickable button linking back to the Asana task — all without any Slack app configuration changes. Reference the output from the previous step using steps.fetch_task.$return_value.
- 1Click '+' below the fetch_task code step
- 2Select 'Run Node.js code'
- 3Paste the Block Kit formatter code (see pro tip section)
- 4Click 'Test' and verify the output contains a valid blocks array
This code handles both the Asana API fetch and Slack Block Kit formatting in a single exportable module. Paste the fetch section into your fetch_task Node.js step and the formatter section into your build_message step — both steps are on the Pipedream canvas after the filter step.
JavaScript — Code Step// ── Step: fetch_task ──────────────────────────────────────────────▸ Show code
// ── Step: fetch_task ──────────────────────────────────────────────
// Paste this into the first Node.js code step (fetch_task)
import { axios } from '@pipedream/platform';... expand to see full code
// ── Step: fetch_task ──────────────────────────────────────────────
// Paste this into the first Node.js code step (fetch_task)
import { axios } from '@pipedream/platform';
export default defineComponent({
async run({ steps, $ }) {
const taskGid = steps.trigger.event.resource.gid;
if (!taskGid) {
$.flow.exit('No task GID found in trigger event');
}
let task;
try {
const response = await axios($, {
url: `https://app.asana.com/api/1.0/tasks/${taskGid}`,
headers: {
Authorization: `Bearer ${process.env.ASANA_ACCESS_TOKEN}`,
},
params: {
opt_fields: 'name,completed,completed_at,due_on,assignee.name,memberships.project.name,permalink_url,notes,resource_subtype',
},
});
task = response.data;
} catch (error) {
$.flow.exit(`Asana API fetch failed: ${error.message}`);
}
return {
gid: task.gid,
name: task.name,
completed: task.completed,
completedAt: task.completed_at,
dueOn: task.due_on,
assigneeName: task.assignee ? task.assignee.name : 'Unassigned',
projectName: task.memberships?.[0]?.project?.name ?? 'Unknown Project',
url: task.permalink_url,
notes: task.notes ? task.notes.substring(0, 250) + (task.notes.length > 250 ? '...' : '') : null,
};
},
});
// ── Step: build_message ───────────────────────────────────────────
// Paste this into the second Node.js code step (build_message)
export default defineComponent({
async run({ steps, $ }) {
const task = steps.fetch_task.$return_value;
const formatDate = (iso) => {
if (!iso) return 'No date set';
return new Date(iso).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
});
};
const completedDate = task.completedAt
? new Date(task.completedAt).toLocaleString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
})
: 'Unknown';
const onTime = task.dueOn
? new Date(task.completedAt) <= new Date(task.dueOn)
? '✅ Completed on time'
: '⚠️ Completed after due date'
: '';
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: `✅ Milestone Complete: ${task.name}`, emoji: true },
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Project:*\n${task.projectName}` },
{ type: 'mrkdwn', text: `*Assignee:*\n${task.assigneeName}` },
{ type: 'mrkdwn', text: `*Due Date:*\n${formatDate(task.dueOn)}` },
{ type: 'mrkdwn', text: `*Completed At:*\n${completedDate}` },
],
},
onTime ? { type: 'section', text: { type: 'mrkdwn', text: onTime } } : null,
task.notes
? { type: 'section', text: { type: 'mrkdwn', text: `*Notes:*\n${task.notes}` } }
: null,
{ type: 'divider' },
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'View in Asana', emoji: true },
url: task.url,
style: 'primary',
},
],
},
].filter(Boolean);
return { blocks };
},
});channel: {{channel}}
ts: {{ts}}
Workflow Canvas > + Add Step > Slack > Send Message to a Channel
Add the Slack Send Message Step
Click '+' to add another step, search for 'Slack', and select the 'Send Message' action. Connect your Slack workspace via the 'Connect Account' OAuth flow. In the Channel field, type the exact channel name (e.g., #project-updates) or paste the channel ID — channel IDs are more reliable than names if your workspace renames channels. Set the Blocks field to reference the blocks array output from your formatter step: {{steps.build_message.$return_value.blocks}}.
- 1Click '+' and search for 'Slack'
- 2Select 'Send Message to a Channel'
- 3Click 'Connect Account' and authorize Pipedream's Slack app in your workspace
- 4Set Channel to your target channel name or ID (e.g., #project-updates)
- 5Set Blocks to '{{steps.build_message.$return_value.blocks}}'
- 6Leave the Text field as a fallback: 'Milestone completed in Asana'
pipedream.com > Workflows > New Workflow > Trigger > Schedule
Add Deadline-Approaching Logic (Optional Branch)
If you also want alerts when deadlines are approaching, add a separate scheduled trigger workflow rather than complicating this one. Create a second Pipedream workflow with a Schedule trigger set to run daily at 9 AM. That workflow queries the Asana API for tasks with due_on equal to tomorrow and posts a digest to Slack. This keeps the milestone-completion workflow clean and makes each workflow easier to debug independently.
- 1Open a new browser tab and create a second workflow at pipedream.com
- 2Select 'Schedule' as the trigger type
- 3Set the cron expression to '0 9 * * *' (9 AM daily)
- 4Add a Node.js step to call GET /tasks?project={gid}&due_on={tomorrow}&completed=false
Workflow Canvas > fetch_task step > Edit code
Add Error Handling to the Code Steps
Go back to your fetch_task code step and wrap the Asana API call in a try/catch block. In the catch block, use $.flow.exit('Task fetch failed: ' + error.message) to halt the workflow cleanly instead of letting it crash silently. Add the same pattern to the build_message step. Pipedream logs all exits in the workflow event history, so you'll see exactly which step failed and why when something goes wrong.
- 1Click the fetch_task code step to open the editor
- 2Wrap the API call in try { } catch (error) { }
- 3Inside catch, add: $.flow.exit('Asana fetch failed: ' + error.message)
- 4Repeat the same pattern in the build_message step
- 5Click 'Test' on each step to confirm they still pass with valid data
This code handles both the Asana API fetch and Slack Block Kit formatting in a single exportable module. Paste the fetch section into your fetch_task Node.js step and the formatter section into your build_message step — both steps are on the Pipedream canvas after the filter step.
JavaScript — Code Step// ── Step: fetch_task ──────────────────────────────────────────────▸ Show code
// ── Step: fetch_task ──────────────────────────────────────────────
// Paste this into the first Node.js code step (fetch_task)
import { axios } from '@pipedream/platform';... expand to see full code
// ── Step: fetch_task ──────────────────────────────────────────────
// Paste this into the first Node.js code step (fetch_task)
import { axios } from '@pipedream/platform';
export default defineComponent({
async run({ steps, $ }) {
const taskGid = steps.trigger.event.resource.gid;
if (!taskGid) {
$.flow.exit('No task GID found in trigger event');
}
let task;
try {
const response = await axios($, {
url: `https://app.asana.com/api/1.0/tasks/${taskGid}`,
headers: {
Authorization: `Bearer ${process.env.ASANA_ACCESS_TOKEN}`,
},
params: {
opt_fields: 'name,completed,completed_at,due_on,assignee.name,memberships.project.name,permalink_url,notes,resource_subtype',
},
});
task = response.data;
} catch (error) {
$.flow.exit(`Asana API fetch failed: ${error.message}`);
}
return {
gid: task.gid,
name: task.name,
completed: task.completed,
completedAt: task.completed_at,
dueOn: task.due_on,
assigneeName: task.assignee ? task.assignee.name : 'Unassigned',
projectName: task.memberships?.[0]?.project?.name ?? 'Unknown Project',
url: task.permalink_url,
notes: task.notes ? task.notes.substring(0, 250) + (task.notes.length > 250 ? '...' : '') : null,
};
},
});
// ── Step: build_message ───────────────────────────────────────────
// Paste this into the second Node.js code step (build_message)
export default defineComponent({
async run({ steps, $ }) {
const task = steps.fetch_task.$return_value;
const formatDate = (iso) => {
if (!iso) return 'No date set';
return new Date(iso).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
});
};
const completedDate = task.completedAt
? new Date(task.completedAt).toLocaleString('en-US', {
month: 'short', day: 'numeric', year: 'numeric',
hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
})
: 'Unknown';
const onTime = task.dueOn
? new Date(task.completedAt) <= new Date(task.dueOn)
? '✅ Completed on time'
: '⚠️ Completed after due date'
: '';
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: `✅ Milestone Complete: ${task.name}`, emoji: true },
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Project:*\n${task.projectName}` },
{ type: 'mrkdwn', text: `*Assignee:*\n${task.assigneeName}` },
{ type: 'mrkdwn', text: `*Due Date:*\n${formatDate(task.dueOn)}` },
{ type: 'mrkdwn', text: `*Completed At:*\n${completedDate}` },
],
},
onTime ? { type: 'section', text: { type: 'mrkdwn', text: onTime } } : null,
task.notes
? { type: 'section', text: { type: 'mrkdwn', text: `*Notes:*\n${task.notes}` } }
: null,
{ type: 'divider' },
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'View in Asana', emoji: true },
url: task.url,
style: 'primary',
},
],
},
].filter(Boolean);
return { blocks };
},
});Workflow Canvas > Deploy (top right)
Deploy and Monitor the Workflow
Click 'Deploy' in the top-right corner of the workflow canvas. The workflow status changes from 'Inactive' to 'Active' and Pipedream registers the Asana webhook automatically. Go to Asana, mark a real milestone task complete, and watch the Pipedream event history — the workflow should appear in the Events tab within 15 seconds. Check the Slack channel to confirm the message posted correctly with all field values populated.
- 1Click the blue 'Deploy' button in the top-right corner
- 2Wait for the status indicator to switch to 'Active'
- 3Open Asana and mark a milestone task complete
- 4Return to Pipedream and click 'Events' in the left sidebar to watch the run appear
- 5Open Slack and confirm the message posted to the correct channel
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
Use Pipedream for this if your team has anyone who can read JavaScript — even at a basic level. The webhook trigger is instant, the Node.js code steps give you full control over the Slack Block Kit payload, and the Asana API enrichment step (which every other no-code tool handles poorly) is straightforward in async/await. The one scenario where you'd skip Pipedream: if your team has zero coding tolerance and wants drag-and-drop everything, Zapier has a pre-built Asana + Slack template that gets you 70% of the way there in 5 minutes with no code written.
On cost: Pipedream's free tier gives you 10,000 workflow invocations per month. Each milestone completion = 1 invocation. A team completing 50 milestones a month pays nothing. At 500 milestones/month you're still on the free tier. The paid plan starts at $19/month for 100,000 invocations — you will not hit that limit with Asana milestones unless you're running dozens of projects simultaneously. Zapier charges per task: at 500 events/month you're looking at $20-49/month depending on your plan, and each workflow step counts separately. Pipedream is cheaper by a factor of 2-5x for this use case once you're past 100 events/month.
Make's scenario builder handles the Asana-to-Slack connection in fewer clicks — its native Asana module parses the webhook payload without a code step, and the Slack module has a built-in Block Kit composer that's visual rather than JSON. n8n gives you a self-hosted option with the same code flexibility as Pipedream but lets you reuse credential nodes across workflows more cleanly. Power Automate has a native Asana connector but it's polling-only (5-minute minimum delay) — not acceptable for real-time milestone notifications. Zapier's template gets non-technical teams live faster, but you lose the ability to do the Asana API enrichment step without Zapier's Code by Zapier, which is clunky. Pipedream is still the right call if you want instant delivery, formatted Block Kit messages, and no per-task pricing.
Three things you'll hit after setup. First: Asana's webhook payload does not include the project name — just a project GID. The fetch_task step solves this, but if you skip that step and use the raw webhook payload, your Slack message will show a numeric ID where the project name should be. Second: Asana rate-limits the REST API to 1,500 requests per minute per user token. With one workflow and one API call per milestone completion, you will never hit this — but if you duplicate this workflow for 20 projects all completing milestones simultaneously during a sprint close, check the rate limit math. Third: Slack's Block Kit button element requires the URL to start with https:// — Asana's permalink_url always does, but if you're constructing the URL manually in code instead of pulling it from the API, a missing protocol will cause the Send Message step to return an invalid_blocks error with no other hint about what's wrong.
Ideas for what to build next
- →Add a Daily Deadline-Approaching Digest — Create a second Pipedream workflow with a Schedule trigger (daily at 9 AM) that queries the Asana API for tasks due tomorrow and posts a digest message to Slack — gives teams a heads-up before milestones are due, not just after completion.
- →Route Updates to Project-Specific Channels — Update the build_message step to map project names to specific Slack channel IDs using a lookup object — so 'Q3 Feature Launch' posts to #q3-launch and 'Platform Infra' posts to #infra-updates, eliminating the need for a single catch-all channel.
- →Log Milestone Completions to a Google Sheet — Add a Google Sheets step after the Slack step to append each milestone completion as a row — tracks a running history of milestone dates, assignees, and on-time status that Asana's built-in reporting doesn't give you by default.
Related guides
How to Share Notion Meeting Notes to Slack with Pipedream
~15 min setup
How to Share Notion Meeting Notes to Slack with Power Automate
~15 min setup
How to Share Notion Meeting Notes to Slack with n8n
~20 min setup
How to Send Notion Meeting Notes to Slack with Zapier
~8 min setup
How to Share Notion Meeting Notes to Slack with Make
~12 min setup
How to Create Notion Tasks from Slack with Pipedream
~15 min setup