From adbd7bd87dc09dee7fb744d1f54af912abba3003 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Wed, 4 Feb 2026 16:19:33 -0500 Subject: [PATCH] fix(batch): resolve container IDs in sub-workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When batch operations are triggered via keyboard callbacks, the main workflow only passes container names (not IDs). The sub-workflows now: - Check if containerId is empty - If empty, query Docker API to resolve name → ID - Then proceed with the action This fixes batch start/stop/restart/update operations failing with 404 "page not found" errors. Co-Authored-By: Claude Opus 4.5 --- n8n-container-actions.json | 121 +++++++++++++++++++++++++++++++++---- n8n-container-update.json | 99 ++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 11 deletions(-) diff --git a/n8n-container-actions.json b/n8n-container-actions.json index b537e45..cb454b9 100644 --- a/n8n-container-actions.json +++ b/n8n-container-actions.json @@ -44,6 +44,65 @@ 300 ] }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "has-container-id", + "leftValue": "={{ $json.containerId }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEquals" + } + } + ], + "combinator": "and" + } + }, + "id": "if-has-id", + "name": "Has Container ID?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 420, + 300 + ] + }, + { + "parameters": { + "url": "http://docker-socket-proxy:2375/v1.47/containers/json?all=true", + "options": { + "timeout": 5000 + } + }, + "id": "http-get-containers", + "name": "Get All Containers", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 600, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Find container by name and resolve ID\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerName = triggerData.containerName;\nconst containers = $input.all();\n\n// Normalize function to strip leading slash\nconst normalizeName = (name) => name.replace(/^\\//, '').toLowerCase();\nconst searchName = normalizeName(containerName);\n\n// Find matching container\nlet matched = null;\nfor (const item of containers) {\n const c = item.json;\n if (c.Names && c.Names.length > 0) {\n const cName = normalizeName(c.Names[0]);\n if (cName === searchName || cName.includes(searchName)) {\n matched = c;\n break;\n }\n }\n}\n\nif (!matched) {\n // Return error - container not found\n return {\n json: {\n ...triggerData,\n containerId: '',\n error: `Container '${containerName}' not found`\n }\n };\n}\n\nreturn {\n json: {\n ...triggerData,\n containerId: matched.Id\n }\n};" + }, + "id": "code-resolve-id", + "name": "Resolve Container ID", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 780, + 400 + ] + }, { "parameters": { "rules": { @@ -128,7 +187,7 @@ "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [ - 460, + 960, 300 ] }, @@ -145,7 +204,7 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 680, + 1160, 200 ], "onError": "continueRegularOutput" @@ -163,7 +222,7 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 680, + 1160, 300 ], "onError": "continueRegularOutput" @@ -181,53 +240,93 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 680, + 1160, 400 ], "onError": "continueRegularOutput" }, { "parameters": { - "jsCode": "// Format start action result\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerId = triggerData.containerId;\nconst containerName = triggerData.containerName;\nconst action = triggerData.action;\nconst chatId = triggerData.chatId;\nconst messageId = triggerData.messageId;\nconst responseMode = triggerData.responseMode;\n\n// Docker API returns 204 No Content on success (empty response body)\n// Error responses contain 'message' field\nconst response = $input.item.json || {};\nconst hasError = response.message || response.error || false;\n\n// Success = no error message in response (empty {} means 204 success)\nconst success = !hasError;\n\nlet message;\nif (success) {\n message = `\\u25B6\\uFE0F ${containerName} started successfully`;\n} else {\n message = `\\u274C Failed to start ${containerName}`;\n}\n\nreturn {\n json: {\n success,\n message,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n};" + "jsCode": "// Format start action result\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerId = $json.containerId || triggerData.containerId;\nconst containerName = triggerData.containerName;\nconst action = triggerData.action;\nconst chatId = triggerData.chatId;\nconst messageId = triggerData.messageId;\nconst responseMode = triggerData.responseMode;\n\n// Docker API returns 204 No Content on success (empty response body)\n// Error responses contain 'message' field\nconst response = $input.item.json || {};\nconst hasError = response.message || response.error || false;\n\n// Success = no error message in response (empty {} means 204 success)\nconst success = !hasError;\n\nlet message;\nif (success) {\n message = `\\u25B6\\uFE0F ${containerName} started successfully`;\n} else {\n message = `\\u274C Failed to start ${containerName}`;\n}\n\nreturn {\n json: {\n success,\n message,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n};" }, "id": "code-format-start-result", "name": "Format Start Result", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 900, + 1380, 200 ] }, { "parameters": { - "jsCode": "// Format stop action result\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerId = triggerData.containerId;\nconst containerName = triggerData.containerName;\nconst action = triggerData.action;\nconst chatId = triggerData.chatId;\nconst messageId = triggerData.messageId;\nconst responseMode = triggerData.responseMode;\n\n// Docker API returns 204 No Content on success (empty response body)\n// Error responses contain 'message' field\nconst response = $input.item.json || {};\nconst hasError = response.message || response.error || false;\n\n// Success = no error message in response (empty {} means 204 success)\nconst success = !hasError;\n\nlet message;\nif (success) {\n message = `\\u23F9\\uFE0F ${containerName} stopped`;\n} else {\n message = `\\u274C Failed to stop ${containerName}`;\n}\n\nreturn {\n json: {\n success,\n message,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n};" + "jsCode": "// Format stop action result\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerId = $json.containerId || triggerData.containerId;\nconst containerName = triggerData.containerName;\nconst action = triggerData.action;\nconst chatId = triggerData.chatId;\nconst messageId = triggerData.messageId;\nconst responseMode = triggerData.responseMode;\n\n// Docker API returns 204 No Content on success (empty response body)\n// Error responses contain 'message' field\nconst response = $input.item.json || {};\nconst hasError = response.message || response.error || false;\n\n// Success = no error message in response (empty {} means 204 success)\nconst success = !hasError;\n\nlet message;\nif (success) {\n message = `\\u23F9\\uFE0F ${containerName} stopped`;\n} else {\n message = `\\u274C Failed to stop ${containerName}`;\n}\n\nreturn {\n json: {\n success,\n message,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n};" }, "id": "code-format-stop-result", "name": "Format Stop Result", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 900, + 1380, 300 ] }, { "parameters": { - "jsCode": "// Format restart action result\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerId = triggerData.containerId;\nconst containerName = triggerData.containerName;\nconst action = triggerData.action;\nconst chatId = triggerData.chatId;\nconst messageId = triggerData.messageId;\nconst responseMode = triggerData.responseMode;\n\n// Docker API returns 204 No Content on success (empty response body)\n// Error responses contain 'message' field\nconst response = $input.item.json || {};\nconst hasError = response.message || response.error || false;\n\n// Success = no error message in response (empty {} means 204 success)\nconst success = !hasError;\n\nlet message;\nif (success) {\n message = `\\u{1F504} ${containerName} restarted`;\n} else {\n message = `\\u274C Failed to restart ${containerName}`;\n}\n\nreturn {\n json: {\n success,\n message,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n};" + "jsCode": "// Format restart action result\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerId = $json.containerId || triggerData.containerId;\nconst containerName = triggerData.containerName;\nconst action = triggerData.action;\nconst chatId = triggerData.chatId;\nconst messageId = triggerData.messageId;\nconst responseMode = triggerData.responseMode;\n\n// Docker API returns 204 No Content on success (empty response body)\n// Error responses contain 'message' field\nconst response = $input.item.json || {};\nconst hasError = response.message || response.error || false;\n\n// Success = no error message in response (empty {} means 204 success)\nconst success = !hasError;\n\nlet message;\nif (success) {\n message = `\\u{1F504} ${containerName} restarted`;\n} else {\n message = `\\u274C Failed to restart ${containerName}`;\n}\n\nreturn {\n json: {\n success,\n message,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n};" }, "id": "code-format-restart-result", "name": "Format Restart Result", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 900, + 1380, 400 ] } ], "connections": { "When executed by another workflow": { + "main": [ + [ + { + "node": "Has Container ID?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has Container ID?": { + "main": [ + [ + { + "node": "Route Action", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Get All Containers", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get All Containers": { + "main": [ + [ + { + "node": "Resolve Container ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "Resolve Container ID": { "main": [ [ { @@ -301,4 +400,4 @@ "executionOrder": "v1", "callerPolicy": "any" } -} \ No newline at end of file +} diff --git a/n8n-container-update.json b/n8n-container-update.json index 1ff3d97..0e00ddf 100644 --- a/n8n-container-update.json +++ b/n8n-container-update.json @@ -39,6 +39,65 @@ 300 ] }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "has-container-id", + "leftValue": "={{ $json.containerId }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "notEquals" + } + } + ], + "combinator": "and" + } + }, + "id": "if-has-id", + "name": "Has Container ID?", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 400, + 300 + ] + }, + { + "parameters": { + "url": "http://docker-socket-proxy:2375/v1.47/containers/json?all=true", + "options": { + "timeout": 5000 + } + }, + "id": "http-get-containers", + "name": "Get All Containers", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 560, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Find container by name and resolve ID\nconst triggerData = $('When executed by another workflow').item.json;\nconst containerName = triggerData.containerName;\nconst containers = $input.all();\n\n// Normalize function to strip leading slash\nconst normalizeName = (name) => name.replace(/^\\//, '').toLowerCase();\nconst searchName = normalizeName(containerName);\n\n// Find matching container\nlet matched = null;\nfor (const item of containers) {\n const c = item.json;\n if (c.Names && c.Names.length > 0) {\n const cName = normalizeName(c.Names[0]);\n if (cName === searchName || cName.includes(searchName)) {\n matched = c;\n break;\n }\n }\n}\n\nif (!matched) {\n throw new Error(`Container '${containerName}' not found`);\n}\n\nreturn {\n json: {\n ...triggerData,\n containerId: matched.Id\n }\n};" + }, + "id": "code-resolve-id", + "name": "Resolve Container ID", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 720, + 400 + ] + }, { "parameters": { "method": "GET", @@ -616,6 +675,46 @@ ], "connections": { "When executed by another workflow": { + "main": [ + [ + { + "node": "Has Container ID?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Has Container ID?": { + "main": [ + [ + { + "node": "Inspect Container", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Get All Containers", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get All Containers": { + "main": [ + [ + { + "node": "Resolve Container ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "Resolve Container ID": { "main": [ [ {