HubSpot + Asana: Why the Native Integration Hides Tasks from Your Sales Team

The native HubSpot + Asana integration creates tasks in Asana from HubSpot, but does not surface completed tasks in the Deal Activity Timeline. A sales manager opens a deal and has no idea that the ops team has already completed 3 of 5 onboarding tasks. All status communication happens over Slack or email - instead of a single source of truth in the CRM.

The right fix: Asana webhook on task completion -> your server -> HubSpot Note associated with the Deal via associationTypeId: 214. After that, every completed Asana task appears instantly in the Deal Timeline with no manual action required.

HubSpot Deal Activity Timeline is the activity feed inside a deal: calls, emails, notes, meetings. It is the primary tool sales teams use to understand current client status. If an event does not appear in the Timeline - as far as the manager is concerned, it never happened.

Why the Native Integration Does Not Solve the Problem

HubSpot and Asana have an official integration. It lets you:

  • Create Asana tasks from HubSpot (on Deal creation, or manually)
  • View Asana task status in a widget on the Deal page

What it does NOT do:

  • Add task completion events to the Activity Timeline
  • Create Notes or Activities in HubSpot when things change in Asana
  • Support custom triggers (only fires on Deal creation)

A manager can check the widget - but that is not the same as seeing a timeline entry that reads “Task ‘Set up integration’ completed by Johnson on 12.06.2026 at 14:30”.

What the Business Loses

In a typical B2B SaaS with a 30-day onboarding cycle: 5-7 tasks are created in Asana when a deal is won. The sales manager needs to know the client’s status before an upsell call. Without Asana task visibility in the CRM:

  • The manager calls a client without knowing onboarding is not yet complete
  • The ops team cannot see when sales schedules a follow-up (no bidirectional sync)
  • Customer Success does not know which onboarding tasks have been completed

With 20+ active clients, this becomes a systemic coordination problem.

Solution Architecture

Asana: task completed (completed = true)
  -> Asana webhook -> POST your-server/webhooks/asana
  -> Your server:
     -> GET Asana /tasks/{gid}: fetch name, project, assignee
     -> Find HubSpot Deal by custom field (asana_project_id or deal_id)
     -> POST HubSpot /crm/v3/objects/notes
        {properties: {hs_note_body, hs_timestamp},
         associations: [{to: {id: dealId}, types: [{associationTypeId: 214}]}]}

Setting Up the Asana Webhook

import requests, os, hashlib, hmac
from flask import Flask, request, jsonify, abort

app = Flask(__name__)

ASANA_PAT        = os.environ["ASANA_ACCESS_TOKEN"]
ASANA_BASE       = "https://app.asana.com/api/1.0"
ASANA_HDR        = {"Authorization": f"Bearer {ASANA_PAT}",
                    "Content-Type": "application/json"}

HS_TOKEN         = os.environ["HUBSPOT_PRIVATE_APP_TOKEN"]
HS_BASE          = "https://api.hubapi.com"
HS_HDR           = {"Authorization": f"Bearer {HS_TOKEN}",
                    "Content-Type": "application/json"}

# Store: Asana project_gid -> HubSpot deal_id
# In production - PostgreSQL or Redis
project_to_deal: dict = {}

def register_asana_webhook(project_gid: str,
                             callback_url: str, deal_id: str) -> str:
    r = requests.post(
        f"{ASANA_BASE}/webhooks",
        headers=ASANA_HDR,
        json={
            "data": {
                "resource": project_gid,
                "target":   callback_url,
                "filters": [
                    {"resource_type": "task", "action": "changed",
                     "fields": ["completed"]},
                    {"resource_type": "task", "action": "added"},
                ],
            }
        },
    )
    r.raise_for_status()
    webhook_id = r.json()["data"]["gid"]
    project_to_deal[project_gid] = deal_id
    return webhook_id

@app.route("/webhooks/asana", methods=["POST"])
def asana_webhook():
    # Handshake: on the first request Asana sends X-Hook-Secret
    secret = request.headers.get("X-Hook-Secret")
    if secret:
        # Echo the secret back to complete the handshake
        # In production save it to env/DB
        return "", 200, {"X-Hook-Secret": secret}

    # HMAC verification (after initial handshake)
    hook_secret = os.environ.get("ASANA_HOOK_SECRET", "")
    if hook_secret:
        sig  = request.headers.get("X-Hook-Signature", "")
        body = request.get_data()
        expected = hmac.new(
            hook_secret.encode(), body, hashlib.sha256
        ).hexdigest()
        if not hmac.compare_digest(sig, expected):
            abort(401)

    events = request.json.get("events", [])
    for event in events:
        if event.get("type") != "task":
            continue
        if event.get("action") != "changed":
            continue

        task_gid    = event.get("resource", {}).get("gid", "")
        project_gid = event.get("parent", {}).get("gid", "")

        if not task_gid:
            continue

        # Fetch task details
        rt = requests.get(
            f"{ASANA_BASE}/tasks/{task_gid}",
            headers=ASANA_HDR,
            params={"opt_fields": "name,completed,assignee.name,due_on,notes"},
        )
        task = rt.json().get("data", {})
        if not task.get("completed"):
            continue  # we only care about completions

        deal_id = project_to_deal.get(project_gid)
        if not deal_id:
            continue

        task_name    = task.get("name", "Task")
        assignee     = (task.get("assignee") or {}).get("name", "Unassigned")
        completed_at = event.get("created_at", "")

        note_body = (
            f"Asana task completed: {task_name}\n"
            f"Assignee: {assignee}\n"
            f"Project GID: {project_gid}"
        )
        create_hubspot_note(deal_id, note_body, completed_at)

    return jsonify({"status": "ok"}), 200

