Automate a sales-to-CS handoff when a HubSpot deal closes won using n8n

low complexityCost: $0-24/moRecommended

Prerequisites

Prerequisites
  • n8n instance — either n8n cloud or self-hosted
  • HubSpot private app token with crm.objects.deals.read, crm.objects.contacts.read, and crm.objects.tasks.write scopes
  • Slack workspace with a Slack app configured (Bot Token Scopes: chat:write, chat:write.public)
  • n8n credentials set up for both HubSpot and Slack

Step 1: Add a HubSpot Trigger node

Create a new workflow and add a HubSpot Trigger node:

  • Authentication: Select your HubSpot credential
  • Event: Deal Property Changed
  • Property: dealstage

This fires every time any deal's stage property changes. You'll filter for closedwon in the next step.

Webhook-based trigger

The HubSpot Trigger node uses webhooks for near-instant delivery (1-5 seconds). The trigger sends the deal ID and the new stage value.

Step 2: Filter for Closed Won only

Add an IF node to only process deals that moved to Closed Won:

  • Condition: {{ $json.properties.dealstage.value }} equals closedwon

Route the true output to the rest of the workflow. The false output goes nowhere — other stage changes are ignored.

Step 3: Fetch full deal details

The trigger only sends the deal ID and changed property. Add an HTTP Request node to get the full deal record:

  • Method: GET
  • URL: https://api.hubapi.com/crm/v3/objects/deals/{{ $json.objectId }}
  • Authentication: Predefined -> HubSpot API
  • Query params: properties=dealname,amount,dealstage,hubspot_owner_id,closedate,hs_num_associated_contacts,notes_last_updated,contract_length,description&associations=contacts

The associations=contacts parameter returns associated contact IDs so you can pull contact details.

Step 4: Fetch associated contacts

Add another HTTP Request node to get the primary contact's details:

  • Method: GET
  • URL: https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.associations.contacts.results[0].id }}
  • Query params: properties=firstname,lastname,email,phone,jobtitle,company
Multiple contacts

A deal may have multiple associated contacts. The example above fetches only the first. To handle all contacts, add a Split In Batches node to iterate over $json.associations.contacts.results and fetch each one.

Step 5: Fetch the deal owner's name

Add an HTTP Request node to resolve the sales rep's owner ID to a name:

  • Method: GET
  • URL: https://api.hubapi.com/crm/v3/owners/{{ $json.properties.hubspot_owner_id }}

Step 6: Send Slack notification to CS channel

Add a Slack node:

  • Resource: Message
  • Operation: Send a Message
  • Channel: #cs-handoffs
  • Message Type: Block Kit
{
  "blocks": [
    {
      "type": "header",
      "text": {
        "type": "plain_text",
        "text": "🎉 New Closed-Won Deal — CS Handoff"
      }
    },
    {
      "type": "section",
      "fields": [
        {
          "type": "mrkdwn",
          "text": "*Deal*\n{{ $('Fetch Deal').item.json.properties.dealname }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Value*\n${{ parseFloat($('Fetch Deal').item.json.properties.amount || '0').toLocaleString() }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Sales Rep*\n{{ $('Fetch Owner').item.json.firstName }} {{ $('Fetch Owner').item.json.lastName }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Close Date*\n{{ $('Fetch Deal').item.json.properties.closedate.split('T')[0] }}"
        }
      ]
    },
    {
      "type": "divider"
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*Primary Contact*\n{{ $('Fetch Contact').item.json.properties.firstname }} {{ $('Fetch Contact').item.json.properties.lastname }} ({{ $('Fetch Contact').item.json.properties.jobtitle || 'No title' }})\n📧 {{ $('Fetch Contact').item.json.properties.email }}\n📞 {{ $('Fetch Contact').item.json.properties.phone || 'No phone' }}"
      }
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*Contract Length*\n{{ $('Fetch Deal').item.json.properties.contract_length || 'Not specified' }}"
      }
    },
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "text": {
            "type": "plain_text",
            "text": "View Deal in HubSpot"
          },
          "url": "https://app.hubspot.com/contacts/YOUR_PORTAL_ID/deal/{{ $('Fetch Deal').item.json.id }}"
        }
      ]
    }
  ]
}

Step 7: Create a HubSpot task for the CS rep

Add an HTTP Request node to create an onboarding task:

  • Method: POST
  • URL: https://api.hubapi.com/crm/v3/objects/tasks
  • Body:
{
  "properties": {
    "hs_task_subject": "Onboarding: {{ $('Fetch Deal').item.json.properties.dealname }}",
    "hs_task_body": "New closed-won deal ready for CS onboarding.\n\nDeal: {{ $('Fetch Deal').item.json.properties.dealname }}\nValue: ${{ parseFloat($('Fetch Deal').item.json.properties.amount || '0').toLocaleString() }}\nSales Rep: {{ $('Fetch Owner').item.json.firstName }} {{ $('Fetch Owner').item.json.lastName }}\nPrimary Contact: {{ $('Fetch Contact').item.json.properties.email }}",
    "hs_task_status": "NOT_STARTED",
    "hs_task_priority": "HIGH",
    "hubspot_owner_id": "CS_REP_OWNER_ID",
    "hs_timestamp": "{{ new Date().toISOString() }}"
  },
  "associations": [
    {
      "to": { "id": "{{ $('Fetch Deal').item.json.id }}" },
      "types": [{ "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 204 }]
    }
  ]
}
Task association type ID

The association type ID 204 links a task to a deal. Use 1 for contact-to-task associations. These IDs are HubSpot-defined — find the full list in the HubSpot associations API docs.

Replace CS_REP_OWNER_ID with the actual owner ID of your CS rep, or add logic to route based on deal size, industry, or region.

Step 8: Add error handling and activate

  1. Enable Settings -> Retry On Fail on each HTTP Request node (2 retries, 5-second wait)
  2. Create an Error Workflow that sends a Slack DM when the handoff fails
  3. Click Execute Workflow to test (manually move a test deal to Closed Won in HubSpot)
  4. Toggle the workflow to Active

Cost

  • n8n Cloud Starter: $24/mo for 2,500 executions. Each closed-won deal triggers 1 execution. Self-hosted n8n is free.
  • Maintenance: update the CS rep owner ID if team assignments change. Consider building an owner routing table in a Code node for automatic assignment.

Next steps

  • CS rep routing — add a Code node that maps deal properties (size, industry, region) to the appropriate CS rep owner ID
  • Onboarding checklist — create multiple tasks (kickoff call, intro email, account setup) instead of a single task
  • Timeline note — add an HTTP Request to post a note on the deal timeline (POST /crm/v3/objects/notes) documenting that the handoff occurred
  • Welcome email — add a step to trigger a HubSpot workflow or send an email to the customer's primary contact

Need help implementing this?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.