1084 lines
37 KiB
JSON
1084 lines
37 KiB
JSON
{
|
|
"updatedAt": "2026-06-04T13:28:20.465Z",
|
|
"createdAt": "2026-04-16T19:14:28.262Z",
|
|
"id": "WIwmV3nCtHM5TSUM",
|
|
"name": "Ticket Group Auto-Assignment",
|
|
"active": true,
|
|
"isArchived": false,
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"rule": {
|
|
"interval": [
|
|
{
|
|
"field": "minutes"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"id": "3fc30aa2-0ef7-4d4f-985a-95b5c8e681df",
|
|
"name": "Every X minutes",
|
|
"type": "n8n-nodes-base.scheduleTrigger",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
-1312,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Reset pagination state for this run\nconst sd = $getWorkflowStaticData('global');\nsd.tickets = [];\nsd.page = 1;\nreturn [{ json: { page: 1 } }];"
|
|
},
|
|
"id": "6341557a-414f-4bc3-b0b4-af1f2999d764",
|
|
"name": "Init Pagination",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1088,
|
|
-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": "47a0dbb0-c6fc-4fd7-88c8-7771b2193047",
|
|
"name": "Fetch Tickets Page",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
-864,
|
|
-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": "6343d141-b452-44da-ba12-e95963149074",
|
|
"name": "Collect Results",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-640,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"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": "0bb8f69a-e881-46c8-a541-c91bfcb45074",
|
|
"name": "More Pages?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-416,
|
|
-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": "01a298db-4c5e-45c3-9d00-4e327d517551",
|
|
"name": "Split Into Items",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-192,
|
|
-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": "8f85e242-80cf-4965-9266-ef27c3f780b5",
|
|
"name": "Has Tickets?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
32,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "1975c60e-7687-4415-8ac7-35d31468d492",
|
|
"name": "Loop Each Ticket",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
256,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"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 call the Update Freshdesk ticket tool.',\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; otherwise leave group_id as 0 (do not assign).',\n '4. Call the Update Freshdesk ticket tool with:',\n ` - Ticket_ID = ${ticketId}`,\n ' - Group_ID = the numeric group ID from above (or 0 if uncertain)',\n '5. Do not ask for confirmation. Act immediately.'\n].join('\\n');\n\nreturn [{ json: { ...t, chatInput } }];"
|
|
},
|
|
"id": "d54d96cc-349e-4df3-9f2b-3c982319693e",
|
|
"name": "Format Agent Prompt",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
480,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"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 assign them to the correct internal group by calling the Update Freshdesk ticket tool.\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## Decision Rules\n1. BILLING group: Only if the ticket is about the Masttro platform subscription, licensing fees, or payment for the SaaS service itself.\n2. AP MUS group: Vendor/supplier AP for the US entity. USD, W-9, 1099, ACH are strong signals.\n3. AP MMX group: Vendor/supplier AP for Mexico. CFDI, RFC, XML invoice, MXN, SAT are strong signals.\n4. AP MSAG group: Vendor/supplier AP for Switzerland or Europe. CHF, EUR (Swiss context), Topal, MHAG are strong signals.\n5. UNCERTAIN: If the entity cannot be determined and no billing signal exists, call the tool with Group_ID=0 to leave unassigned for manual triage.\n\n## Behavior\n- Always call the tool. Never respond with just text.\n- Never guess between two AP groups — if ambiguous between entities, use Group_ID=0.\n- Do not ask clarifying questions."
|
|
}
|
|
},
|
|
"id": "a6b76cb5-dc7b-4eb8-9b0d-a4952398651b",
|
|
"name": "AI Agent",
|
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
"typeVersion": 3.1,
|
|
"position": [
|
|
704,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "update",
|
|
"ticketId": "={{ $fromAI('Ticket_ID', 'The numeric ID of the ticket to update', 'string') }}",
|
|
"updateFields": {
|
|
"group": "={{ $fromAI('Group_ID', 'The numeric Freshdesk group ID to assign the ticket to. Use 0 if uncertain.', 'number') }}"
|
|
}
|
|
},
|
|
"id": "e326cd7f-feed-41c6-b098-a6dc90c89ac4",
|
|
"name": "Update a ticket in Freshdesk",
|
|
"type": "n8n-nodes-base.freshdeskTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
832,
|
|
192
|
|
],
|
|
"credentials": {
|
|
"freshdeskApi": {
|
|
"id": "CjhwO4a2iGx7J4v0",
|
|
"name": "Freshdesk - Finance"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Validate that the group ID chosen by the AI is one of the known valid group IDs.\n// If it's 0 (uncertain) or invalid, we skip the update so the ticket stays unassigned for manual review.\n\nconst VALID_GROUP_IDS = new Set([158000821060, 158000821061, 158000821062, 158000821063]);\n\nconst agentOutput = $input.first().json;\n\n// The AI agent output is a string — extract the group_id it used from the tool call context.\n// We store it on the item via the tool call. Check output text for any override signals.\n// Primary guard: if the agent set group to 0 or a non-valid ID, route to skip branch.\nconst groupId = agentOutput.group_id_used !== undefined\n ? Number(agentOutput.group_id_used)\n : null;\n\n// If we can't parse or it's invalid/zero, mark for skip\nif (groupId === null || groupId === 0 || !VALID_GROUP_IDS.has(groupId)) {\n return [{ json: { ...agentOutput, validAssignment: false } }];\n}\n\nreturn [{ json: { ...agentOutput, validAssignment: true, group_id_used: groupId } }];"
|
|
},
|
|
"id": "89768e22-9f8d-4e06-9415-7614b1a8e46a",
|
|
"name": "Validate Group Assignment",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
928,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Log skipped/uncertain tickets for manual review\nconst item = $input.first().json;\nconsole.log(`[TRIAGE NEEDED] Ticket could not be auto-assigned. Ticket data: ${JSON.stringify({ id: item.id, subject: item.subject })}`);\nreturn [{ json: { status: 'triage_needed', ticket_id: item.id, subject: item.subject } }];"
|
|
},
|
|
"id": "50b5bd16-0774-4e76-a069-91531ed984bd",
|
|
"name": "Log Triage Needed",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1152,
|
|
96
|
|
]
|
|
},
|
|
{
|
|
"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": "67aff9eb-1927-486e-b98c-407f668fd794",
|
|
"name": "Valid Assignment?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1152,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "console.log('All tickets processed.');\nreturn [{ json: { status: 'done' } }];"
|
|
},
|
|
"id": "f00bbfd9-b595-4513-b5db-cddc7700d442",
|
|
"name": "All Done",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
32,
|
|
-224
|
|
]
|
|
},
|
|
{
|
|
"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": [
|
|
640,
|
|
208
|
|
],
|
|
"id": "7c26a027-1dcc-46da-adad-33e81019b477",
|
|
"name": "Anthropic Chat Model",
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "k9lzArwvHtlGPXmY",
|
|
"name": "Anthropic - Freshdesk"
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"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
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Validate Group Assignment": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Valid Assignment?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Valid Assignment?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Loop Each Ticket",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Triage Needed",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Log Triage Needed": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Loop Each Ticket",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update a ticket in Freshdesk": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Anthropic Chat Model": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"settings": {
|
|
"binaryMode": "separate",
|
|
"errorWorkflow": "tJSgL4iy63ys2NCA",
|
|
"executionOrder": "v1"
|
|
},
|
|
"staticData": {
|
|
"node:Every X minutes": {
|
|
"recurrenceRules": []
|
|
},
|
|
"global": {
|
|
"tickets": [],
|
|
"page": 1
|
|
}
|
|
},
|
|
"meta": {
|
|
"templateCredsSetupCompleted": true
|
|
},
|
|
"pinData": {},
|
|
"versionId": "e55d6d46-c2ff-4b1b-8a84-15cac7e145b9",
|
|
"activeVersionId": "e55d6d46-c2ff-4b1b-8a84-15cac7e145b9",
|
|
"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-04T13:28:31.528Z",
|
|
"createdAt": "2026-06-04T13:28:20.468Z",
|
|
"versionId": "e55d6d46-c2ff-4b1b-8a84-15cac7e145b9",
|
|
"workflowId": "WIwmV3nCtHM5TSUM",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"rule": {
|
|
"interval": [
|
|
{
|
|
"field": "minutes"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"id": "3fc30aa2-0ef7-4d4f-985a-95b5c8e681df",
|
|
"name": "Every X minutes",
|
|
"type": "n8n-nodes-base.scheduleTrigger",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
-1312,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Reset pagination state for this run\nconst sd = $getWorkflowStaticData('global');\nsd.tickets = [];\nsd.page = 1;\nreturn [{ json: { page: 1 } }];"
|
|
},
|
|
"id": "6341557a-414f-4bc3-b0b4-af1f2999d764",
|
|
"name": "Init Pagination",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1088,
|
|
-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": "47a0dbb0-c6fc-4fd7-88c8-7771b2193047",
|
|
"name": "Fetch Tickets Page",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
-864,
|
|
-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": "6343d141-b452-44da-ba12-e95963149074",
|
|
"name": "Collect Results",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-640,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"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": "0bb8f69a-e881-46c8-a541-c91bfcb45074",
|
|
"name": "More Pages?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-416,
|
|
-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": "01a298db-4c5e-45c3-9d00-4e327d517551",
|
|
"name": "Split Into Items",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-192,
|
|
-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": "8f85e242-80cf-4965-9266-ef27c3f780b5",
|
|
"name": "Has Tickets?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
32,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "1975c60e-7687-4415-8ac7-35d31468d492",
|
|
"name": "Loop Each Ticket",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
256,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"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 call the Update Freshdesk ticket tool.',\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; otherwise leave group_id as 0 (do not assign).',\n '4. Call the Update Freshdesk ticket tool with:',\n ` - Ticket_ID = ${ticketId}`,\n ' - Group_ID = the numeric group ID from above (or 0 if uncertain)',\n '5. Do not ask for confirmation. Act immediately.'\n].join('\\n');\n\nreturn [{ json: { ...t, chatInput } }];"
|
|
},
|
|
"id": "d54d96cc-349e-4df3-9f2b-3c982319693e",
|
|
"name": "Format Agent Prompt",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
480,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"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 assign them to the correct internal group by calling the Update Freshdesk ticket tool.\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## Decision Rules\n1. BILLING group: Only if the ticket is about the Masttro platform subscription, licensing fees, or payment for the SaaS service itself.\n2. AP MUS group: Vendor/supplier AP for the US entity. USD, W-9, 1099, ACH are strong signals.\n3. AP MMX group: Vendor/supplier AP for Mexico. CFDI, RFC, XML invoice, MXN, SAT are strong signals.\n4. AP MSAG group: Vendor/supplier AP for Switzerland or Europe. CHF, EUR (Swiss context), Topal, MHAG are strong signals.\n5. UNCERTAIN: If the entity cannot be determined and no billing signal exists, call the tool with Group_ID=0 to leave unassigned for manual triage.\n\n## Behavior\n- Always call the tool. Never respond with just text.\n- Never guess between two AP groups — if ambiguous between entities, use Group_ID=0.\n- Do not ask clarifying questions."
|
|
}
|
|
},
|
|
"id": "a6b76cb5-dc7b-4eb8-9b0d-a4952398651b",
|
|
"name": "AI Agent",
|
|
"type": "@n8n/n8n-nodes-langchain.agent",
|
|
"typeVersion": 3.1,
|
|
"position": [
|
|
704,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"operation": "update",
|
|
"ticketId": "={{ $fromAI('Ticket_ID', 'The numeric ID of the ticket to update', 'string') }}",
|
|
"updateFields": {
|
|
"group": "={{ $fromAI('Group_ID', 'The numeric Freshdesk group ID to assign the ticket to. Use 0 if uncertain.', 'number') }}"
|
|
}
|
|
},
|
|
"id": "e326cd7f-feed-41c6-b098-a6dc90c89ac4",
|
|
"name": "Update a ticket in Freshdesk",
|
|
"type": "n8n-nodes-base.freshdeskTool",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
832,
|
|
192
|
|
],
|
|
"credentials": {
|
|
"freshdeskApi": {
|
|
"id": "CjhwO4a2iGx7J4v0",
|
|
"name": "Freshdesk - Finance"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Validate that the group ID chosen by the AI is one of the known valid group IDs.\n// If it's 0 (uncertain) or invalid, we skip the update so the ticket stays unassigned for manual review.\n\nconst VALID_GROUP_IDS = new Set([158000821060, 158000821061, 158000821062, 158000821063]);\n\nconst agentOutput = $input.first().json;\n\n// The AI agent output is a string — extract the group_id it used from the tool call context.\n// We store it on the item via the tool call. Check output text for any override signals.\n// Primary guard: if the agent set group to 0 or a non-valid ID, route to skip branch.\nconst groupId = agentOutput.group_id_used !== undefined\n ? Number(agentOutput.group_id_used)\n : null;\n\n// If we can't parse or it's invalid/zero, mark for skip\nif (groupId === null || groupId === 0 || !VALID_GROUP_IDS.has(groupId)) {\n return [{ json: { ...agentOutput, validAssignment: false } }];\n}\n\nreturn [{ json: { ...agentOutput, validAssignment: true, group_id_used: groupId } }];"
|
|
},
|
|
"id": "89768e22-9f8d-4e06-9415-7614b1a8e46a",
|
|
"name": "Validate Group Assignment",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
928,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Log skipped/uncertain tickets for manual review\nconst item = $input.first().json;\nconsole.log(`[TRIAGE NEEDED] Ticket could not be auto-assigned. Ticket data: ${JSON.stringify({ id: item.id, subject: item.subject })}`);\nreturn [{ json: { status: 'triage_needed', ticket_id: item.id, subject: item.subject } }];"
|
|
},
|
|
"id": "50b5bd16-0774-4e76-a069-91531ed984bd",
|
|
"name": "Log Triage Needed",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1152,
|
|
96
|
|
]
|
|
},
|
|
{
|
|
"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": "67aff9eb-1927-486e-b98c-407f668fd794",
|
|
"name": "Valid Assignment?",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1152,
|
|
-32
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "console.log('All tickets processed.');\nreturn [{ json: { status: 'done' } }];"
|
|
},
|
|
"id": "f00bbfd9-b595-4513-b5db-cddc7700d442",
|
|
"name": "All Done",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
32,
|
|
-224
|
|
]
|
|
},
|
|
{
|
|
"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": [
|
|
640,
|
|
208
|
|
],
|
|
"id": "7c26a027-1dcc-46da-adad-33e81019b477",
|
|
"name": "Anthropic Chat Model",
|
|
"credentials": {
|
|
"anthropicApi": {
|
|
"id": "k9lzArwvHtlGPXmY",
|
|
"name": "Anthropic - Freshdesk"
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"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
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Validate Group Assignment": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Valid Assignment?",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Valid Assignment?": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Loop Each Ticket",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Triage Needed",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Log Triage Needed": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Loop Each Ticket",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Update a ticket in Freshdesk": {
|
|
"ai_tool": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "ai_tool",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Anthropic Chat Model": {
|
|
"ai_languageModel": [
|
|
[
|
|
{
|
|
"node": "AI Agent",
|
|
"type": "ai_languageModel",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"nodeGroups": [],
|
|
"authors": "Michael Nolan",
|
|
"name": "improvements, change llm to sonnet",
|
|
"description": "",
|
|
"autosaved": true
|
|
},
|
|
"tags": []
|
|
} |