def create_hubspot_note(deal_id: str,
                         note_body: str, timestamp: str) -> str:
    from datetime import datetime
    ts_ms = int(
        datetime.fromisoformat(
            timestamp.replace("Z", "+00:00")
        ).timestamp() * 1000
    ) if timestamp else None

    payload = {
        "properties": {
            "hs_note_body": note_body,
            "hs_timestamp": str(ts_ms) if ts_ms else "",
        },
        "associations": [
            {
                "to": {"id": deal_id},
                "types": [
                    {
                        "associationCategory": "HUBSPOT_DEFINED",
                        "associationTypeId":   214,
                    }
                ],
            }
        ],
    }
    r = requests.post(
        f"{HS_BASE}/crm/v3/objects/notes",
        headers=HS_HDR,
        json=payload,
    )
    r.raise_for_status()
    return r.json()["id"]

Linking the Asana Project to the HubSpot Deal

The project_gid -> deal_id mapping must be created when a deal is won. Typical flow:

# On HubSpot webhook deal.propertyChange (dealstage = closedwon):
def on_deal_won(deal_id: str):
    asana_project_gid = create_asana_project_for_deal(deal_id)
    register_asana_webhook(
        asana_project_gid,
        "https://your-server.com/webhooks/asana",
        deal_id,
    )
    # Save mapping to DB
    db.save_mapping(asana_project_gid, deal_id)

Alternatively: if Asana projects are created manually - add a custom field “HubSpot Deal ID” to the Asana project and read it when processing webhook events.

Real-World Case

A SaaS company with 30 clients in onboarding simultaneously. Asana is the CS team’s tool (5-7 tasks per client: setup, training, first value, NPS). Sales managers could not see onboarding status before renewal calls. After the integration: every completed Asana task appears in the HubSpot Deal Timeline. The manager opens a deal and sees the full picture without pinging anyone on Slack. Prep time for renewal calls dropped from 15 to 3 minutes.

Who This Is For

B2B companies with a sales / CS / ops role split that use HubSpot as their CRM and Asana as their execution tool. Especially relevant for SaaS with onboarding, consulting firms, and agencies. If you have more than 10 active clients in Asana, the lack of CRM visibility becomes a systemic problem.

Other HubSpot anti-patterns: HubSpot + Harvest: time tracking not in Deal (logged hours), HubSpot + Loom: video not in Deal Timeline.

Frequently Asked Questions

How does Asana verify the webhook on the first request?

The first request from Asana includes an X-Hook-Secret header. Your server must return that same secret in the X-Hook-Secret response header. After that, Asana starts signing requests with HMAC-SHA256 - your server verifies the signature in X-Hook-Signature.

Why associationTypeId: 214 for Note -> Deal?

214 is the standard HubSpot ID for associating a Note with a Deal. Full list: GET /crm/v4/associations/{fromObjectType}/{toObjectType}/labels with fromObjectType=notes, toObjectType=deals. For Note -> Contact - 202, Note -> Company - 190.

Can tasks be synced in the reverse direction (HubSpot -> Asana)?

Yes: HubSpot webhook deal.propertyChange on the relevant field change -> your server -> POST /tasks in Asana. This is bidirectional sync, which the native integration also does not support.

How do I find the HubSpot deal_id by a client’s email in Asana?

There is no direct link between Asana and HubSpot. Solution: store the mapping in PostgreSQL (asana_project_gid -> hubspot_deal_id), created when the project is created. Or add a custom field “HubSpot Deal ID” to the Asana Project and read it when processing events via GET /projects/{gid}?opt_fields=custom_fields.

Summary

The HubSpot + Asana native integration is incomplete:

  • Native integration: creates tasks but does not add events to the Deal Timeline
  • Correct approach: Asana webhook -> HubSpot Note with associationTypeId: 214
  • Handshake: echo X-Hook-Secret back in the response header
  • Store project_gid -> deal_id mapping in a DB to link events to deals
  • Result: full Asana task visibility inside HubSpot Deal Timeline

If your team uses HubSpot + Asana and wants to set up proper bidirectional visibility - describe your situation to the Exceltic.dev team.

More articles

All →