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

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-time

Use case type

notification

Real-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.

/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 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.

Asana account with access to the project you want to monitor, plus permission to create webhooks (Project Admin or Workspace Admin role required)
Slack workspace with permission to install apps and post to the target channel — you need 'chat:write' scope, which requires being a Slack workspace member or admin
Pipedream account (free tier works) with a verified email address — webhook triggers require an active account
Asana Personal Access Token or OAuth credentials — Pipedream's Asana connected account handles OAuth, but your Asana role must include API access (all paid plans and free plans include this)
The Asana project must already exist and contain at least one milestone task so you can generate a real test event during setup

Field Mapping

Map these fields between your apps.

FieldAPI Name
Required
Task Nameresource.name
Task GIDresource.gid
Resource Subtyperesource.resource_subtype
Completedresource.completed
Project Namememberships[0].project.name
Completed Atcompleted_at
Permalink URLpermalink_url
3 optional fields▸ show
Assignee Nameassignee.name
Due Datedue_on
Task Notesnotes

Step-by-Step Setup

1

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.

  1. 1Log in at pipedream.com
  2. 2Click 'New Workflow' in the top-right corner
  3. 3When the canvas opens, click 'Add a trigger'
  4. 4Type 'Asana' in the search box and select the Asana app
What you should see: You should see the Asana trigger configuration panel open on the right side of the canvas.
Common mistake — Pipedream free tier limits you to 10,000 invocations per month and workflows pause after 30 days of inactivity. Set a calendar reminder to run a test event monthly if your project volume is low.
2

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.

  1. 1Select 'New Event (Instant)' from the Asana event list
  2. 2Click 'Connect Account' and complete the Asana OAuth flow in the pop-up window
  3. 3Once connected, select your target project from the 'Project' dropdown
  4. 4Click 'Save and Continue'
What you should see: You should see a green checkmark next to your connected Asana account and the selected project name displayed below the event selector.
Common mistake — Asana webhooks are project-scoped. If you want to monitor multiple projects, you need separate Pipedream workflows for each — there is no multi-project webhook in a single trigger step.
Pipedream
+
click +
search apps
Slack
SL
Slack
Configure the Asana Webhook …
Slack
SL
module added
3

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.

  1. 1Click 'Generate Test Event' in the trigger configuration panel
  2. 2If no event loads, open Asana in a new tab and mark a test task complete
  3. 3Return to Pipedream and click 'Refresh Events'
  4. 4Select the event from the list that appears
What you should see: You should see a JSON payload appear in the trigger panel with fields like resource.name, resource.completed, resource.due_on, and resource.assignee.
Pipedream
▶ Deploy & test
executed
Slack
Asana
Asana
🔔 notification
received
4

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.

  1. 1Click the '+' icon below the trigger step
  2. 2Search for and select 'Filter' from the built-in helpers
  3. 3Set Condition 1: '{{steps.trigger.event.resource.resource_subtype}}' equals 'milestone'
  4. 4Set Condition 2: '{{steps.trigger.event.resource.completed}}' is true
  5. 5Set the logic to 'AND' and click 'Continue'
What you should see: The filter step should show a green 'Passed' badge when tested against your milestone event, and a red 'Stopped' badge if you test it against a regular task completion.
Common mistake — If you skip this filter step, the workflow fires on every task completion in the project — a busy project with 50 task completions a day will flood your Slack channel.
Slack
SL
trigger
filter
Condition
matches criteria?
yes — passes through
no — skipped
Asana
AS
notified
5

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.

  1. 1Click the '+' icon below the filter step
  2. 2Select 'Run Node.js code' from the step list
  3. 3Paste the enrichment code from the pro tip section into the code editor
  4. 4Click 'Test' to run the step against the sample event
What you should see: The step output should show a full task object with fields including name, completed_at, due_on, assignee.name, memberships[0].project.name, and permalink_url.
Common mistake — Asana's API returns assignee as null if the task has no assignee. Your code must handle this — otherwise the Slack message will display 'null' as the assignee name.

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 };
  },
});
6

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.

  1. 1Click '+' below the fetch_task code step
  2. 2Select 'Run Node.js code'
  3. 3Paste the Block Kit formatter code (see pro tip section)
  4. 4Click 'Test' and verify the output contains a valid blocks array
What you should see: The step output should be a JavaScript object with a blocks array containing at least a header section, a fields section with project/assignee/due date, and a button element pointing to the Asana task URL.
Common mistake — Slack's Block Kit has a 3,000-character limit per block. If your Asana task notes are long, truncate them in the code step before passing to Slack — use .substring(0, 250) + '...' as a safe cutoff.

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 };
  },
});
message template
🔔 New Record: {{text}} {{user}}
channel: {{channel}}
ts: {{ts}}
#sales
🔔 New Record: Jane Smith
Company: Acme Corp
7

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}}.

  1. 1Click '+' and search for 'Slack'
  2. 2Select 'Send Message to a Channel'
  3. 3Click 'Connect Account' and authorize Pipedream's Slack app in your workspace
  4. 4Set Channel to your target channel name or ID (e.g., #project-updates)
  5. 5Set Blocks to '{{steps.build_message.$return_value.blocks}}'
  6. 6Leave the Text field as a fallback: 'Milestone completed in Asana'
What you should see: After clicking Test, you should see a new message appear in your Slack channel within 5 seconds, formatted with the milestone name, assignee, project, and a button linking to the task.
Common mistake — The Pipedream Slack app needs 'chat:write' and 'chat:write.public' OAuth scopes. If your Slack admin restricts app installs, you may need admin approval before the OAuth flow completes.
8

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.

  1. 1Open a new browser tab and create a second workflow at pipedream.com
  2. 2Select 'Schedule' as the trigger type
  3. 3Set the cron expression to '0 9 * * *' (9 AM daily)
  4. 4Add a Node.js step to call GET /tasks?project={gid}&due_on={tomorrow}&completed=false
What you should see: The scheduled workflow appears as a separate entry in your Pipedream Workflows list with a clock icon indicating it runs on a schedule.
Common mistake — Do not try to combine the webhook and schedule trigger in one Pipedream workflow — each workflow supports exactly one trigger. Attempting to merge them leads to missed events or doubled messages.
9

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.

  1. 1Click the fetch_task code step to open the editor
  2. 2Wrap the API call in try { } catch (error) { }
  3. 3Inside catch, add: $.flow.exit('Asana fetch failed: ' + error.message)
  4. 4Repeat the same pattern in the build_message step
  5. 5Click 'Test' on each step to confirm they still pass with valid data
What you should see: When you manually trigger the workflow with a bad task GID, the workflow should stop at the fetch_task step with a logged exit message rather than throwing an unhandled error.

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 };
  },
});
10

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.

  1. 1Click the blue 'Deploy' button in the top-right corner
  2. 2Wait for the status indicator to switch to 'Active'
  3. 3Open Asana and mark a milestone task complete
  4. 4Return to Pipedream and click 'Events' in the left sidebar to watch the run appear
  5. 5Open Slack and confirm the message posted to the correct channel
What you should see: The Events tab shows a completed run with green checkmarks on all steps. The Slack channel shows a formatted message with the task name, assignee, project, completion time, and a link to Asana.

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 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.

Cost

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.

Tradeoffs

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 DigestCreate 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 ChannelsUpdate 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 SheetAdd 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

Was this guide helpful?
Slack + Asana overviewPipedream profile →