This workflow corresponds to n8n.io template #15780 — we link there as the canonical source.
This workflow follows the Airtable → Gmail 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 →
{
"id": "GGpGB62a2dvUHnPN",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Triage property maintenance tickets and assign vendors with Airtable, Twilio & Gmail",
"tags": [
{
"id": "N0cknoA6lPH5cSHa",
"name": "Property Management",
"createdAt": "2025-11-14T12:07:44.588Z",
"updatedAt": "2025-11-14T12:07:44.588Z"
}
],
"nodes": [
{
"id": "281d8d55-262e-4785-beea-36859e32e82f",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"color": 4,
"width": 900,
"height": 1440,
"content": "### Triage Property Maintenance Tickets and Assign Vendors with Airtable, Twilio & Gmail\n\nThis n8n template demonstrates how to automatically triage tenant maintenance requests, match them to the right contractor by category and postcode area, dispatch the job by email and SMS, and handle duplicate tickets \u2014 eliminating the manual triage work that eats up property managers' days.\n\n### Use Cases\n\n- Property managers handling maintenance requests across multiple buildings\n- Lettings agencies that want faster contractor response times for tenants\n- Short-let and HMO operators tired of duplicate \"is anyone fixing this?\" tickets\n\n### How It Works\n\n1. **Trigger**: Airtable Trigger polls the `Maintenance Tickets` table every minute for new tenant submissions.\n2. **Duplicate Detection**: Searches for tickets from the same tenant, same property, and same category within the last 24 hours:\n - If duplicate found:\n - Updates ticket status to **Possible Duplicate**\n - Sends an alert email to the admin\n - If not a duplicate:\n - Continues to the triage flow\n3. **Postcode Normalisation**: Extracts the postcode district (e.g. `SW1A 1AA` \u2192 `SW1`) so vendors can be matched on service area rather than exact postcode.\n4. **Vendor Matching**: Searches the `Vendors` table for active vendors who:\n - Match the issue category (Plumbing, Electrical, etc.)\n - Serve the postcode district\n - Are marked as Active\n - Sorts results by response time and selects the fastest match\n5. **Routing**:\n - If a vendor is found:\n - Updates the ticket: vendor assigned, status **Assigned**, dispatch timestamp\n - Sends a Gmail to the vendor with property address, issue, priority, tenant contact, and reference number\n - Sends a Twilio SMS to the vendor for instant phone-level notification\n - Sends a Gmail confirmation to the tenant with the assigned contractor and expected response time\n - If no vendor is found:\n - Updates ticket status to **Pending Manual Assignment**\n - Sends an alert email to the admin\n - Sends a holding email to the tenant so they know the request landed\n\n### Customisation Options\n\n- Swap Twilio with MessageBird, Vonage, or any SMS node\n- Replace Gmail with SendGrid, Outlook, or SMTP\n- Replace Airtable with Notion, Google Sheets, or your existing PMS\n- Add a Slack notification on the manual fallback branch\n- Adjust the duplicate window inside the **Evaluate Duplicates** code node (default: 24 hours)\n- Add a priority-based escalation path so Emergency tickets trigger a Twilio voice call\n\n### Prerequisites/Credential Setup\n\nTo use this workflow securely, you'll need the following credentials set up in n8n:\n\n- **Airtable Personal Access Token** \u2013 to read and update the `Vendors` and `Maintenance Tickets` tables\n- **Gmail OAuth2** \u2013 to send vendor, tenant, and admin emails\n- **Twilio API credentials** \u2013 with at least one verified `from` number for outbound SMS\n\nYou'll also need an Airtable base with two tables:\n\n- **Vendors** \u2013 `Name`, `Email`, `Phone`, `Categories`, `Service Areas` (postcode districts), `Response Time`, `Active`\n- **Maintenance Tickets** \u2013 tenant details, property address, category, priority, description, status, `Assigned Vendor` linked field\n\n### Secure Configuration\n\n- All credential fields use **n8n Credential Types**\n- No API keys, base IDs, table IDs, or admin emails are hardcoded\n- Placeholder values are clearly marked (e.g. `appXXXXXXXXXXXXXX`, `+15551234567`) for safe import\n\n### Why This Helps\n\n- Eliminates manual triage and contractor sourcing for routine requests\n- Cuts response time on time-sensitive issues like leaks and outages\n- Stops duplicate tickets from snowballing into multiple dispatches and angry tenants\n- Maintains a clean audit trail of every assignment and exception in Airtable\n\n---\n\nWith this template, property managers can automate the entire maintenance triage flow, dispatch contractors in seconds, and keep tenants informed without chasing emails.\n"
},
"typeVersion": 1
},
{
"id": "1d98835b-810c-4929-a27a-9fcda3d4d85e",
"name": "Watch for New Records",
"type": "n8n-nodes-base.airtableTrigger",
"position": [
1056,
928
],
"parameters": {
"baseId": {
"__rl": true,
"mode": "url",
"value": "https://airtable.com/appXXXXXXXXXXXXXX/tblVendorsXXXXXXX/viwtO9MK0IbII0K5z?blocks=hide"
},
"tableId": {
"__rl": true,
"mode": "url",
"value": "https://airtable.com/appXXXXXXXXXXXXXX/tblTicketsXXXXXX/viw12ORkBCRFZAPIC?blocks=hide"
},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"triggerField": "Submitted At",
"authentication": "airtableTokenApi",
"additionalFields": {}
},
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "2389fff0-7cf2-49cf-bc66-baf92e029cb0",
"name": "Search records",
"type": "n8n-nodes-base.airtable",
"onError": "continueRegularOutput",
"position": [
2464,
928
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX",
"cachedResultName": "Property Manager Base"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblVendorsXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX/tblVendorsXXXXXXX",
"cachedResultName": "Vendors"
},
"options": {},
"operation": "search",
"filterByFormula": "=AND(\n {Category} = \"{{ $json.fields['Issue Category'] }}\",\n FIND(\"{{ $json.postcodeDistrict }}\", ARRAYJOIN({Service Areas}, \",\")),\n {Status} = \"Active\"\n)"
},
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1,
"alwaysOutputData": true
},
{
"id": "3aad8a0b-9e0b-4987-9b76-206a9d7415df",
"name": "Extract Postcode District",
"type": "n8n-nodes-base.code",
"position": [
2192,
928
],
"parameters": {
"jsCode": "// Get the item coming into this node (from the Duplicate? IF node)\nconst currentItem = $input.first().json;\n\n// Always pull the Postcode from the original Airtable trigger\nconst triggerItem = $('Watch for New Records').first().json;\nconst fullPostcode = triggerItem.fields?.Postcode;\n\nif (!fullPostcode) {\n // Failsafe: don't crash the workflow if Postcode is missing\n return {\n json: {\n ...currentItem,\n postcodeDistrict: null,\n },\n };\n}\n\n// Extract postcode district (e.g., \"SW1A 1AA\" \u2192 \"SW1\")\nconst postcodeDistrict = fullPostcode\n .toString()\n .split(' ')[0] // \"SW1A\"\n .toUpperCase()\n .replace(/[A-Z]+$/, ''); // \"SW1\"\n\nreturn {\n json: {\n ...currentItem,\n postcodeDistrict,\n },\n};\n"
},
"typeVersion": 2
},
{
"id": "70ab5414-fd24-4b3b-9f19-d5a1d4940e7d",
"name": "Check if Vendor Found",
"type": "n8n-nodes-base.if",
"position": [
2736,
928
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "115fb26a-f211-441e-9d6d-c190178726e6",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ Object.keys($json).length > 0 }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "96d79b36-7a06-45dd-9fe1-4e61cf342e09",
"name": "Limit",
"type": "n8n-nodes-base.limit",
"position": [
3008,
912
],
"parameters": {},
"typeVersion": 1
},
{
"id": "5bfaed05-8785-4466-871b-4ba25632e142",
"name": "Update Ticket with Assigned Vendor",
"type": "n8n-nodes-base.airtable",
"position": [
3296,
912
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX",
"cachedResultName": "Property Manager Base"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblTicketsXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX/tblTicketsXXXXXX",
"cachedResultName": "Maintenance Tickets"
},
"columns": {
"value": {
"id": "={{ $('Watch for New Records').first().json.id }}",
"Status": "Assigned",
"Assigned Vendor": "=[\"{{ $('Limit').first().json.id }}\"]",
"Vendor Notified At": "={{ $now.toISO() }}"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "id",
"defaultMatch": true
},
{
"id": "Ticket ID",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "Ticket ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Submitted At",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "Submitted At",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tenant Name",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Tenant Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tenant Email",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Tenant Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Property Address",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Property Address",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Postcode",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Postcode",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Issue Category",
"type": "options",
"display": true,
"options": [
{
"name": "Plumbing",
"value": "Plumbing"
},
{
"name": "Electrical",
"value": "Electrical"
},
{
"name": "Heating",
"value": "Heating"
},
{
"name": "General Maintenance",
"value": "General Maintenance"
},
{
"name": "Locksmith",
"value": "Locksmith"
},
{
"name": "Pest Control",
"value": "Pest Control"
}
],
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Issue Category",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Issue Description",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Issue Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Priority",
"type": "options",
"display": true,
"options": [
{
"name": "Low",
"value": "Low"
},
{
"name": "Medium",
"value": "Medium"
},
{
"name": "High",
"value": "High"
},
{
"name": "Emergency",
"value": "Emergency"
}
],
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Priority",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Assigned Vendor",
"type": "array",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Assigned Vendor",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Vendor Notified At",
"type": "dateTime",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Vendor Notified At",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "options",
"display": true,
"options": [
{
"name": "New",
"value": "New"
},
{
"name": "Assigned",
"value": "Assigned"
},
{
"name": "In Progress",
"value": "In Progress"
},
{
"name": "Completed",
"value": "Completed"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"id"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update"
},
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "fc5cdab7-1087-4a26-9934-dec6aec83a50",
"name": "Send SMS to Vendor (Twilio)",
"type": "n8n-nodes-base.twilio",
"position": [
3856,
912
],
"parameters": {
"to": "={{ $('Limit').first().json.Phone }}",
"from": "+1234567890",
"message": "=New Maintenance Job - {{ $('Watch for New Records').first().json.fields['Issue Category'] }}\n\nProperty: {{ $('Watch for New Records').first().json.fields.Postcode }}\nAddress: {{ $('Watch for New Records').first().json.fields['Property Address'] }}\nPriority: {{ $('Watch for New Records').first().json.fields.Priority }}\n\nTenant: {{ $('Watch for New Records').first().json.fields['Tenant Name'] }}\n\nDetails sent to your email.\nTicket: #{{ $('Watch for New Records').first().json.fields['Ticket ID'] }}",
"options": {}
},
"credentials": {
"twilioApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "e06dfadd-108c-42b3-a081-118906ba5a85",
"name": "Send Email to Vendor",
"type": "n8n-nodes-base.gmail",
"position": [
3584,
912
],
"parameters": {
"sendTo": "={{ $('Search records').first().json.Email }}",
"message": "=<!DOCTYPE html>\n<html>\n<head>\n <style>\n body {\n font-family: Arial, sans-serif;\n line-height: 1.6;\n color: #333;\n }\n .container {\n max-width: 600px;\n margin: 0 auto;\n padding: 20px;\n }\n .header {\n background-color: #FF9800;\n color: white;\n padding: 20px;\n text-align: center;\n border-radius: 5px 5px 0 0;\n }\n .content {\n background-color: #f9f9f9;\n padding: 30px;\n border: 1px solid #ddd;\n }\n .section {\n background-color: white;\n padding: 15px;\n margin: 15px 0;\n border-left: 4px solid #FF9800;\n border-radius: 3px;\n }\n .section h3 {\n margin-top: 0;\n color: #FF9800;\n }\n .info-row {\n padding: 8px 0;\n border-bottom: 1px solid #eee;\n }\n .info-label {\n font-weight: bold;\n display: inline-block;\n width: 150px;\n color: #555;\n }\n .priority-badge {\n display: inline-block;\n padding: 5px 15px;\n border-radius: 20px;\n font-weight: bold;\n font-size: 14px;\n }\n .priority-high {\n background-color: #f44336;\n color: white;\n }\n .priority-medium {\n background-color: #FF9800;\n color: white;\n }\n .priority-low {\n background-color: #4CAF50;\n color: white;\n }\n .priority-emergency {\n background-color: #d32f2f;\n color: white;\n animation: pulse 1.5s infinite;\n }\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.7; }\n }\n .footer {\n text-align: center;\n padding: 20px;\n color: #777;\n font-size: 12px;\n }\n .action-box {\n background-color: #fff3cd;\n padding: 15px;\n border-radius: 5px;\n margin: 20px 0;\n border-left: 4px solid #ffc107;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h2>\ud83d\udd27 New Maintenance Job Assignment</h2>\n </div>\n \n <div class=\"content\">\n <p>Hi <strong>{{ $('Limit').first().json['Vendor Name'] }}</strong>,</p>\n \n <p>You have been assigned a new maintenance job. Please review the details below and contact the tenant to schedule a visit.</p>\n \n <div class=\"section\">\n <h3>\ud83d\udccd Property Details</h3>\n <div class=\"info-row\">\n <span class=\"info-label\">Address:</span>\n {{ $('Watch for New Records').first().json.fields['Property Address'] }}\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Postcode:</span>\n <strong>{{ $('Watch for New Records').first().json.fields.Postcode }}</strong>\n </div>\n </div>\n \n <div class=\"section\">\n <h3>\ud83d\udd28 Issue Details</h3>\n <div class=\"info-row\">\n <span class=\"info-label\">Category:</span>\n <strong>{{ $('Watch for New Records').first().json.fields['Issue Category'] }}</strong>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Priority:</span>\n <span style=\"\n display: inline-block;\n padding: 6px 15px;\n border-radius: 20px;\n font-weight: bold;\n font-size: 14px;\n background-color: {{ $('Watch for New Records').first().json.fields.Priority === 'Emergency' ? '#d32f2f' : $('Watch for New Records').first().json.fields.Priority === 'High' ? '#f44336' : $('Watch for New Records').first().json.fields.Priority === 'Medium' ? '#FF9800' : '#4CAF50' }};\n color: white;\n \">\n {{ $('Watch for New Records').first().json.fields.Priority }}\n </span>\n</div>\n <div class=\"info-row\">\n <span class=\"info-label\">Description:</span>\n </div>\n <div style=\"background-color: #f5f5f5; padding: 15px; margin-top: 10px; border-radius: 3px; font-style: italic;\">\n {{ $('Watch for New Records').first().json.fields['Issue Description'] }}\n </div>\n </div>\n \n <div class=\"section\">\n <h3>\ud83d\udc64 Tenant Contact</h3>\n <div class=\"info-row\">\n <span class=\"info-label\">Name:</span>\n <strong>{{ $('Watch for New Records').first().json.fields['Tenant Name'] }}</strong>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Email:</span>\n <a href=\"mailto:{{ $('Watch for New Records').first().json.fields['Tenant Email'] }}\">{{ $('Watch for New Records').first().json.fields['Tenant Email'] }}</a>\n </div>\n </div>\n \n <div class=\"action-box\">\n <strong>\u26a1 Action Required:</strong>\n <p style=\"margin: 10px 0 0 0;\">Please acknowledge receipt of this job and contact the tenant to schedule a convenient time for the visit.</p>\n </div>\n \n <div style=\"background-color: #e3f2fd; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #2196F3;\">\n <strong>\ud83d\udccb Ticket Reference:</strong> #{{ $('Watch for New Records').first().json.fields['Ticket ID'] }}\n </div>\n </div>\n \n <div class=\"footer\">\n <p><strong>Property Management Team</strong></p>\n <p style=\"font-size: 11px;\">This is an automated job assignment. Please contact the tenant directly to arrange the visit.</p>\n </div>\n </div>\n</body>\n</html>",
"options": {
"appendAttribution": false
},
"subject": "=New Maintenance Job - New Maintenance Job - {{ $('Watch for New Records').first().json.fields['Issue Category'] }} - {{ $('Watch for New Records').first().json.fields.Postcode }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "eeef7368-44a1-48d7-bc83-9242e0b002ee",
"name": "Send Confirmation to Tenant",
"type": "n8n-nodes-base.gmail",
"position": [
4112,
912
],
"parameters": {
"sendTo": "={{ $('Watch for New Records').first().json.fields['Tenant Email'] }}",
"message": "=<!DOCTYPE html>\n<html>\n<head>\n <style>\n body {\n font-family: Arial, sans-serif;\n line-height: 1.6;\n color: #333;\n }\n .container {\n max-width: 600px;\n margin: 0 auto;\n padding: 20px;\n }\n .header {\n background-color: #4CAF50;\n color: white;\n padding: 20px;\n text-align: center;\n border-radius: 5px 5px 0 0;\n }\n .content {\n background-color: #f9f9f9;\n padding: 30px;\n border: 1px solid #ddd;\n }\n .section {\n background-color: white;\n padding: 15px;\n margin: 15px 0;\n border-left: 4px solid #4CAF50;\n border-radius: 3px;\n }\n .section h3 {\n margin-top: 0;\n color: #4CAF50;\n }\n .info-row {\n padding: 8px 0;\n border-bottom: 1px solid #eee;\n }\n .info-label {\n font-weight: bold;\n display: inline-block;\n width: 150px;\n }\n .footer {\n text-align: center;\n padding: 20px;\n color: #777;\n font-size: 12px;\n }\n .button {\n background-color: #4CAF50;\n color: white;\n padding: 12px 30px;\n text-decoration: none;\n border-radius: 5px;\n display: inline-block;\n margin: 15px 0;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h2>\u2713 Maintenance Request Confirmed</h2>\n </div>\n \n <div class=\"content\">\n <p>Hi <strong>{{ $('Watch for New Records').first().json.fields['Tenant Name'] }}</strong>,</p>\n \n <p>Thank you for submitting your maintenance request. We've assigned your issue to a qualified contractor who will be in touch shortly.</p>\n \n <div class=\"section\">\n <h3>\ud83d\udccb Your Request</h3>\n <div class=\"info-row\">\n <span class=\"info-label\">Ticket ID:</span>\n <strong>#{{ $('Watch for New Records').first().json.fields['Ticket ID'] }}</strong>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Issue Type:</span>\n {{ $('Watch for New Records').first().json.fields['Issue Category'] }}\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Priority:</span>\n <span style=\"\n display: inline-block;\n padding: 5px 12px;\n border-radius: 4px;\n font-weight: bold;\n background-color: {{ $('Watch for New Records').first().json.fields.Priority === 'Emergency' ? '#d32f2f' : $('Watch for New Records').first().json.fields.Priority === 'High' ? '#f44336' : $('Watch for New Records').first().json.fields.Priority === 'Medium' ? '#FF9800' : '#4CAF50' }};\n color: white;\n \">\n {{ $('Watch for New Records').first().json.fields.Priority }}\n </span>\n</div>\n <div class=\"info-row\">\n <span class=\"info-label\">Property:</span>\n {{ $('Watch for New Records').first().json.fields['Property Address'] }}\n </div>\n </div>\n \n <div class=\"section\">\n <h3>\ud83d\udc77 Assigned Contractor</h3>\n <div class=\"info-row\">\n <span class=\"info-label\">Company:</span>\n <strong>{{ $('Limit').first().json['Vendor Name'] }}</strong>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Expected Response:</span>\n {{ $('Limit').first().json['Response Time'] }}\n </div>\n </div>\n \n <div style=\"background-color: #fff3cd; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #ffc107;\">\n <strong>\u23f0 What happens next?</strong>\n <p style=\"margin: 10px 0 0 0;\">The contractor will contact you directly to schedule a convenient time for the visit.</p>\n </div>\n \n <div style=\"text-align: center; margin-top: 30px;\">\n <p style=\"color: #777;\">Need urgent assistance?</p>\n <p style=\"font-size: 18px; color: #4CAF50; font-weight: bold;\">\ud83d\udcde 074 1237 1905</p>\n </div>\n </div>\n \n <div class=\"footer\">\n <p>This is an automated message from your Property Management Team.</p>\n <p style=\"font-size: 11px;\">Please do not reply directly to this email.</p>\n </div>\n </div>\n</body>\n</html>",
"options": {
"senderName": "The Property Manager"
},
"subject": "=Maintenance Request Confirmed - Ticket #{{ $('Watch for New Records').first().json.fields['Ticket ID'] }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "68de7c58-c06d-4480-93cf-6f32171a72b1",
"name": "Email to Admin - Alert",
"type": "n8n-nodes-base.gmail",
"position": [
3168,
1168
],
"parameters": {
"sendTo": "admin@example.com",
"message": "=<!DOCTYPE html>\n<html>\n<head>\n <style>\n body {\n font-family: Arial, sans-serif;\n line-height: 1.6;\n color: #333;\n }\n .container {\n max-width: 600px;\n margin: 0 auto;\n padding: 20px;\n }\n .header {\n background-color: #d32f2f;\n color: white;\n padding: 20px;\n text-align: center;\n border-radius: 5px 5px 0 0;\n }\n .header h2 {\n margin: 0;\n animation: pulse 2s infinite;\n }\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.7; }\n }\n .content {\n background-color: #f9f9f9;\n padding: 30px;\n border: 2px solid #d32f2f;\n }\n .alert-box {\n background-color: #ffebee;\n padding: 20px;\n margin: 20px 0;\n border-left: 5px solid #d32f2f;\n border-radius: 3px;\n }\n .section {\n background-color: white;\n padding: 15px;\n margin: 15px 0;\n border-left: 4px solid #d32f2f;\n border-radius: 3px;\n }\n .section h3 {\n margin-top: 0;\n color: #d32f2f;\n }\n .info-row {\n padding: 8px 0;\n border-bottom: 1px solid #eee;\n }\n .info-label {\n font-weight: bold;\n display: inline-block;\n width: 150px;\n color: #555;\n }\n .priority-badge {\n display: inline-block;\n padding: 5px 15px;\n border-radius: 20px;\n font-weight: bold;\n font-size: 14px;\n background-color: #d32f2f;\n color: white;\n }\n .action-required {\n background-color: #fff3cd;\n padding: 20px;\n border-radius: 5px;\n margin: 20px 0;\n border: 2px solid #ffc107;\n text-align: center;\n }\n .footer {\n text-align: center;\n padding: 20px;\n color: #777;\n font-size: 12px;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h2>\u26a0\ufe0f URGENT: Manual Assignment Required</h2>\n </div>\n \n <div class=\"content\">\n <div class=\"alert-box\">\n <h3 style=\"margin-top: 0; color: #d32f2f;\">\ud83d\udea8 No Vendor Available</h3>\n <p><strong>A maintenance request was submitted but no matching vendor was found in the service area.</strong></p>\n <p>Immediate manual assignment is required to prevent service delays.</p>\n </div>\n \n <div class=\"section\">\n <h3>\ud83d\udccb Ticket Information</h3>\n <div class=\"info-row\">\n <span class=\"info-label\">Ticket ID:</span>\n <strong style=\"font-size: 18px; color: #d32f2f;\">#{{ $('Watch for New Records').first().json.fields['Ticket ID'] }}</strong>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Submitted:</span>\n {{ $('Watch for New Records').first().json.fields['Submitted At'] }}\n </div>\n </div>\n \n <div class=\"section\">\n <h3>\ud83d\udd28 Issue Details</h3>\n <div class=\"info-row\">\n <span class=\"info-label\">Category:</span>\n <strong>{{ $('Watch for New Records').first().json.fields['Issue Category'] }}</strong>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Postcode:</span>\n <strong>{{ $('Watch for New Records').first().json.fields.Postcode }}</strong>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Priority:</span>\n <span class=\"priority-badge\">{{ $('Watch for New Records').first().json.fields.Priority }}</span>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Description:</span>\n </div>\n <div style=\"background-color: #f5f5f5; padding: 15px; margin-top: 10px; border-radius: 3px; font-style: italic;\">\n {{ $('Watch for New Records').first().json.fields['Issue Description'] }}\n </div>\n </div>\n \n <div class=\"section\">\n <h3>\ud83d\udccd Property & Tenant</h3>\n <div class=\"info-row\">\n <span class=\"info-label\">Property:</span>\n {{ $('Watch for New Records').first().json.fields['Property Address'] }}\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Tenant Name:</span>\n <strong>{{ $('Watch for New Records').first().json.fields['Tenant Name'] }}</strong>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Tenant Email:</span>\n <a href=\"mailto:{{ $('Watch for New Records').first().json.fields['Tenant Email'] }}\">{{ $('Watch for New Records').first().json.fields['Tenant Email'] }}</a>\n </div>\n </div>\n \n <div class=\"action-required\">\n <h3 style=\"margin-top: 0; color: #856404;\">\u26a1 ACTION REQUIRED</h3>\n <p style=\"font-size: 16px; margin: 10px 0;\">Please manually assign a contractor immediately</p>\n <p style=\"margin: 15px 0 0 0;\">\n <strong>Possible reasons for no match:</strong><br>\n \u2022 No vendors serve this postcode area<br>\n \u2022 No vendors available for this category<br>\n \u2022 All vendors inactive\n </p>\n </div>\n \n <div style=\"background-color: #e3f2fd; padding: 15px; border-radius: 5px; margin: 20px 0; text-align: center;\">\n <p style=\"margin: 0;\"><strong>View in Airtable to manually assign</strong></p>\n </div>\n </div>\n \n <div class=\"footer\">\n <p><strong>Property Management System - Automated Alert</strong></p>\n <p style=\"font-size: 11px;\">This email was triggered because no vendor match was found in the automated dispatch system.</p>\n </div>\n </div>\n</body>\n</html>",
"options": {},
"subject": "=\u26a0\ufe0f No Vendor Available - Manual Assignment Needed - Ticket #{{ $('Watch for New Records').first().json.fields['Ticket ID'] }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "d2ad5b73-1fb7-43cc-b936-4c39e03e6d2b",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
960,
656
],
"parameters": {
"width": 272,
"height": 432,
"content": "**Watch for New Records**\n\nTriggers when a tenant submits a maintenance request via the Airtable form.\n\nMonitors the Maintenance Tickets table every minute for new entries."
},
"typeVersion": 1
},
{
"id": "355772eb-ae07-42cf-a2c6-0cb9ab9d3f0f",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
2096,
656
],
"parameters": {
"width": 272,
"height": 432,
"content": "**Extract Postcode District**\n\nConverts full postcode (e.g., SW1A 1AA) to district code (SW1) by removing \ntrailing letters. \n\nThis enables area-based vendor matching instead of exact \npostcode matching."
},
"typeVersion": 1
},
{
"id": "868ae1b3-cf18-4b15-bbad-8a51da8a66a4",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
2384,
656
],
"parameters": {
"width": 256,
"height": 432,
"content": "**Search Records**\n\nFinds active vendors who:\n1. Match the issue category (Plumbing, Electrical, etc.)\n2. Serve the postcode district (using Service Areas field)\n3. Are marked as Active\n\nSorts results by Response Time to prioritize fastest responders."
},
"typeVersion": 1
},
{
"id": "7bc822ff-c602-404c-bfe2-4045504c4445",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2912,
656
],
"parameters": {
"width": 272,
"height": 432,
"content": "**Limit**\n\nSelects only the top vendor from search results (fastest responder) to prevent \ndispatching the same job to multiple contractors."
},
"typeVersion": 1
},
{
"id": "7b436a31-ca36-4860-b44f-ae9a93ac86c1",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2656,
656
],
"parameters": {
"height": 432,
"content": "**Check if Vendor Found** \n\nChecks if the search returned at least one matching vendor.\n\nTRUE = Proceed with dispatch\nFALSE = Alert admin (no coverage for this area/category)"
},
"typeVersion": 1
},
{
"id": "eacf4ca1-e2a6-4399-a36f-b1884e674d49",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
3200,
656
],
"parameters": {
"width": 272,
"height": 432,
"content": "**Update Ticket (TRUE branch)**\n\nUpdates the maintenance ticket in Airtable with:\n- Assigned Vendor (linked record)\n- Status changed to \"Assigned\"\n- Vendor Notified At timestamp"
},
"typeVersion": 1
},
{
"id": "db3c4b83-0e6e-480e-95b6-a3332394004a",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
3072,
1120
],
"parameters": {
"width": 272,
"height": 464,
"content": "**Email to Admin (FALSE branch)** \n\n\n\n\n\n\n\n\n\n\n\n\n\nAlerts property manager when no vendor coverage exists for the submitted \nrequest. Requires manual assignment.\n\nCommon causes:\n- No vendors serve this postcode area\n- No vendors available for this category\n- All vendors inactive"
},
"typeVersion": 1
},
{
"id": "2a0960d3-b198-4fde-8dcd-852c375fe1f2",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
3488,
656
],
"parameters": {
"width": 256,
"height": 432,
"content": "**Send Email to Vendor** \n\nSends HTML formatted email to the assigned vendor with:\n- Property address and postcode\n- Issue category, priority, and description\n- Tenant contact details\n- Ticket reference number"
},
"typeVersion": 1
},
{
"id": "7e38434d-d34f-4439-b397-631d6bfee569",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
3760,
656
],
"parameters": {
"width": 256,
"height": 432,
"content": "**Send SMS to Vendor** \n\nSends immediate SMS notification to vendor's mobile for time-sensitive issues.\n\nEnsures quick response, especially for High/Emergency priority jobs."
},
"typeVersion": 1
},
{
"id": "92213041-c642-4132-b383-d75d8619d3cd",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
4032,
656
],
"parameters": {
"width": 272,
"height": 432,
"content": "**Send Confirmation to Tenant**\n\nSends HTML formatted confirmation email to tenant with:\n- Ticket reference number\n- Issue summary and priority level\n- Assigned contractor name and response time\n- Next steps (contractor will call to schedule)"
},
"typeVersion": 1
},
{
"id": "35bcf71b-8a53-4f18-ad4c-b0b2960bd697",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
3072,
1600
],
"parameters": {
"width": 272,
"height": 448,
"content": "**Send Email to Tenant (Holding Message)** \n\nSends holding email to tenant confirming request received and explaining \nmanual assignment in progress. \n\nExpected response: 2-4 hours. Includes \nemergency contact for urgent issues."
},
"typeVersion": 1
},
{
"id": "9223b17e-29cc-42d5-bda9-c2e0b28f6505",
"name": "Sticky Note11",
"type": "n8n-nodes-base.stickyNote",
"position": [
3072,
2064
],
"parameters": {
"width": 272,
"height": 432,
"content": "**Update Ticket** \n\nSets status to \"Pending Manual Assignment\" and logs why auto-assignment \nfailed. \n\nCreates audit trail for property manager to manually assign \ncontractor."
},
"typeVersion": 1
},
{
"id": "61948e6e-0ba9-4c7d-a6eb-f4ac0329fc43",
"name": "Update Ticket - No Vendor Found",
"type": "n8n-nodes-base.airtable",
"position": [
3152,
2304
],
"parameters": {
"base": {
"__rl": true,
"mode": "list",
"value": "appXXXXXXXXXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX",
"cachedResultName": "Property Manager Base"
},
"table": {
"__rl": true,
"mode": "list",
"value": "tblTicketsXXXXXX",
"cachedResultUrl": "https://airtable.com/appXXXXXXXXXXXXXX/tblTicketsXXXXXX",
"cachedResultName": "Maintenance Tickets"
},
"columns": {
"value": {
"id": "={{ $('Watch for New Records').first().json.id }}",
"Status": "Pending Manual Assignment",
"Assignment Notes": "=No vendor available in {{ $('Extract Postcode District').first().json.postcodeDistrict }} area for {{ $('Watch for New Records').first().json.fields['Issue Category'] }}. Manual assignment required."
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "id",
"defaultMatch": true
},
{
"id": "Ticket ID",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "Ticket ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Submitted At",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "Submitted At",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tenant Name",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Tenant Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Tenant Email",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Tenant Email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Property Address",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Property Address",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Postcode",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Postcode",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Issue Category",
"type": "options",
"display": true,
"options": [
{
"name": "Plumbing",
"value": "Plumbing"
},
{
"name": "Electrical",
"value": "Electrical"
},
{
"name": "Heating",
"value": "Heating"
},
{
"name": "General Maintenance",
"value": "General Maintenance"
},
{
"name": "Locksmith",
"value": "Locksmith"
},
{
"name": "Pest Control",
"value": "Pest Control"
}
],
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Issue Category",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Issue Description",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Issue Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Priority",
"type": "options",
"display": true,
"options": [
{
"name": "Low",
"value": "Low"
},
{
"name": "Medium",
"value": "Medium"
},
{
"name": "High",
"value": "High"
},
{
"name": "Emergency",
"value": "Emergency"
}
],
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Priority",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Assigned Vendor",
"type": "array",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Assigned Vendor",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Vendor Notified At",
"type": "dateTime",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "Vendor Notified At",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "options",
"display": true,
"options": [
{
"name": "New",
"value": "New"
},
{
"name": "Assigned",
"value": "Assigned"
},
{
"name": "In Progress",
"value": "In Progress"
},
{
"name": "Completed",
"value": "Completed"
},
{
"name": "Pending Manual Assignment",
"value": "Pending Manual Assignment"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Assignment Notes",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Assignment Notes",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"id"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update"
},
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "8a01bef1-6fde-4121-8f12-e52ab01fd18c",
"name": "Send Holding Email to Tenant (Pending Assignment)",
"type": "n8n-nodes-base.gmail",
"position": [
3168,
1872
],
"parameters": {
"sendTo": "={{ $('Watch for New Records').first().json.fields['Tenant Email'] }}",
"message": "=<!DOCTYPE html>\n<html>\n<head>\n <style>\n body {\n font-family: Arial, sans-serif;\n line-height: 1.6;\n color: #333;\n }\n .container {\n max-width: 600px;\n margin: 0 auto;\n padding: 20px;\n }\n .header {\n background-color: #FF9800;\n color: white;\n padding: 20px;\n text-align: center;\n border-radius: 5px 5px 0 0;\n }\n .content {\n background-color: #f9f9f9;\n padding: 30px;\n border: 1px solid #ddd;\n }\n .section {\n background-color: white;\n padding: 15px;\n margin: 15px 0;\n border-left: 4px solid #FF9800;\n border-radius: 3px;\n }\n .section h3 {\n margin-top: 0;\n color: #FF9800;\n }\n .info-row {\n padding: 8px 0;\n border-bottom: 1px solid #eee;\n }\n .info-label {\n font-weight: bold;\n display: inline-block;\n width: 150px;\n }\n .alert-box {\n background-color: #fff3cd;\n padding: 20px;\n border-radius: 5px;\n margin: 20px 0;\n border-left: 4px solid #ffc107;\n }\n .footer {\n text-align: center;\n padding: 20px;\n color: #777;\n font-size: 12px;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h2>\u2713 Maintenance Request Received</h2>\n </div>\n \n <div class=\"content\">\n <p>Hi <strong>{{ $('Watch for New Records').first().json.fields['Tenant Name'] }}</strong>,</p>\n \n <p>Thank you for submitting your maintenance request. We have received your report and are currently reviewing it.</p>\n \n <div class=\"section\">\n <h3>\ud83d\udccb Your Request</h3>\n <div class=\"info-row\">\n <span class=\"info-label\">Ticket ID:</span>\n <strong>#{{ $('Watch for New Records').first().json.fields['Ticket ID'] }}</strong>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Issue Type:</span>\n {{ $('Watch for New Records').first().json.fields['Issue Category'] }}\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Priority:</span>\n <span style=\"\n display: inline-block;\n padding: 5px 12px;\n border-radius: 4px;\n font-weight: bold;\n background-color: {{ $('Watch for New Records').first().json.fields.Priority === 'Emergency' ? '#d32f2f' : $('Watch for New Records').first().json.fields.Priority === 'High' ? '#f44336' : $('Watch for New Records').first().json.fields.Priority === 'Medium' ? '#FF9800' : '#4CAF50' }};\n color: white;\n \">\n {{ $('Watch for New Records').first().json.fields.Priority }}\n </span>\n</div>\n <div class=\"info-row\">\n <span class=\"info-label\">Property:</span>\n {{ $('Watch for New Records').first().json.fields['Property Address'] }}\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">Description:</span>\n </div>\n <div style=\"background-color: #f5f5f5; padding: 15px; margin-top: 10px; border-radius: 3px; font-style: italic;\">\n {{ $('Watch for New Records').first().json.fields['Issue Description'] }}\n </div>\n </div>\n \n <div class=\"alert-box\">\n <strong>\u23f0 What happens next?</strong>\n <p style=\"margin: 10px 0 0 0;\">Our property management team is currently assigning a qualified contractor for your area. We will send you their contact details shortly, typically within 2-4 business hours.</p>\n <p style=\"margin: 10px 0 0 0;\">For urgent matters, please contact us directly at <strong>074 1237 1905</strong>.</p>\n </div>\n \n <div style=\"background-color: #e3f2fd; padding: 15px; border-radius: 5px; margin: 20px 0; text-align: center;\">\n <p style=\"margin: 0;\"><strong>We appreciate your patience</strong></p>\n <p style=\"margin: 5px 0 0 0; font-size: 14px;\">Your request is being prioritized based on urgency</p>\n </div>\n </div>\n \n <div class=\"footer\">\n <p><strong>Property Management Team</strong></p>\n <p style=\"font-size: 11px;\">This is an automated acknowledgment. You will receive a follow-up email once a contractor has been assigned.</p>\n </div>\n </div>\n</body>\n</html>",
"options": {},
"subject": "=Maintenance Request Received - Ticket #{{ $('Watch for New Records').first().json.fields['Ticket ID'] }} - Assignment in Progress"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "73d6be92-4a5a-4ee8-8fc8-232dbac5de25",
"name": "Evaluate Duplicates",
"type": "n8n-nodes-base.code",
"position": [
1616,
928
],
"parameters": {
"jsCode": "// Ticket that just triggered the workflow\nconst triggerItem = $('Watch for New Records').first().json;\nconst triggerFields = triggerItem.fields || triggerItem;\n\n// All open tickets from \"Search duplicate tickets\"\nconst allTickets = $input\n .all()\n .map(item => item.json)\n .filter(rec => rec && rec.id);\n\n// Helper\nconst norm = (v) => (v ?? '').toString().trim().toLowerCase();\nconst getFields = (rec) => rec.fields || rec;\n\n// Current ticket ID\nconst currentId = triggerItem.id;\n\n// Find real duplicates\nconst potentialDuplicates = allTickets.filter((rec) => {\n if (rec.id === currentId) return false;\n\n const f = getFields(rec);\n\n const sameTenant = norm(f['Tenant Email']) === norm(triggerFields['Tenant Email']);\n const sameAddress = norm(f['Property Address'])=== norm(triggerFields['Property Address']);\n const sameCategory = norm(f['Issue Category']) === norm(triggerFields['Issue Category']);\n\n return sameTenant && sameAddress && sameCategory;\n});\n\n// Start from the trigger and add flags\nconst base = {\n ...triggerItem,\n hasDuplicate: false,\n originalRecordId: null,\n originalTicketNumber: null,\n // store just the fields of each duplicate ticket\n duplicates: potentialDuplicates.map(rec => getFields(rec)),\n};\n\nif (!potentialDuplicates.length) {\n return [{ json: base }];\n}\n\n// Pick most recent duplicate\npotentialDuplicates.sort((a, b) => {\n const fa = getFields(a);\n const fb = getFields(b);\n return new Date(fb['Submitted At'] || '') - new Date(fa['Submitted At'] || '');\n});\n\nconst original = potentialDuplicates[0]
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.
airtableTokenApigmailOAuth2twilioApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This n8n template demonstrates how to triage tenant maintenance requests automatically; matching each ticket to the right contractor by category and postcode area, dispatching the job by email and SMS, and detecting duplicate tickets before they snowball.
Source: https://n8n.io/workflows/15780/ — 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.
This workflow streamlines accounts receivable management by automatically monitoring invoices in Google Sheets and sending scheduled payment reminders. It is designed for businesses using Gmail and Go
This workflow runs every Monday at 8 AM and automatically monitors your Jira project, measures progress against the active sprint, and delivers a structured report to stakeholders — with zero manual e
2025-12-03 fix JS code in node
Fetches all open sprint tickets daily from your Jira project Analyzes each ticket for overdue days and blocked status Routes to the right escalation level: assignee email → team Google Chat alert → ma
Recruiting agency. Uses typeformTrigger, airtable, httpRequest, googleDrive. Event-driven trigger; 36 nodes.