Instantly notify a rep in Slack when a high-intent lead books a demo using n8n
Install this workflow
Download the n8n workflow JSON and import it into your n8n instance.
demo-alerts.n8n.jsonPrerequisites
- n8n instance (cloud or self-hosted)
- HubSpot private app token with
crm.objects.contacts.readandformsscopes - Slack app with Bot Token (
chat:writescope) - n8n credentials configured for both HubSpot and Slack
- A HubSpot form used for demo bookings (or a Calendly/Cal.com form synced to HubSpot)
- A mapping of HubSpot owner IDs to Slack user IDs
Why n8n?
n8n gives you event-driven demo alerts with a visual workflow builder. The HubSpot Trigger node fires on form submissions, and the Code node handles the owner-to-Slack mapping without external services. Self-hosted n8n is free with unlimited executions — important when every demo booking generates an alert regardless of volume.
The trade-off vs. Zapier is setup time (slightly more configuration), but you get faster execution, no per-task limits, and full control over the routing logic.
How it works
- HubSpot Trigger fires on form submissions, then an IF node filters to your demo form
- HTTP Request fetches the full contact record with enrichment data
- Code node maps the HubSpot owner ID to a Slack user ID using a lookup dictionary
- Slack node sends a Block Kit DM to the assigned rep (or a fallback channel for unassigned leads)
Step 1: Add a HubSpot Trigger node
Create a new workflow and add a HubSpot Trigger node:
- Authentication: Select your HubSpot credential
- Event: Form Submission
This fires every time someone submits a form in HubSpot. The payload includes the form ID and the contact's email.
HubSpot's form submission webhook fires for ALL forms. Add an IF node after the trigger to check $json.formId === 'YOUR_DEMO_FORM_ID' to only process demo bookings.
Step 2: Filter to the demo form
Add an IF node:
- Condition:
{{ $json.formId }}equalsYOUR_DEMO_FORM_ID
Only the "true" branch continues. This prevents alerts for newsletter signups, content downloads, and other forms.
Step 3: Fetch the contact record
Add an HTTP Request node to get the full contact:
- Method: GET
- URL:
https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.objectId }} - Authentication: Predefined -> HubSpot API
- Query params:
properties=firstname,lastname,email,jobtitle,company,numberofemployees,industry,hubspot_owner_id,hs_analytics_source
Step 4: Look up the owner's Slack user ID
Add a Code node to map the HubSpot owner ID to a Slack user ID:
const contact = $('Fetch Contact').first().json;
const props = contact.properties;
// HubSpot owner ID → Slack user ID mapping
const ownerToSlack = {
'12345678': 'U01AAAA', // Alice
'23456789': 'U02BBBB', // Bob
'34567890': 'U03CCCC', // Carol
'45678901': 'U04DDDD', // Dave
};
const ownerId = props.hubspot_owner_id;
const slackUserId = ownerToSlack[ownerId];
if (!slackUserId) {
// No owner or unmapped owner — send to a fallback channel
return [{ json: { ...props, contactId: contact.id, slackTarget: '#demo-alerts', isFallback: true } }];
}
return [{ json: {
contactId: contact.id,
name: `${props.firstname || ''} ${props.lastname || ''}`.trim(),
email: props.email,
title: props.jobtitle,
company: props.company,
employees: props.numberofemployees,
industry: props.industry,
source: props.hs_analytics_source,
slackTarget: slackUserId,
isFallback: false,
}}];If the lead doesn't have an owner yet (common if routing happens after the form submission), the Slack alert goes to a fallback channel. Chain this with a routing recipe to assign the owner first.
Step 5: Send a Slack DM with Block Kit
Add a Slack node:
- Resource: Message
- Operation: Send
- Channel:
{{ $json.slackTarget }} - Message Type: Block Kit
{
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "🔥 Demo Booked!" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Name:*\n{{ $json.name }}" },
{ "type": "mrkdwn", "text": "*Title:*\n{{ $json.title || 'Not provided' }}" },
{ "type": "mrkdwn", "text": "*Company:*\n{{ $json.company || 'Unknown' }}" },
{ "type": "mrkdwn", "text": "*Size:*\n{{ $json.employees || 'Unknown' }} employees" },
{ "type": "mrkdwn", "text": "*Industry:*\n{{ $json.industry || 'Unknown' }}" },
{ "type": "mrkdwn", "text": "*Source:*\n{{ $json.source || 'Unknown' }}" }
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "View Contact" },
"url": "https://app.hubspot.com/contacts/YOUR_PORTAL_ID/contact/{{ $json.contactId }}",
"style": "primary"
}
]
}
]
}Step 6: Activate
- Submit your demo form with test data
- Verify the Slack DM arrives with the correct contact info
- Toggle the workflow to Active
Troubleshooting
Common questions
Should I use a Form Submission trigger or a Contact Property Changed trigger?
Form Submission is more direct — it fires when someone submits a form. But it fires for ALL forms, so you need the IF node to filter. Contact Property Changed on recent_conversion_event_name is an alternative that works even for non-HubSpot forms (Typeform, Calendly) that sync to HubSpot. Choose based on how your demo bookings enter HubSpot.
How fast is the notification?
With the HubSpot Trigger node, notifications arrive within 1-5 seconds of the form submission. This is webhook-based, so there's no polling delay.
What if the lead doesn't have an assigned owner yet?
The Code node falls back to a #demo-alerts channel for unassigned leads. For best results, chain this with a lead routing recipe so the owner is assigned before the alert fires.
How do I handle a large sales team?
The owner-to-Slack mapping in the Code node works for teams up to ~20 reps. For larger teams, store the mapping in a Google Sheet or n8n's built-in data store, and look up the Slack user ID dynamically.
Cost
- n8n Cloud Starter: $24/mo for 2,500 executions. Each demo booking = 1 execution.
- Self-hosted: Free. Unlimited executions.
Looking to scale your AI operations?
We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.