This workflow corresponds to n8n.io template #10398 — we link there as the canonical source.
This workflow follows the Agent → Datatable recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"nodes": [
{
"id": "2bdfeaf7-f4db-446d-84bc-68e914138e28",
"name": "Structured Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
-112,
192
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"subject\": {\n \"type\": \"string\",\n \"description\": \"The subject line for the email\"\n },\n \"body\": {\n \"type\": \"string\",\n \"description\": \"The full, complete body of the email, formatted in Markdown\"\n }\n },\n \"required\": [\"subject\", \"body\"]\n}"
},
"typeVersion": 1.2
},
{
"id": "46c35571-3823-4cb9-8e07-df85c8fd6a2f",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2368,
240
],
"parameters": {
"color": 6,
"width": 368,
"height": 212,
"content": "## Contact me\n- If you need any modification to this workflow\n- if you need some help with this workflow\n- Or if you need any workflow in n8n, Make, or Langchain / Langgraph\n\nWrite to me: [thomas@pollup.net](mailto:thomas@pollup.net)\n\n**Take a look at my others workflows [here](https://n8n.io/creators/zeerobug/)**\n\n"
},
"typeVersion": 1
},
{
"id": "3d3a2b5f-c5cd-4fdc-bc1d-55956b6b7809",
"name": "If Deal Is Won",
"type": "n8n-nodes-base.if",
"position": [
-1824,
-32
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b70f4699-008f-4924-8e69-af4fa69422a5",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $(\"Trigger: Deal Is 'Closed Won'\").item.json.body[0].propertyValue }}",
"rightValue": "contact.creation"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "3b7f2eb6-c57f-454e-8bca-dece1d7ac508",
"name": "Configure Template Variables",
"type": "n8n-nodes-base.set",
"position": [
-2048,
-32
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "11a8b9e9-a7ed-454a-9aef-a9137c0e17ea",
"name": "company_name",
"type": "string",
"value": "Your Company Name"
},
{
"id": "f2dcfe2e-3145-4a30-9731-0a8d02c7aa9a",
"name": "sender_name",
"type": "string",
"value": "Your Sender Name"
},
{
"id": "18b5c0bd-4e75-4b98-92fc-5fca90a8b680",
"name": "sender_email",
"type": "string",
"value": "user@example.com"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "e26d9e1d-de10-445d-bbc7-aa20753c3bbb",
"name": "If Role is 'Champion'",
"type": "n8n-nodes-base.if",
"position": [
-480,
-32
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "90d9c369-80df-4938-adac-af3b452f97ca",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.properties.hs_buying_role.value }}",
"rightValue": "CHAMPION"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "e22c6e48-5899-4f13-b857-65d499e296ca",
"name": "HubSpot: Get Deal Details",
"type": "n8n-nodes-base.hubspot",
"position": [
-1152,
-32
],
"parameters": {
"dealId": {
"__rl": true,
"mode": "id",
"value": "={{ $(\"Trigger: Deal Is 'Closed Won'\").item.json.body[0].objectId }}"
},
"filters": {},
"resource": "deal",
"operation": "get",
"authentication": "oAuth2"
},
"typeVersion": 2.2
},
{
"id": "8f569481-1ee0-4441-8269-a0630cb116d3",
"name": "Split Contact IDs",
"type": "n8n-nodes-base.code",
"position": [
-928,
-32
],
"parameters": {
"jsCode": "// Get the list of contact IDs\nconst vids = $('HubSpot: Get Deal Details').first().json.associations.associatedVids;\n\n// Make sure it's always a list (even if there's only one)\nconst vidsList = [].concat(vids);\n\n// Return each ID as a separate item for the next node to process\nreturn vidsList.map(id => {\n return {\n json: {\n contactId: id\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "7ccef67d-88ba-4ee2-88b8-ea0888b57319",
"name": "HubSpot: Get Contact Details",
"type": "n8n-nodes-base.hubspot",
"position": [
-704,
-32
],
"parameters": {
"contactId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.contactId }}"
},
"operation": "get",
"authentication": "oAuth2",
"additionalFields": {
"propertiesCollection": {
"propertiesValues": {
"properties": [
"hs_buying_role"
],
"propertyMode": "valueOnly"
}
}
}
},
"typeVersion": 2.2
},
{
"id": "8f0f446d-2705-4e0b-a982-76a04c9010ca",
"name": "AI: Write Welcome Email",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-256,
-32
],
"parameters": {
"text": "=Your task is to write a warm, personalized welcome email to a new customer.\n\n## Content to Include\n- Congratulate them on their purchase.\n- Welcome them to the [Your Company Name] family.\n- Let them know that their new Customer Success Manager will be in touch shortly to schedule a kickoff call.\n- Provide a link to a \"Getting Started\" video: [Link to Your Video]\n- Provide a link to a \"Help Documentation\" page: [Link to Your Help Doc]\n\n## Rules\n- Be professional, enthusiastic, and welcoming.\n- You are writing on behalf of the sender.\n\n## Use the variables below\nSender's name: {{ $('Configure Template Variables').item.json.sender_name }}\nSender's email: {{ $('Configure Template Variables').item.json.sender_email }}\nSender's company name: {{ $('Configure Template Variables').item.json.company_name }}\nRecipient's first name: {{ $('HubSpot: Get Contact Details').item.json.properties.firstname.value }}\nRecipient's last name: {{ $('HubSpot: Get Contact Details').item.json.properties.lastname.value }}\nRecipient's email: {{ $('HubSpot: Get Contact Details').item.json.properties.email.value }}",
"options": {
"systemMessage": "=# Overview\nYou are a professional Customer Success Manager.\n"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.9
},
{
"id": "c3795f85-b2cf-4baf-86f5-7919234d1cad",
"name": "AI Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-256,
192
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "gpt-4o-mini"
},
"options": {}
},
"typeVersion": 1.2
},
{
"id": "7262a3e6-dae7-4011-ab90-54897deb9f19",
"name": "Util: Markdown to HTML",
"type": "n8n-nodes-base.markdown",
"position": [
96,
-32
],
"parameters": {
"mode": "markdownToHtml",
"options": {},
"markdown": "={{ $json.output.body }}"
},
"typeVersion": 1
},
{
"id": "366e7c5d-f6df-4896-bd32-ae1fba5b4f49",
"name": "Gmail: Send Welcome Email",
"type": "n8n-nodes-base.gmail",
"position": [
320,
-32
],
"parameters": {
"sendTo": "={{ $('HubSpot: Get Contact Details').item.json.properties.email.value }}",
"message": "={{ $json.data }}",
"options": {},
"subject": "={{ $json.output.subject }}"
},
"typeVersion": 2.1
},
{
"id": "1033acf3-11d8-4ea2-89f2-13ceda2e9f8d",
"name": "HubSpot: Assign Contact Owner",
"type": "n8n-nodes-base.hubspot",
"position": [
544,
-32
],
"parameters": {
"email": "={{ $('HubSpot: Get Contact Details').item.json.properties.email.value }}",
"options": {},
"authentication": "oAuth2",
"additionalFields": {
"contactOwner": "={{ $('Find Least Busy CSM').item.json.csm_id }}"
}
},
"typeVersion": 2.1
},
{
"id": "cb68f201-669e-4f65-a065-93e1e1586fd5",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2368,
-1104
],
"parameters": {
"width": 768,
"height": 656,
"content": "# Automate a CSM assignment and email send from a HubSpot onboarding\n\nThis workflow triggers when a HubSpot deal is marked `Closed Won`, assigns the **least busy CSM based on a Data Table**, and sends a personalized welcome email generated by AI.\n\n## How to Set Up\n1. **Create CSM Data Table:** You must first create an n8n Data Table with the following setup:\n * **Name:** `csm_assignments`\n * **Columns:** `csm_id` (String) and `deal_count` (Number)\n * **Data:** Add one row for each CSM, putting their HubSpot Owner ID in `csm_id` and `0` in `deal_count`.\n2. **Link Data Table Nodes:** Open the `Get CSM List` and `Increment CSM Deal Count` nodes. In the **Table** field, select the `csm_assignments` table you just created.\n3. **Configure Variables:** Open the **`Configure Template Variables`** node and fill in your sender info (`company_name`, `sender_name`, and `sender_email`).\n4. **Add Credentials:** Add your credentials to all other required nodes:\n * `Trigger: Deal Is 'Closed Won'`\n * `HubSpot: Get Deal Details`\n * `HubSpot: Get Contact Details`\n * `HubSpot: Assign Contact Owner`\n * `AI Model` (This node is connected to the AI agent and holds the credentials)\n * `Gmail: Send Welcome Email`\n5. **Customize AI Prompt:** Open the **`AI: Write Welcome Email`** node (the node you posted) and edit the prompt to match your company's tone.\n6. **Activate Workflow:** Save and activate the workflow."
},
"typeVersion": 1
},
{
"id": "c1c64eb1-7108-4579-8b77-3cadb79fab4d",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2464,
-352
],
"parameters": {
"color": 7,
"width": 352,
"height": 480,
"content": "This node is the **live** trigger for the workflow.\n\n* **Action:** It watches for changes to the \"Is closed won\" property on a HubSpot deal.\n* **Heads Up:** This trigger will fire when the property changes to **True** *and* when it changes back to **False**.\n* **Filtering:** The `If Deal Is Won` node downstream is essential, as it filters for the **True** case, allowing the rest of the workflow to run only when a deal is won.\n* **Credential:** This node requires a **\"HubSpot Developer API\"** credential to function."
},
"typeVersion": 1
},
{
"id": "d1cc6510-b720-4076-910a-b528a72b6f62",
"name": "Trigger: Deal Is 'Closed Won'",
"type": "n8n-nodes-base.hubspotTrigger",
"position": [
-2272,
-32
],
"parameters": {
"eventsUi": {
"eventValues": [
{
"name": "deal.propertyChange",
"property": "=hs_is_closed_won"
}
]
},
"additionalFields": {
"maxConcurrentRequests": 5
}
},
"typeVersion": 1
},
{
"id": "04545ef6-cae4-4c4e-bf96-ea4ac26be3ad",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
-304,
-352
],
"parameters": {
"color": 7,
"width": 336,
"height": 672,
"content": "### AI Email Writer\n* Uses AI to write a personalized Welcome Email.\n* Requires OpenAI credentials.\n* You can customize the prompt in the `AI: Write Welcome Email` node."
},
"typeVersion": 1
},
{
"id": "7d1fd6d1-7ded-4760-8158-55f9e9f4d209",
"name": "Get CSM List",
"type": "n8n-nodes-base.dataTable",
"position": [
-1600,
-32
],
"parameters": {
"operation": "get",
"returnAll": true,
"dataTableId": {
"__rl": true,
"mode": "id",
"value": ""
}
},
"typeVersion": 1
},
{
"id": "f5774385-f71b-4b5c-bf67-54d8f184af06",
"name": "Find Least Busy CSM",
"type": "n8n-nodes-base.code",
"position": [
-1376,
-32
],
"parameters": {
"jsCode": "// Get the list of CSMs from the previous node\nconst csmList = $input.all();\n\n// Check if the table is empty\nif (csmList.length === 0) {\n throw new Error(\"CSM assignment table is empty. Please add CSMs to the Data Table.\");\n}\n\n// Sort the list to find the CSM with the lowest deal_count\n// The input item looks like: { json: { csm_id: '123', deal_count: 0 }, row_id: 'abc' }\nconst sortedList = csmList.sort((a, b) => {\n return a.json.deal_count - b.json.deal_count;\n});\n\n// The least busy CSM is the first one in the sorted list\nconst leastBusyCsm = sortedList[0];\n\n// Return the data needed for the next nodes\nreturn {\n csm_id: leastBusyCsm.json.csm_id,\n row_id_to_update: leastBusyCsm.json.id,\n current_count: leastBusyCsm.json.deal_count\n};"
},
"typeVersion": 2
},
{
"id": "46936c4a-7dcf-4cc0-bce9-6f06d230ee92",
"name": "Increment CSM Deal Count",
"type": "n8n-nodes-base.dataTable",
"position": [
768,
-32
],
"parameters": {
"columns": {
"value": {},
"schema": [],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"filters": {
"conditions": [
{
"keyValue": "={{ $('Find Least Busy CSM').item.json.row_id_to_update }}"
}
]
},
"options": {},
"operation": "update",
"dataTableId": {
"__rl": true,
"mode": "id",
"value": ""
}
},
"typeVersion": 1
},
{
"id": "426da06b-eb9f-4a6d-8b13-a2cffd6dd6ba",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1664,
-352
],
"parameters": {
"color": 7,
"width": 432,
"height": 480,
"content": "### Least-Busy CSM Assignment\n\nThese nodes find the CSM with the lowest `deal_count` from the `csm_assignments` Data Table, assign them the new deal, and then add 1 to their count to keep the table in sync."
},
"typeVersion": 1
},
{
"id": "e9606b09-3cbc-4f51-a1e9-23ba1a617aa4",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
704,
-352
],
"parameters": {
"color": 7,
"width": 544,
"height": 512,
"content": "This node updates the CSM's deal count.\n\n**After you select your `csm_assignments` table:**\n1. Go to the \"Fields to Update\" section.\n2. Click **Add Field**.\n3. **Field:** Select `deal_count`.\n4. **Value:** Switch to expression mode (`f(x)`) and paste:\n `{{ $('Find Least Busy CSM').item.json.current_count + 1 }}`"
},
"typeVersion": 1
}
],
"connections": {
"AI Model": {
"ai_languageModel": [
[
{
"node": "AI: Write Welcome Email",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Get CSM List": {
"main": [
[
{
"node": "Find Least Busy CSM",
"type": "main",
"index": 0
}
]
]
},
"If Deal Is Won": {
"main": [
[
{
"node": "Get CSM List",
"type": "main",
"index": 0
}
]
]
},
"Split Contact IDs": {
"main": [
[
{
"node": "HubSpot: Get Contact Details",
"type": "main",
"index": 0
}
]
]
},
"Find Least Busy CSM": {
"main": [
[
{
"node": "HubSpot: Get Deal Details",
"type": "main",
"index": 0
}
]
]
},
"If Role is 'Champion'": {
"main": [
[
{
"node": "AI: Write Welcome Email",
"type": "main",
"index": 0
}
]
]
},
"Util: Markdown to HTML": {
"main": [
[
{
"node": "Gmail: Send Welcome Email",
"type": "main",
"index": 0
}
]
]
},
"AI: Write Welcome Email": {
"main": [
[
{
"node": "Util: Markdown to HTML",
"type": "main",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "AI: Write Welcome Email",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Gmail: Send Welcome Email": {
"main": [
[
{
"node": "HubSpot: Assign Contact Owner",
"type": "main",
"index": 0
}
]
]
},
"HubSpot: Get Deal Details": {
"main": [
[
{
"node": "Split Contact IDs",
"type": "main",
"index": 0
}
]
]
},
"Configure Template Variables": {
"main": [
[
{
"node": "If Deal Is Won",
"type": "main",
"index": 0
}
]
]
},
"HubSpot: Get Contact Details": {
"main": [
[
{
"node": "If Role is 'Champion'",
"type": "main",
"index": 0
}
]
]
},
"HubSpot: Assign Contact Owner": {
"main": [
[
{
"node": "Increment CSM Deal Count",
"type": "main",
"index": 0
}
]
]
},
"Trigger: Deal Is 'Closed Won'": {
"main": [
[
{
"node": "Configure Template Variables",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This template is for Customer Success and Sales teams who use HubSpot. It automates the critical handoff from sales to success, ensuring every new customer gets a fast, personalized welcome. It's perfect for anyone looking to standardize their onboarding process, save time on…
Source: https://n8n.io/workflows/10398/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
How it works:
🧠 AI-Powered Lead Routing and Sales Team Distribution
This template enables natural-language-driven automation using Bright Data's MCP tools, triggered directly by new leads in HubSpot. It dynamically extracts and executes the right tool based on lead co
This workflow automates end-to-end contract and invoice management using AI intelligence. It processes proposals through intelligent contract generation, approval workflows, and automated invoicing. O
Automates SaaS operations by consolidating user management, AI-driven support triage, analytics, and billing into one unified system. User signups flow through registration, support requests route via