Automate a sales-to-CS handoff when a HubSpot deal closes won using n8n
Prerequisites
- n8n instance — either n8n cloud or self-hosted
- HubSpot private app token with
crm.objects.deals.read,crm.objects.contacts.read, andcrm.objects.tasks.writescopes - 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.
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 }}equalsclosedwon
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
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 }]
}
]
}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
- Enable Settings -> Retry On Fail on each HTTP Request node (2 retries, 5-second wait)
- Create an Error Workflow that sends a Slack DM when the handoff fails
- Click Execute Workflow to test (manually move a test deal to Closed Won in HubSpot)
- 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.