Files
unraid-docker-manager/n8n-matching.json
T
Lucas Berger f57c706e01 feat(10.1-06): create n8n-matching.json sub-workflow
- Extract 12 matching/disambiguation logic nodes into sub-workflow
- 23 nodes total: trigger + router + 12 logic + 9 format-return nodes
- Three action paths: match_action, match_update, match_batch
- Every exit path returns JSON with action field per established pattern
- Adapted input references to use trigger data instead of main workflow refs
- No response nodes included (stay in main per locked decision)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 18:56:44 -05:00

866 lines
35 KiB
JSON

{
"name": "Container Matching",
"nodes": [
{
"parameters": {
"inputSource": "passthrough",
"schema": {
"schemaType": "fromFields",
"inputFieldName": "",
"fields": [
{
"fieldName": "action",
"fieldType": "string"
},
{
"fieldName": "containerList",
"fieldType": "string"
},
{
"fieldName": "searchTerm",
"fieldType": "string"
},
{
"fieldName": "selectedContainers",
"fieldType": "string"
},
{
"fieldName": "chatId",
"fieldType": "number"
},
{
"fieldName": "messageId",
"fieldType": "number"
}
]
}
},
"id": "trigger-matching-workflow",
"name": "When executed by another workflow",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
240,
300
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "match-action",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-match-action",
"leftValue": "={{ $json.action }}",
"rightValue": "match_action",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "match_action"
},
{
"id": "match-update",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-match-update",
"leftValue": "={{ $json.action }}",
"rightValue": "match_update",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "match_update"
},
{
"id": "match-batch",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "is-match-batch",
"leftValue": "={{ $json.action }}",
"rightValue": "match_batch",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "match_batch"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "switch-route-matching-action",
"name": "Route Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
460,
300
]
},
{
"parameters": {
"jsCode": "// Match container name for action commands\nconst input = $('When executed by another workflow').item.json;\nconst containerListRaw = input.containerList;\nconst searchTerm = input.searchTerm;\nconst chatId = input.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!containerListRaw || (typeof containerListRaw === 'string' && containerListRaw.trim() === '')) {\n throw new Error('Empty response');\n }\n containers = typeof containerListRaw === 'string' ? JSON.parse(containerListRaw) : containerListRaw;\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\nconst normalized = searchTerm.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n action: 'action',\n containerQuery: searchTerm,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results with all necessary context\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n action: 'action',\n containerQuery: searchTerm,\n chatId: chatId,\n allContainers: containers\n }\n}];"
},
"id": "code-match-container",
"name": "Match Container",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
100
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "docker-error",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "count-negative",
"leftValue": "={{ $json.matchCount }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "lt"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "error"
},
{
"id": "no-match",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "count-zero",
"leftValue": "={{ $json.matchCount }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "no_match"
},
{
"id": "single-match",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "count-one",
"leftValue": "={{ $json.matchCount }}",
"rightValue": 1,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "single"
},
{
"id": "multiple-matches",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "count-gt-one",
"leftValue": "={{ $json.matchCount }}",
"rightValue": 1,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "multiple"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "switch-match-count",
"name": "Check Match Count",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
920,
100
]
},
{
"parameters": {
"jsCode": "// Find closest match for suggestion\nconst query = $json.containerQuery.toLowerCase();\nconst containers = $json.allContainers;\nconst action = $json.action;\nconst chatId = $json.chatId;\n\n// Simple closest match: longest common substring or starts-with\nlet bestMatch = null;\nlet bestScore = 0;\n\nfor (const container of containers) {\n const name = container.Names[0].replace(/^\\//, '').toLowerCase();\n // Score by: contains query, or query contains name, or matching chars\n let score = 0;\n if (name.includes(query)) score = query.length * 2;\n else if (query.includes(name)) score = name.length * 1.5;\n else {\n // Simple: count matching starting characters\n for (let i = 0; i < Math.min(query.length, name.length); i++) {\n if (query[i] === name[i]) score++;\n else break;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestMatch = container;\n }\n}\n\n// Require minimum score of 2 to suggest\nif (!bestMatch || bestScore < 2) {\n return { json: { hasSuggestion: false, query, action, chatId } };\n}\n\nconst suggestedName = bestMatch.Names[0].replace(/^\\//, '');\nconst suggestedId = bestMatch.Id.substring(0, 12); // Short ID for callback_data\n\nreturn {\n json: {\n hasSuggestion: true,\n query,\n action,\n chatId,\n suggestedName,\n suggestedId,\n timestamp: Date.now()\n }\n};"
},
"id": "code-find-closest",
"name": "Find Closest Match",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "has-suggestion",
"leftValue": "={{ $json.hasSuggestion }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-has-suggestion",
"name": "Check Suggestion",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1360,
0
]
},
{
"parameters": {
"jsCode": "// Build suggestion keyboard for Telegram\nconst { chatId, query, action, suggestedName, suggestedId, timestamp } = $json;\n\n// Use modern action callback format: action:{action}:{containerName}\nconst callbackData = `action:${action}:${suggestedName}`;\n\nreturn {\n json: {\n action: \"suggestion\",\n chat_id: chatId,\n text: `No container '<b>${query}</b>' found.\\n\\nDid you mean <b>${suggestedName}</b>?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${suggestedName}`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n }\n }\n};"
},
"id": "code-build-suggestion",
"name": "Build Suggestion Keyboard",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1580,
-100
]
},
{
"parameters": {
"jsCode": "// No match and no suggestion - return no_match action\nconst { query, action, chatId } = $json;\n\nreturn {\n json: {\n action: \"no_match\",\n query: query,\n chatId: chatId\n }\n};"
},
"id": "code-format-no-match-return",
"name": "Format No Match Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1580,
100
]
},
{
"parameters": {
"jsCode": "// Single action match found - return matched action\nconst data = $json;\nconst containerId = data.matches[0].Id;\nconst containerName = data.matches[0].Name;\n\nreturn {\n json: {\n action: \"matched\",\n containerId: containerId,\n containerName: containerName,\n matches: data.matches,\n matchCount: data.matchCount,\n actionType: data.action,\n containerQuery: data.containerQuery,\n chatId: data.chatId,\n allContainers: data.allContainers\n }\n};"
},
"id": "code-format-single-match-return",
"name": "Format Single Match Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
200
]
},
{
"parameters": {
"jsCode": "// Docker error - return error action\nconst data = $json;\n\nreturn {\n json: {\n action: \"error\",\n errorMessage: data.text || \"Cannot connect to Docker\",\n chatId: data.chatId\n }\n};"
},
"id": "code-format-error-return",
"name": "Format Error Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
-100
]
},
{
"parameters": {
"jsCode": "// Multiple action matches - return for main workflow to build keyboard\nconst data = $json;\n\nreturn {\n json: {\n action: \"multiple\",\n matches: data.matches,\n matchCount: data.matchCount,\n actionType: data.action,\n containerQuery: data.containerQuery,\n chatId: data.chatId,\n allContainers: data.allContainers\n }\n};"
},
"id": "code-format-multiple-match-return",
"name": "Format Multiple Match Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
300
]
},
{
"parameters": {
"jsCode": "// Match container name for update commands\nconst input = $('When executed by another workflow').item.json;\nconst containerListRaw = input.containerList;\nconst searchTerm = input.searchTerm;\nconst chatId = input.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!containerListRaw || (typeof containerListRaw === 'string' && containerListRaw.trim() === '')) {\n throw new Error('Empty response');\n }\n containers = typeof containerListRaw === 'string' ? JSON.parse(containerListRaw) : containerListRaw;\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\nconst normalized = searchTerm.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n containerQuery: searchTerm,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: searchTerm,\n chatId: chatId,\n allContainers: containers\n }\n}];"
},
"id": "code-match-update-container",
"name": "Match Update Container",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
500
]
},
{
"parameters": {
"rules": {
"values": [
{
"id": "update-docker-error",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "count-negative",
"leftValue": "={{ $json.matchCount }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "lt"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "error"
},
{
"id": "update-no-match",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "count-zero",
"leftValue": "={{ $json.matchCount }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "no_match"
},
{
"id": "update-single-match",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "count-one",
"leftValue": "={{ $json.matchCount }}",
"rightValue": 1,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "single"
},
{
"id": "update-multiple-matches",
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"id": "count-gt-one",
"leftValue": "={{ $json.matchCount }}",
"rightValue": 1,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "multiple"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "switch-update-match-count",
"name": "Check Update Match Count",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
920,
500
]
},
{
"parameters": {
"jsCode": "// Docker error on update - return error action\nconst data = $json;\n\nreturn {\n json: {\n action: \"error\",\n errorMessage: data.text || \"Cannot connect to Docker\",\n chatId: data.chatId\n }\n};"
},
"id": "code-format-update-error-return",
"name": "Format Update Error Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
400
]
},
{
"parameters": {
"jsCode": "// No update match found - return no_match_update action\nconst data = $json;\n\nreturn {\n json: {\n action: \"no_match_update\",\n containerQuery: data.containerQuery,\n chatId: data.chatId\n }\n};"
},
"id": "code-format-update-no-match-return",
"name": "Format Update No Match Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
500
]
},
{
"parameters": {
"jsCode": "// Single update match found - return matched_update action\nconst data = $json;\nconst containerId = data.matches[0].Id;\nconst containerName = data.matches[0].Name;\n\nreturn {\n json: {\n action: \"matched_update\",\n containerId: containerId,\n containerName: containerName,\n matches: data.matches,\n matchCount: data.matchCount,\n containerQuery: data.containerQuery,\n chatId: data.chatId\n }\n};"
},
"id": "code-format-update-single-return",
"name": "Format Update Single Match Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
600
]
},
{
"parameters": {
"jsCode": "// Multiple update matches found - return multiple_update action\nconst data = $json;\n\nreturn {\n json: {\n action: \"multiple_update\",\n matches: data.matches,\n matchCount: data.matchCount,\n containerQuery: data.containerQuery,\n chatId: data.chatId\n }\n};"
},
"id": "code-format-update-multiple-return",
"name": "Format Update Multiple Match Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
700
]
},
{
"parameters": {
"jsCode": "// Match batch container names with exact-match priority\nconst input = $('When executed by another workflow').item.json;\nconst containerListRaw = input.containerList;\nconst selectedContainers = input.selectedContainers;\nconst chatId = input.chatId;\nconst messageId = input.messageId;\n\n// Parse selectedContainers (CSV string) into array\nconst containerNames = typeof selectedContainers === 'string' \n ? selectedContainers.split(',').map(s => s.trim()).filter(s => s)\n : (Array.isArray(selectedContainers) ? selectedContainers : []);\n\n// Parse Docker container list\nlet containers;\ntry {\n if (!containerListRaw || (typeof containerListRaw === 'string' && containerListRaw.trim() === '')) {\n throw new Error('Empty response');\n }\n containers = typeof containerListRaw === 'string' ? JSON.parse(containerListRaw) : containerListRaw;\n} catch (e) {\n return [{\n json: {\n action: \"error\",\n errorMessage: 'Cannot connect to Docker',\n chatId: chatId\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Match results\nconst allMatched = []; // Containers that matched exactly\nconst needsDisambiguation = []; // { input, matches: [...] }\nconst notFound = []; // Container names with no matches\n\nfor (const inputName of containerNames) {\n const normalized = inputName.toLowerCase().trim();\n \n // 1. Check for exact match first (highest priority)\n const exactMatch = containers.find(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName === normalized;\n });\n \n if (exactMatch) {\n // Exact match wins - no disambiguation needed\n allMatched.push({\n Id: exactMatch.Id,\n Name: exactMatch.Names[0].replace(/^\\//, ''),\n State: exactMatch.State,\n Image: exactMatch.Image,\n inputName: inputName,\n matchType: 'exact'\n });\n continue;\n }\n \n // 2. Check for substring matches (fuzzy)\n const fuzzyMatches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n });\n \n if (fuzzyMatches.length === 0) {\n // No matches at all\n notFound.push(inputName);\n } else if (fuzzyMatches.length === 1) {\n // Single fuzzy match - treat as found\n const match = fuzzyMatches[0];\n allMatched.push({\n Id: match.Id,\n Name: match.Names[0].replace(/^\\//, ''),\n State: match.State,\n Image: match.Image,\n inputName: inputName,\n matchType: 'fuzzy'\n });\n } else {\n // Multiple fuzzy matches - needs disambiguation\n needsDisambiguation.push({\n input: inputName,\n matches: fuzzyMatches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n }))\n });\n }\n}\n\nreturn [{\n json: {\n allMatched: allMatched,\n needsDisambiguation: needsDisambiguation,\n notFound: notFound,\n hasDisambiguation: needsDisambiguation.length > 0,\n hasNotFound: notFound.length > 0,\n action: 'batch',\n chatId: chatId,\n messageId: messageId,\n originalContainerNames: containerNames,\n allContainers: containers\n }\n}];"
},
"id": "code-match-batch-containers",
"name": "Match Batch Containers",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
900
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "has-disambiguation",
"leftValue": "={{ $json.hasDisambiguation }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-needs-disambiguation",
"name": "Needs Disambiguation",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
920,
900
]
},
{
"parameters": {
"jsCode": "// Build disambiguation message with inline keyboard\nconst data = $json;\nconst disambig = data.needsDisambiguation;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message text\nlet text = '<b>Multiple matches found:</b>\\n\\n';\n\nfor (const item of disambig) {\n const matchNames = item.matches.map(m => m.Name).join(', ');\n text += `\\u2022 '<b>${item.input}</b>' matches: ${matchNames}\\n`;\n}\n\ntext += '\\nPlease be more specific or select from options below.';\n\n// Build inline keyboard with exact container names\n// Each row has one disambiguation option\nconst keyboard = [];\n\nfor (const item of disambig) {\n // Create a row for each ambiguous input\n const row = [];\n for (const match of item.matches.slice(0, 3)) { // Max 3 options per row\n // Callback format: bselect:{action}:{containerName}\n row.push({\n text: match.Name,\n callback_data: `bselect:${action}:${match.Name}`\n });\n }\n keyboard.push(row);\n}\n\n// Add cancel button\nkeyboard.push([{ text: 'Cancel', callback_data: '{\"a\":\"x\"}' }]);\n\nreturn {\n json: {\n action: \"disambiguation\",\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: keyboard\n }\n }\n};"
},
"id": "code-build-disambiguation",
"name": "Build Disambiguation Message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
800
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "has-not-found",
"leftValue": "={{ $json.hasNotFound }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-has-not-found",
"name": "Has Not Found",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1140,
1000
]
},
{
"parameters": {
"jsCode": "// Build not found message\nconst data = $json;\nconst notFound = data.notFound;\nconst allMatched = data.allMatched;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message\nlet text = '';\n\nif (notFound.length > 0) {\n text += '<b>Container(s) not found:</b>\\n';\n for (const name of notFound) {\n text += `\\u2022 ${name}\\n`;\n }\n text += '\\n';\n}\n\nif (allMatched.length > 0) {\n text += `<b>Found ${allMatched.length} container(s):</b>\\n`;\n for (const c of allMatched) {\n text += `\\u2022 ${c.Name}\\n`;\n }\n text += `\\nProceed with ${action} on found containers?`;\n \n // Build confirmation keyboard\n const names = allMatched.map(c => c.Name).join(',');\n const timestamp = Math.floor(Date.now() / 1000);\n \n return {\n json: {\n action: \"not_found\",\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${allMatched.length} containers`, callback_data: `bexec:${action}:${names}:${timestamp}` },\n { text: 'Cancel', callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n },\n hasMatched: true,\n matchedContainers: allMatched\n }\n };\n} else {\n // No containers matched at all\n return {\n json: {\n action: \"not_found\",\n chat_id: chatId,\n text: text + 'No valid containers to ' + action + '.',\n parse_mode: 'HTML',\n hasMatched: false\n }\n };\n}"
},
"id": "code-build-not-found",
"name": "Build Not Found Message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1360,
1100
]
},
{
"parameters": {
"jsCode": "// All batch containers matched - return batch_matched action\nconst data = $json;\n\nreturn {\n json: {\n action: \"batch_matched\",\n matchedContainers: data.allMatched,\n chatId: data.chatId,\n messageId: data.messageId,\n originalContainerNames: data.originalContainerNames\n }\n};"
},
"id": "code-format-batch-matched-return",
"name": "Format Batch Matched Return",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1360,
1000
]
}
],
"connections": {
"When executed by another workflow": {
"main": [
[
{
"node": "Route Action",
"type": "main",
"index": 0
}
]
]
},
"Route Action": {
"main": [
[
{
"node": "Match Container",
"type": "main",
"index": 0
}
],
[
{
"node": "Match Update Container",
"type": "main",
"index": 0
}
],
[
{
"node": "Match Batch Containers",
"type": "main",
"index": 0
}
]
]
},
"Match Container": {
"main": [
[
{
"node": "Check Match Count",
"type": "main",
"index": 0
}
]
]
},
"Check Match Count": {
"main": [
[
{
"node": "Format Error Return",
"type": "main",
"index": 0
}
],
[
{
"node": "Find Closest Match",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Single Match Return",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Multiple Match Return",
"type": "main",
"index": 0
}
]
]
},
"Find Closest Match": {
"main": [
[
{
"node": "Check Suggestion",
"type": "main",
"index": 0
}
]
]
},
"Check Suggestion": {
"main": [
[
{
"node": "Build Suggestion Keyboard",
"type": "main",
"index": 0
}
],
[
{
"node": "Format No Match Return",
"type": "main",
"index": 0
}
]
]
},
"Match Update Container": {
"main": [
[
{
"node": "Check Update Match Count",
"type": "main",
"index": 0
}
]
]
},
"Check Update Match Count": {
"main": [
[
{
"node": "Format Update Error Return",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Update No Match Return",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Update Single Match Return",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Update Multiple Match Return",
"type": "main",
"index": 0
}
]
]
},
"Match Batch Containers": {
"main": [
[
{
"node": "Needs Disambiguation",
"type": "main",
"index": 0
}
]
]
},
"Needs Disambiguation": {
"main": [
[
{
"node": "Build Disambiguation Message",
"type": "main",
"index": 0
}
],
[
{
"node": "Has Not Found",
"type": "main",
"index": 0
}
]
]
},
"Has Not Found": {
"main": [
[
{
"node": "Build Not Found Message",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Batch Matched Return",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"callerPolicy": "any"
}
}