Files
n8n-masttro/workflows/WIwmV3nCtHM5TSUM_Freshdesk_Ticket_Group_Auto_Assignment.json

1164 lines
41 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"updatedAt": "2026-06-11T19:10:15.346Z",
"createdAt": "2026-04-16T19:14:28.262Z",
"id": "WIwmV3nCtHM5TSUM",
"name": "Freshdesk Ticket Group Auto-Assignment",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 1
}
]
}
},
"id": "94a33907-1092-434d-8af4-86e34926850a",
"name": "Every X minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-1632,
32
]
},
{
"parameters": {
"jsCode": "// Reset pagination state for this run\nconst sd = $getWorkflowStaticData('global');\nsd.tickets = [];\nsd.page = 1;\nreturn [{ json: { page: 1 } }];"
},
"id": "5b693030-06ad-4f1e-85ed-00a1ace1f188",
"name": "Init Pagination",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1408,
32
]
},
{
"parameters": {
"url": "https://masttro.freshdesk.com/api/v2/search/tickets",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "\"status:2\""
},
{
"name": "page",
"value": "={{ $json.page }}"
}
]
},
"options": {}
},
"id": "8e87b3de-2109-4e9c-b748-b513543a0a66",
"name": "Fetch Tickets Page",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-1184,
32
],
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nconst body = $input.first().json;\nconst results = body.results || [];\n\n// Filter and accumulate unassigned tickets (no group_id)\nconst unassigned = results.filter(t => !t.group_id);\nsd.tickets = [...(sd.tickets || []), ...unassigned];\n\n// Freshdesk search returns max 30 per page, caps at page 10\nconst hasMore = results.length === 30 && (sd.page || 1) < 10;\n\nif (hasMore) {\n sd.page = (sd.page || 1) + 1;\n return [{ json: { page: sd.page, hasMore: true } }];\n} else {\n return [{ json: { page: sd.page, hasMore: false } }];\n}"
},
"id": "a027844a-8d7c-4e58-98fd-5c2f9af43cf6",
"name": "Collect Results",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-960,
-48
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "has-more-pages",
"leftValue": "={{ $json.hasMore }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "97e1b16a-9fea-42e6-ba8b-f4ed155714b5",
"name": "More Pages?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-736,
32
]
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nconst tickets = [...(sd.tickets || [])];\n\n// Clear state so next run starts clean\nsd.tickets = [];\nsd.page = 1;\n\nif (tickets.length === 0) {\n return [{ json: { skipped: true } }];\n}\n\nreturn tickets.map(t => ({ json: t }));"
},
"id": "7b62b5e7-85ea-4e15-aa8a-41ac10762825",
"name": "Split Into Items",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-512,
32
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "no-skip",
"leftValue": "={{ $json.skipped }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "052c9cb2-3d00-4e2c-83c4-c37df478ca66",
"name": "Has Tickets?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-288,
32
]
},
{
"parameters": {
"options": {}
},
"id": "cd59e31d-c7a6-4d73-9afe-c6952988fad7",
"name": "Loop Each Ticket",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
-64,
128
]
},
{
"parameters": {
"jsCode": "const t = $input.first().json;\n\n// --- Ticket fields ---\nconst ticketId = t.id;\nconst subject = t.subject || '(no subject)';\nconst desc = t.description_text || t.description || '(no description)';\nconst tags = (t.tags || []).join(', ') || '(none)';\nconst ticketType = t.type || '(none)';\nconst priority = t.priority || '(none)';\nconst source = t.source || '(none)';\nconst customFields = JSON.stringify(t.custom_fields || {});\n\n// --- Group definitions with hard routing rules ---\nconst groups = [\n {\n id: 158000821060,\n name: 'Billing',\n rules: [\n 'SaaS platform subscription charges or renewals',\n 'Credit card or payment method failures',\n 'Refund requests for Masttro platform fees',\n 'Plan upgrade or downgrade requests',\n 'Receipts or invoices FOR the Masttro service itself (not vendor invoices)'\n ]\n },\n {\n id: 158000821061,\n name: 'AP MUS',\n rules: [\n 'Accounts payable for the US entity (MUS)',\n 'US domestic vendor or supplier invoices in USD',\n 'US vendor payment status, remittance, or ACH/wire questions',\n 'W-9 or 1099 requests from US vendors',\n 'Any AP query that explicitly references the US entity or USD'\n ]\n },\n {\n id: 158000821062,\n name: 'AP MMX',\n rules: [\n 'Accounts payable for the Mexico entity (MMX)',\n 'Mexican supplier invoices (CFDI, XML, RFC numbers)',\n 'MXN currency payments or peso-denominated AP',\n 'CONTPAQi or Mexican tax authority (SAT) related AP queries',\n 'Any AP query that explicitly references Mexico, MMX, or MXN'\n ]\n },\n {\n id: 158000821063,\n name: 'AP MSAG',\n rules: [\n 'Accounts payable for the Swiss entity (MSAG or MHAG)',\n 'Swiss or European vendor invoices in CHF or EUR',\n 'Topal accounting system queries for Swiss entities',\n 'Swiss employment or contractor payment AP questions',\n 'Any AP query that explicitly references Switzerland, MSAG, MHAG, CHF, or European vendors'\n ]\n }\n];\n\nconst groupList = groups.map(g =>\n `GROUP ID ${g.id} — ${g.name}\\nRoute here if:\\n` +\n g.rules.map(r => ` • ${r}`).join('\\n')\n).join('\\n\\n');\n\nconst chatInput = [\n '## Support Ticket Classification Task',\n '',\n 'Classify the ticket below and either call the Update Freshdesk ticket tool OR respond with UNASSIGNED.',\n '',\n '### Ticket Details',\n `Ticket ID: ${ticketId}`,\n `Subject: ${subject}`,\n `Type: ${ticketType}`,\n `Priority: ${priority}`,\n `Source: ${source}`,\n `Tags: ${tags}`,\n `Custom Fields: ${customFields}`,\n '',\n '### Description',\n desc,\n '',\n '### Routing Groups',\n groupList,\n '',\n '### Instructions',\n '1. Read the ticket carefully.',\n '2. Match it to exactly ONE group using the routing rules above.',\n '3. If the entity (US/Mexico/Switzerland) is not clearly identifiable, assign to Billing ONLY if it is a platform subscription question.',\n '4. IF you identified a valid group, call the Update Freshdesk ticket tool with:',\n ` - Ticket_ID = ${ticketId}`,\n ' - Group_ID = the numeric group ID from above (must be one of: 158000821060, 158000821061, 158000821062, 158000821063)',\n '5. IF you cannot confidently assign a group, do NOT call the tool. Instead respond with exactly:',\n ' UNASSIGNED: <one sentence reason>',\n '6. Do not ask for confirmation. Act immediately.',\n '7. NEVER pass 0 as a Group_ID. If uncertain, use the UNASSIGNED response instead.'\n].join('\\n');\n\nreturn [{ json: { ...t, chatInput } }];"
},
"id": "7e30a131-ba2d-4eec-ade3-748b02c7c607",
"name": "Format Agent Prompt",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
160,
0
]
},
{
"parameters": {
"options": {
"systemMessage": "You are a finance support ticket routing agent for Masttro, a B2B SaaS wealth management platform.\n\nYour only job is to classify incoming Freshdesk tickets and either assign them to the correct internal group by calling the Update Freshdesk ticket tool, OR respond with UNASSIGNED if you cannot confidently determine the group.\n\n## Masttro Entity Reference\n- MUS = US entity (USD, domestic US vendors)\n- MMX = Mexico entity (MXN, CFDI invoices, RFC, SAT, CONTPAQi)\n- MSAG = Switzerland entity (CHF/EUR, Swiss/European vendors, Topal)\n- MHAG = Switzerland holding entity (treat same as MSAG)\n\n## Valid Group IDs (the ONLY values you may pass to the tool)\n- 158000821060 = Billing\n- 158000821061 = AP MUS\n- 158000821062 = AP MMX\n- 158000821063 = AP MSAG\n\n## Decision Rules\n1. BILLING group (158000821060): Only if the ticket is about the Masttro platform subscription, licensing fees, or payment for the SaaS service itself.\n2. AP MUS group (158000821061): Vendor/supplier AP for the US entity. USD, W-9, 1099, ACH are strong signals.\n3. AP MMX group (158000821062): Vendor/supplier AP for Mexico. CFDI, RFC, XML invoice, MXN, SAT are strong signals.\n4. AP MSAG group (158000821063): Vendor/supplier AP for Switzerland or Europe. CHF, EUR (Swiss context), Topal, MHAG are strong signals.\n\n## CRITICAL TOOL USAGE RULES\n- ONLY call the Update Freshdesk ticket tool when you have a confident match to one of the four valid group IDs above.\n- NEVER pass 0 or any value other than the four valid group IDs to the tool. Passing an invalid ID will cause an API error.\n- If the entity cannot be determined and no billing signal exists, do NOT call the tool at all. Instead respond with: UNASSIGNED: <one sentence reason>\n- Never guess between two AP groups — if ambiguous between entities, respond UNASSIGNED.\n- Do not ask clarifying questions."
}
},
"id": "4f052358-3af4-40ba-9b46-2ccd9b07fc36",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
384,
0
]
},
{
"parameters": {
"operation": "update",
"ticketId": "={{ $fromAI('Ticket_ID', 'The numeric ID of the Freshdesk ticket to update', 'string') }}",
"updateFields": {
"group": "={{ $fromAI('Group_ID', 'The numeric Freshdesk group ID. Must be exactly one of these four values: 158000821060 (Billing), 158000821061 (AP MUS), 158000821062 (AP MMX), 158000821063 (AP MSAG). Do NOT pass 0 or any other value — if uncertain, do not call this tool at all and respond with UNASSIGNED instead.', 'number') }}"
}
},
"id": "c895fa9e-ebcf-4a3e-874e-1a8a885cb079",
"name": "Update a ticket in Freshdesk",
"type": "n8n-nodes-base.freshdeskTool",
"typeVersion": 1,
"position": [
528,
224
],
"credentials": {
"freshdeskApi": {
"id": "CjhwO4a2iGx7J4v0",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"jsCode": "// Detect whether the agent called the tool (assigned) or responded with UNASSIGNED.\n// The agent output is a string in agentOutput.output.\n\nconst VALID_GROUP_IDS = new Set([158000821060, 158000821061, 158000821062, 158000821063]);\n\nconst agentOutput = $input.first().json;\nconst outputText = (agentOutput.output || '').trim();\n\n// If the agent responded with UNASSIGNED, route to triage\nif (outputText.toUpperCase().startsWith('UNASSIGNED')) {\n return [{ json: { ...agentOutput, validAssignment: false, triageReason: outputText } }];\n}\n\n// Otherwise assume the tool was called successfully — mark as valid\n// (If the tool had errored, execution would not have reached here)\nreturn [{ json: { ...agentOutput, validAssignment: true } }];"
},
"id": "a7a64ad1-b11e-4e45-95f7-be9ce916784e",
"name": "Validate Group Assignment",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
736,
0
]
},
{
"parameters": {
"jsCode": "// Log skipped/uncertain tickets for manual review\nconst item = $input.first().json;\nconst reason = item.triageReason || 'No reason provided';\nconsole.log(`[TRIAGE NEEDED] Ticket ID ${item.id} could not be auto-assigned. Reason: ${reason}. Subject: ${item.subject}`);\nreturn [{ json: { status: 'triage_needed', ticket_id: item.id, subject: item.subject, reason } }];"
},
"id": "4d6ef17b-d822-4446-aaa8-8e6548e004cb",
"name": "Log Triage Needed",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1184,
224
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "valid-assignment",
"leftValue": "={{ $json.validAssignment }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "fc5b2120-bf2d-4c5f-964c-d10d27abb191",
"name": "Valid Assignment?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
960,
144
]
},
{
"parameters": {
"jsCode": "console.log('All tickets processed.');\nreturn [{ json: { status: 'done' } }];"
},
"id": "d7cad154-c238-4784-9135-7a2b72e0dd08",
"name": "All Done",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
160,
-192
]
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-6",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.6"
},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
400,
224
],
"id": "1f389ad0-34e0-4e08-b186-8488f24fb768",
"name": "Anthropic Chat Model",
"credentials": {
"anthropicApi": {
"id": "k9lzArwvHtlGPXmY",
"name": "Anthropic - Freshdesk"
}
}
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "vP7h2QdQeLXImufQ",
"mode": "list",
"cachedResultUrl": "/workflow/vP7h2QdQeLXImufQ",
"cachedResultName": "Freshdesk – Round-Robin Auto Assignment"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.3,
"position": [
384,
-192
],
"id": "ec9c8c81-456c-4764-9ed3-b7005c9152c1",
"name": "Call 'Freshdesk – Round-Robin Auto Assignment'"
}
],
"connections": {
"Every X minutes": {
"main": [
[
{
"node": "Init Pagination",
"type": "main",
"index": 0
}
]
]
},
"Init Pagination": {
"main": [
[
{
"node": "Fetch Tickets Page",
"type": "main",
"index": 0
}
]
]
},
"Fetch Tickets Page": {
"main": [
[
{
"node": "Collect Results",
"type": "main",
"index": 0
}
]
]
},
"Collect Results": {
"main": [
[
{
"node": "More Pages?",
"type": "main",
"index": 0
}
]
]
},
"More Pages?": {
"main": [
[
{
"node": "Fetch Tickets Page",
"type": "main",
"index": 0
}
],
[
{
"node": "Split Into Items",
"type": "main",
"index": 0
}
]
]
},
"Split Into Items": {
"main": [
[
{
"node": "Has Tickets?",
"type": "main",
"index": 0
}
]
]
},
"Has Tickets?": {
"main": [
[
{
"node": "Loop Each Ticket",
"type": "main",
"index": 0
}
],
[
{
"node": "All Done",
"type": "main",
"index": 0
}
]
]
},
"Loop Each Ticket": {
"main": [
[
{
"node": "All Done",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Agent Prompt",
"type": "main",
"index": 0
}
]
]
},
"Format Agent Prompt": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Validate Group Assignment",
"type": "main",
"index": 0
}
]
]
},
"Update a ticket in Freshdesk": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Validate Group Assignment": {
"main": [
[
{
"node": "Valid Assignment?",
"type": "main",
"index": 0
}
]
]
},
"Log Triage Needed": {
"main": [
[
{
"node": "Loop Each Ticket",
"type": "main",
"index": 0
}
]
]
},
"Valid Assignment?": {
"main": [
[
{
"node": "Loop Each Ticket",
"type": "main",
"index": 0
}
],
[
{
"node": "Log Triage Needed",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"All Done": {
"main": [
[
{
"node": "Call 'Freshdesk – Round-Robin Auto Assignment'",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"binaryMode": "separate",
"errorWorkflow": "tJSgL4iy63ys2NCA",
"executionOrder": "v1"
},
"staticData": {
"node:Every X minutes": {
"recurrenceRules": []
},
"global": {
"tickets": [],
"page": 1
}
},
"meta": {
"templateCredsSetupCompleted": true
},
"pinData": {},
"versionId": "5afae1dc-76cb-4bb8-8d35-2b1ec2a0c731",
"activeVersionId": "5afae1dc-76cb-4bb8-8d35-2b1ec2a0c731",
"triggerCount": 1,
"shared": [
{
"updatedAt": "2026-04-16T19:14:28.262Z",
"createdAt": "2026-04-16T19:14:28.262Z",
"role": "workflow:owner",
"workflowId": "WIwmV3nCtHM5TSUM",
"projectId": "X4TTvIwW8wN0Iirz"
}
],
"activeVersion": {
"updatedAt": "2026-06-11T19:10:16.436Z",
"createdAt": "2026-06-11T19:10:15.349Z",
"versionId": "5afae1dc-76cb-4bb8-8d35-2b1ec2a0c731",
"workflowId": "WIwmV3nCtHM5TSUM",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 1
}
]
}
},
"id": "94a33907-1092-434d-8af4-86e34926850a",
"name": "Every X minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-1632,
32
]
},
{
"parameters": {
"jsCode": "// Reset pagination state for this run\nconst sd = $getWorkflowStaticData('global');\nsd.tickets = [];\nsd.page = 1;\nreturn [{ json: { page: 1 } }];"
},
"id": "5b693030-06ad-4f1e-85ed-00a1ace1f188",
"name": "Init Pagination",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1408,
32
]
},
{
"parameters": {
"url": "https://masttro.freshdesk.com/api/v2/search/tickets",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "query",
"value": "\"status:2\""
},
{
"name": "page",
"value": "={{ $json.page }}"
}
]
},
"options": {}
},
"id": "8e87b3de-2109-4e9c-b748-b513543a0a66",
"name": "Fetch Tickets Page",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-1184,
32
],
"credentials": {
"httpBasicAuth": {
"id": "IKEtEPEGfb3MGzuf",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nconst body = $input.first().json;\nconst results = body.results || [];\n\n// Filter and accumulate unassigned tickets (no group_id)\nconst unassigned = results.filter(t => !t.group_id);\nsd.tickets = [...(sd.tickets || []), ...unassigned];\n\n// Freshdesk search returns max 30 per page, caps at page 10\nconst hasMore = results.length === 30 && (sd.page || 1) < 10;\n\nif (hasMore) {\n sd.page = (sd.page || 1) + 1;\n return [{ json: { page: sd.page, hasMore: true } }];\n} else {\n return [{ json: { page: sd.page, hasMore: false } }];\n}"
},
"id": "a027844a-8d7c-4e58-98fd-5c2f9af43cf6",
"name": "Collect Results",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-960,
-48
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "has-more-pages",
"leftValue": "={{ $json.hasMore }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "97e1b16a-9fea-42e6-ba8b-f4ed155714b5",
"name": "More Pages?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-736,
32
]
},
{
"parameters": {
"jsCode": "const sd = $getWorkflowStaticData('global');\nconst tickets = [...(sd.tickets || [])];\n\n// Clear state so next run starts clean\nsd.tickets = [];\nsd.page = 1;\n\nif (tickets.length === 0) {\n return [{ json: { skipped: true } }];\n}\n\nreturn tickets.map(t => ({ json: t }));"
},
"id": "7b62b5e7-85ea-4e15-aa8a-41ac10762825",
"name": "Split Into Items",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-512,
32
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "no-skip",
"leftValue": "={{ $json.skipped }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "052c9cb2-3d00-4e2c-83c4-c37df478ca66",
"name": "Has Tickets?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-288,
32
]
},
{
"parameters": {
"options": {}
},
"id": "cd59e31d-c7a6-4d73-9afe-c6952988fad7",
"name": "Loop Each Ticket",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
-64,
128
]
},
{
"parameters": {
"jsCode": "const t = $input.first().json;\n\n// --- Ticket fields ---\nconst ticketId = t.id;\nconst subject = t.subject || '(no subject)';\nconst desc = t.description_text || t.description || '(no description)';\nconst tags = (t.tags || []).join(', ') || '(none)';\nconst ticketType = t.type || '(none)';\nconst priority = t.priority || '(none)';\nconst source = t.source || '(none)';\nconst customFields = JSON.stringify(t.custom_fields || {});\n\n// --- Group definitions with hard routing rules ---\nconst groups = [\n {\n id: 158000821060,\n name: 'Billing',\n rules: [\n 'SaaS platform subscription charges or renewals',\n 'Credit card or payment method failures',\n 'Refund requests for Masttro platform fees',\n 'Plan upgrade or downgrade requests',\n 'Receipts or invoices FOR the Masttro service itself (not vendor invoices)'\n ]\n },\n {\n id: 158000821061,\n name: 'AP MUS',\n rules: [\n 'Accounts payable for the US entity (MUS)',\n 'US domestic vendor or supplier invoices in USD',\n 'US vendor payment status, remittance, or ACH/wire questions',\n 'W-9 or 1099 requests from US vendors',\n 'Any AP query that explicitly references the US entity or USD'\n ]\n },\n {\n id: 158000821062,\n name: 'AP MMX',\n rules: [\n 'Accounts payable for the Mexico entity (MMX)',\n 'Mexican supplier invoices (CFDI, XML, RFC numbers)',\n 'MXN currency payments or peso-denominated AP',\n 'CONTPAQi or Mexican tax authority (SAT) related AP queries',\n 'Any AP query that explicitly references Mexico, MMX, or MXN'\n ]\n },\n {\n id: 158000821063,\n name: 'AP MSAG',\n rules: [\n 'Accounts payable for the Swiss entity (MSAG or MHAG)',\n 'Swiss or European vendor invoices in CHF or EUR',\n 'Topal accounting system queries for Swiss entities',\n 'Swiss employment or contractor payment AP questions',\n 'Any AP query that explicitly references Switzerland, MSAG, MHAG, CHF, or European vendors'\n ]\n }\n];\n\nconst groupList = groups.map(g =>\n `GROUP ID ${g.id} — ${g.name}\\nRoute here if:\\n` +\n g.rules.map(r => ` • ${r}`).join('\\n')\n).join('\\n\\n');\n\nconst chatInput = [\n '## Support Ticket Classification Task',\n '',\n 'Classify the ticket below and either call the Update Freshdesk ticket tool OR respond with UNASSIGNED.',\n '',\n '### Ticket Details',\n `Ticket ID: ${ticketId}`,\n `Subject: ${subject}`,\n `Type: ${ticketType}`,\n `Priority: ${priority}`,\n `Source: ${source}`,\n `Tags: ${tags}`,\n `Custom Fields: ${customFields}`,\n '',\n '### Description',\n desc,\n '',\n '### Routing Groups',\n groupList,\n '',\n '### Instructions',\n '1. Read the ticket carefully.',\n '2. Match it to exactly ONE group using the routing rules above.',\n '3. If the entity (US/Mexico/Switzerland) is not clearly identifiable, assign to Billing ONLY if it is a platform subscription question.',\n '4. IF you identified a valid group, call the Update Freshdesk ticket tool with:',\n ` - Ticket_ID = ${ticketId}`,\n ' - Group_ID = the numeric group ID from above (must be one of: 158000821060, 158000821061, 158000821062, 158000821063)',\n '5. IF you cannot confidently assign a group, do NOT call the tool. Instead respond with exactly:',\n ' UNASSIGNED: <one sentence reason>',\n '6. Do not ask for confirmation. Act immediately.',\n '7. NEVER pass 0 as a Group_ID. If uncertain, use the UNASSIGNED response instead.'\n].join('\\n');\n\nreturn [{ json: { ...t, chatInput } }];"
},
"id": "7e30a131-ba2d-4eec-ade3-748b02c7c607",
"name": "Format Agent Prompt",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
160,
0
]
},
{
"parameters": {
"options": {
"systemMessage": "You are a finance support ticket routing agent for Masttro, a B2B SaaS wealth management platform.\n\nYour only job is to classify incoming Freshdesk tickets and either assign them to the correct internal group by calling the Update Freshdesk ticket tool, OR respond with UNASSIGNED if you cannot confidently determine the group.\n\n## Masttro Entity Reference\n- MUS = US entity (USD, domestic US vendors)\n- MMX = Mexico entity (MXN, CFDI invoices, RFC, SAT, CONTPAQi)\n- MSAG = Switzerland entity (CHF/EUR, Swiss/European vendors, Topal)\n- MHAG = Switzerland holding entity (treat same as MSAG)\n\n## Valid Group IDs (the ONLY values you may pass to the tool)\n- 158000821060 = Billing\n- 158000821061 = AP MUS\n- 158000821062 = AP MMX\n- 158000821063 = AP MSAG\n\n## Decision Rules\n1. BILLING group (158000821060): Only if the ticket is about the Masttro platform subscription, licensing fees, or payment for the SaaS service itself.\n2. AP MUS group (158000821061): Vendor/supplier AP for the US entity. USD, W-9, 1099, ACH are strong signals.\n3. AP MMX group (158000821062): Vendor/supplier AP for Mexico. CFDI, RFC, XML invoice, MXN, SAT are strong signals.\n4. AP MSAG group (158000821063): Vendor/supplier AP for Switzerland or Europe. CHF, EUR (Swiss context), Topal, MHAG are strong signals.\n\n## CRITICAL TOOL USAGE RULES\n- ONLY call the Update Freshdesk ticket tool when you have a confident match to one of the four valid group IDs above.\n- NEVER pass 0 or any value other than the four valid group IDs to the tool. Passing an invalid ID will cause an API error.\n- If the entity cannot be determined and no billing signal exists, do NOT call the tool at all. Instead respond with: UNASSIGNED: <one sentence reason>\n- Never guess between two AP groups — if ambiguous between entities, respond UNASSIGNED.\n- Do not ask clarifying questions."
}
},
"id": "4f052358-3af4-40ba-9b46-2ccd9b07fc36",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3.1,
"position": [
384,
0
]
},
{
"parameters": {
"operation": "update",
"ticketId": "={{ $fromAI('Ticket_ID', 'The numeric ID of the Freshdesk ticket to update', 'string') }}",
"updateFields": {
"group": "={{ $fromAI('Group_ID', 'The numeric Freshdesk group ID. Must be exactly one of these four values: 158000821060 (Billing), 158000821061 (AP MUS), 158000821062 (AP MMX), 158000821063 (AP MSAG). Do NOT pass 0 or any other value — if uncertain, do not call this tool at all and respond with UNASSIGNED instead.', 'number') }}"
}
},
"id": "c895fa9e-ebcf-4a3e-874e-1a8a885cb079",
"name": "Update a ticket in Freshdesk",
"type": "n8n-nodes-base.freshdeskTool",
"typeVersion": 1,
"position": [
528,
224
],
"credentials": {
"freshdeskApi": {
"id": "CjhwO4a2iGx7J4v0",
"name": "Freshdesk - Finance"
}
}
},
{
"parameters": {
"jsCode": "// Detect whether the agent called the tool (assigned) or responded with UNASSIGNED.\n// The agent output is a string in agentOutput.output.\n\nconst VALID_GROUP_IDS = new Set([158000821060, 158000821061, 158000821062, 158000821063]);\n\nconst agentOutput = $input.first().json;\nconst outputText = (agentOutput.output || '').trim();\n\n// If the agent responded with UNASSIGNED, route to triage\nif (outputText.toUpperCase().startsWith('UNASSIGNED')) {\n return [{ json: { ...agentOutput, validAssignment: false, triageReason: outputText } }];\n}\n\n// Otherwise assume the tool was called successfully — mark as valid\n// (If the tool had errored, execution would not have reached here)\nreturn [{ json: { ...agentOutput, validAssignment: true } }];"
},
"id": "a7a64ad1-b11e-4e45-95f7-be9ce916784e",
"name": "Validate Group Assignment",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
736,
0
]
},
{
"parameters": {
"jsCode": "// Log skipped/uncertain tickets for manual review\nconst item = $input.first().json;\nconst reason = item.triageReason || 'No reason provided';\nconsole.log(`[TRIAGE NEEDED] Ticket ID ${item.id} could not be auto-assigned. Reason: ${reason}. Subject: ${item.subject}`);\nreturn [{ json: { status: 'triage_needed', ticket_id: item.id, subject: item.subject, reason } }];"
},
"id": "4d6ef17b-d822-4446-aaa8-8e6548e004cb",
"name": "Log Triage Needed",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1184,
224
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "valid-assignment",
"leftValue": "={{ $json.validAssignment }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "fc5b2120-bf2d-4c5f-964c-d10d27abb191",
"name": "Valid Assignment?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
960,
144
]
},
{
"parameters": {
"jsCode": "console.log('All tickets processed.');\nreturn [{ json: { status: 'done' } }];"
},
"id": "d7cad154-c238-4784-9135-7a2b72e0dd08",
"name": "All Done",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
160,
-192
]
},
{
"parameters": {
"model": {
"__rl": true,
"value": "claude-sonnet-4-6",
"mode": "list",
"cachedResultName": "Claude Sonnet 4.6"
},
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"typeVersion": 1.3,
"position": [
400,
224
],
"id": "1f389ad0-34e0-4e08-b186-8488f24fb768",
"name": "Anthropic Chat Model",
"credentials": {
"anthropicApi": {
"id": "k9lzArwvHtlGPXmY",
"name": "Anthropic - Freshdesk"
}
}
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "vP7h2QdQeLXImufQ",
"mode": "list",
"cachedResultUrl": "/workflow/vP7h2QdQeLXImufQ",
"cachedResultName": "Freshdesk – Round-Robin Auto Assignment"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.3,
"position": [
384,
-192
],
"id": "ec9c8c81-456c-4764-9ed3-b7005c9152c1",
"name": "Call 'Freshdesk – Round-Robin Auto Assignment'"
}
],
"connections": {
"Every X minutes": {
"main": [
[
{
"node": "Init Pagination",
"type": "main",
"index": 0
}
]
]
},
"Init Pagination": {
"main": [
[
{
"node": "Fetch Tickets Page",
"type": "main",
"index": 0
}
]
]
},
"Fetch Tickets Page": {
"main": [
[
{
"node": "Collect Results",
"type": "main",
"index": 0
}
]
]
},
"Collect Results": {
"main": [
[
{
"node": "More Pages?",
"type": "main",
"index": 0
}
]
]
},
"More Pages?": {
"main": [
[
{
"node": "Fetch Tickets Page",
"type": "main",
"index": 0
}
],
[
{
"node": "Split Into Items",
"type": "main",
"index": 0
}
]
]
},
"Split Into Items": {
"main": [
[
{
"node": "Has Tickets?",
"type": "main",
"index": 0
}
]
]
},
"Has Tickets?": {
"main": [
[
{
"node": "Loop Each Ticket",
"type": "main",
"index": 0
}
],
[
{
"node": "All Done",
"type": "main",
"index": 0
}
]
]
},
"Loop Each Ticket": {
"main": [
[
{
"node": "All Done",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Agent Prompt",
"type": "main",
"index": 0
}
]
]
},
"Format Agent Prompt": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Validate Group Assignment",
"type": "main",
"index": 0
}
]
]
},
"Update a ticket in Freshdesk": {
"ai_tool": [
[
{
"node": "AI Agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Validate Group Assignment": {
"main": [
[
{
"node": "Valid Assignment?",
"type": "main",
"index": 0
}
]
]
},
"Log Triage Needed": {
"main": [
[
{
"node": "Loop Each Ticket",
"type": "main",
"index": 0
}
]
]
},
"Valid Assignment?": {
"main": [
[
{
"node": "Loop Each Ticket",
"type": "main",
"index": 0
}
],
[
{
"node": "Log Triage Needed",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"All Done": {
"main": [
[
{
"node": "Call 'Freshdesk – Round-Robin Auto Assignment'",
"type": "main",
"index": 0
}
]
]
}
},
"nodeGroups": [],
"authors": "Michael Nolan",
"name": "Version 5afae1dc",
"description": "",
"autosaved": false
},
"tags": []
}