This workflow follows the Google Calendar → Slack 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 →
{
"name": "24-hvac-field-service-orchestration",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "hvac-service-request",
"responseMode": "lastNode",
"options": {}
},
"id": "d4e5f6g7-1111-4444-8888-000000000001",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
500
]
},
{
"parameters": {
"model": "gpt-4o",
"prompt": {
"messages": [
{
"role": "system",
"content": "You are an expert HVAC dispatch AI. Analyze the service request. Extract: 1. device_type (e.g., AC, Heater, Refrigerator), 2. issue_summary (1 sentence), 3. required_parts (array of strings, e.g., ['compressor', 'freon']), 4. urgency (critical, high, normal). Respond ONLY in valid JSON format with these exact keys."
},
{
"role": "user",
"content": "Customer: {{ $json.customer_name }}\nAddress: {{ $json.customer_address }}\nIssue Description: {{ $json.issue_description }}"
}
]
},
"options": {
"responseFormat": "json_object"
}
},
"id": "d4e5f6g7-2222-4444-8888-000000000002",
"name": "OpenAI",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1.2,
"position": [
460,
500
],
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT id, name, lat, lon, skills, current_load, van_inventory FROM technicians WHERE status = 'active';",
"options": {}
},
"id": "d4e5f6g7-3333-4444-8888-000000000003",
"name": "PostgreSQL",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
680,
500
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Score technicians returned by PostgreSQL against the original request and AI analysis.\nconst webhookItem = $('Webhook').first().json;\nconst request = webhookItem.body || webhookItem;\nconst customerLat = Number(request.customer_lat);\nconst customerLon = Number(request.customer_lon);\n\nif (!Number.isFinite(customerLat) || !Number.isFinite(customerLon)) {\n throw new Error('customer_lat and customer_lon must be valid numbers');\n}\n\nconst aiResponse = $('OpenAI').first().json;\nconst rawContent = aiResponse.content\n ?? aiResponse.message?.content\n ?? aiResponse.choices?.[0]?.message?.content\n ?? '{}';\n\nlet aiAnalysis;\ntry {\n aiAnalysis = typeof rawContent === 'string' ? JSON.parse(rawContent) : rawContent;\n} catch (error) {\n throw new Error('AI analysis is not valid JSON');\n}\n\nconst normalizeList = (value) => {\n if (Array.isArray(value)) return value.map((item) => String(item).trim().toLowerCase());\n if (typeof value === 'string') {\n return value.replace(/[{}]/g, '').split(',').map((item) => item.trim().replace(/^\"|\"$/g, '').toLowerCase()).filter(Boolean);\n }\n return [];\n};\n\nconst technicians = $input.all().map(({ json }) => ({\n id: String(json.id ?? ''),\n name: String(json.name ?? 'Technician'),\n lat: Number(json.lat),\n lon: Number(json.lon),\n skills: normalizeList(json.skills),\n currentLoad: Math.max(0, Number(json.current_load) || 0),\n inventory: normalizeList(json.van_inventory),\n})).filter((tech) => tech.id && Number.isFinite(tech.lat) && Number.isFinite(tech.lon));\n\nconst requiredSkill = String(aiAnalysis.device_type ?? '').trim().toLowerCase();\nconst requiredParts = normalizeList(aiAnalysis.required_parts);\n\nfunction distanceKm(lat1, lon1, lat2, lon2) {\n const radius = 6371;\n const dLat = (lat2 - lat1) * Math.PI / 180;\n const dLon = (lon2 - lon1) * Math.PI / 180;\n const a = Math.sin(dLat / 2) ** 2\n + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;\n return radius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n}\n\nconst candidates = technicians\n .filter((tech) => tech.skills.includes(requiredSkill))\n .map((tech) => {\n const distance = distanceKm(customerLat, customerLon, tech.lat, tech.lon);\n const proximityScore = Math.max(0, 100 - distance * 5);\n const partsAvailable = requiredParts.every((part) => tech.inventory.includes(part));\n const partsScore = partsAvailable ? 100 : 0;\n const loadScore = Math.max(0, 100 - tech.currentLoad * 15);\n const score = proximityScore * 0.4 + partsScore * 0.4 + loadScore * 0.2;\n return { ...tech, distanceKm: Number(distance.toFixed(2)), partsAvailable, score: Number(score.toFixed(2)) };\n })\n .sort((a, b) => b.score - a.score || a.distanceKm - b.distanceKm || a.id.localeCompare(b.id));\n\nconst bestTech = candidates[0] || null;\nconst basePrice = aiAnalysis.urgency === 'critical' ? 150 : 100;\nconst travelFee = bestTech ? bestTech.distanceKm * 1.5 : 0;\nconst partsFee = requiredParts.length * 25;\n\nreturn [{ json: {\n request_id: request.request_id || null,\n customer_name: request.customer_name || null,\n customer_address: request.customer_address || null,\n ai_analysis: aiAnalysis,\n assigned_technician: bestTech,\n final_price_usd: Number((basePrice + travelFee + partsFee).toFixed(2)),\n parts_available: bestTech?.partsAvailable === true,\n dispatch_status: bestTech?.partsAvailable === true ? 'AUTO_DISPATCH_READY' : 'MANUAL_REVIEW_REQUIRED',\n candidate_count: candidates.length,\n} }];"
},
"id": "d4e5f6g7-4444-4444-8888-000000000004",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
500
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond1",
"leftValue": "={{ $json.parts_available }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equal"
}
},
{
"id": "cond2",
"leftValue": "={{ $json.assigned_technician }}",
"rightValue": "",
"operator": {
"type": "object",
"operation": "notEmpty"
}
}
],
"combinator": "and"
}
}
]
},
"options": {
"fallbackOutput": "default"
}
},
"id": "d4e5f6g7-5555-4444-8888-000000000005",
"name": "Switch",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
1120,
500
]
},
{
"parameters": {
"calendar": {
"__rl": true,
"value": "primary",
"mode": "list"
},
"start": "={{ new Date(Date.now() + 86400000).toISOString() }}",
"end": "={{ new Date(Date.now() + 90000000).toISOString() }}",
"summary": "Service: {{ $json.ai_analysis.device_type }} for {{ $json.customer_name }}",
"description": "Tech: {{ $json.assigned_technician.name }}\nAddress: {{ $json.customer_address }}\nIssue: {{ $json.ai_analysis.issue_summary }}\nEstimated Price: ${{ $json.final_price_usd }}",
"additionalFields": {
"attendees": [
"tech@example.com"
]
}
},
"id": "d4e5f6g7-6666-4444-8888-000000000006",
"name": "Google Calendar",
"type": "n8n-nodes-base.googleCalendar",
"typeVersion": 1.1,
"position": [
1340,
420
],
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"fromNumber": "+1234567890",
"toNumber": "+989123456789",
"message": "Hello {{ $json.customer_name }}. Your {{ $json.ai_analysis.device_type }} service is confirmed! Tech: {{ $json.assigned_technician.name }} will arrive tomorrow. Est. Price: ${{ $json.final_price_usd }}. Reply STOP to opt out.",
"options": {}
},
"id": "d4e5f6g7-7777-4444-8888-000000000007",
"name": "Twilio",
"type": "n8n-nodes-base.twilio",
"typeVersion": 1,
"position": [
1560,
420
],
"credentials": {
"twilioApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"value": "C9988776655",
"mode": "list",
"cachedResultName": "dispatch-manager-alerts"
},
"text": "\u26a0\ufe0f *SERVICE REQUEST UNASSIGNED* \u26a0\ufe0f\n\nCustomer: {{ $json.customer_name }}\nIssue: {{ $json.ai_analysis.issue_summary }}\nReason: No technician with required skills/parts available.\n\nAction: Manual dispatch required or order parts.",
"otherOptions": {}
},
"id": "d4e5f6g7-8888-4444-8888-000000000008",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.2,
"position": [
1340,
580
],
"credentials": {
"slackApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "OpenAI",
"type": "main",
"index": 0
}
]
]
},
"OpenAI": {
"main": [
[
{
"node": "PostgreSQL",
"type": "main",
"index": 0
}
]
]
},
"PostgreSQL": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Switch": {
"main": [
[
{
"node": "Google Calendar",
"type": "main",
"index": 0
}
],
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Google Calendar": {
"main": [
[
{
"node": "Twilio",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all"
},
"id": "24-hvac-field-service-orchestration",
"tags": []
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleCalendarOAuth2ApiopenAiApipostgresslackApitwilioApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
24-hvac-field-service-orchestration. Uses openAi, postgres, googleCalendar, twilio. Webhook trigger; 8 nodes.
Source: https://github.com/kooroosh1363/agentic-automation-lab/blob/main/24-hvac-field-service-orchestration/workflow.json — 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.
The system collects real-time wearable health data, normalizes it, and uses AI to analyze trends and risk scores. It detects anomalies by comparing with historical patterns and automatically triggers
Eastside Remodels - Lead Nurturing AI Agent. Uses openAi, googleCalendar, salesforce, emailSend. Webhook trigger; 13 nodes.
This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La
Pyragogy AI Village - Orchestrazione Master (Architettura Profonda V2). Uses start, postgres, openAi, emailSend. Webhook trigger; 37 nodes.
AI-Driven Handbook Generator with Multi-Agent Orchestration (Pyragogy AI Village). Uses start, postgres, openAi, emailSend. Webhook trigger; 36 nodes.