{ "name": "Container Actions", "nodes": [ { "parameters": { "inputSource": "passthrough", "schema": { "schemaType": "fromFields", "inputFieldName": "", "fields": [ { "fieldName": "containerId", "fieldType": "string" }, { "fieldName": "containerName", "fieldType": "string" }, { "fieldName": "action", "fieldType": "string" }, { "fieldName": "chatId", "fieldType": "number" }, { "fieldName": "messageId", "fieldType": "number" }, { "fieldName": "responseMode", "fieldType": "string" }, { "fieldName": "correlationId", "fieldType": "string" } ] } }, "id": "trigger-sub-workflow", "name": "When executed by another workflow", "type": "n8n-nodes-base.executeWorkflowTrigger", "typeVersion": 1.1, "position": [ 240, 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": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\"query\": \"query { docker { containers { id names state image } } }\"}", "options": { "timeout": 15000 }, "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth" }, "id": "http-get-containers", "name": "Query All Containers", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 600, 400 ], "onError": "continueRegularOutput", "credentials": { "httpHeaderAuth": { "id": "unraid-api-key-credential-id", "name": "Unraid API Key" } } }, { "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 unraidId: matched.Id // Add PrefixedID for downstream mutations\n }\n};" }, "id": "code-resolve-id", "name": "Resolve Container ID", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 780, 400 ] }, { "parameters": { "rules": { "values": [ { "id": "route-start", "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose" }, "conditions": [ { "id": "is-start", "leftValue": "={{ $json.action }}", "rightValue": "start", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "start" }, { "id": "route-stop", "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose" }, "conditions": [ { "id": "is-stop", "leftValue": "={{ $json.action }}", "rightValue": "stop", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "stop" }, { "id": "route-restart", "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose" }, "conditions": [ { "id": "is-restart", "leftValue": "={{ $json.action }}", "rightValue": "restart", "operator": { "type": "string", "operation": "equals" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "restart" } ] }, "options": { "fallbackOutput": "none" } }, "id": "switch-action-type", "name": "Route Action", "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [ 960, 300 ] }, { "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 }, "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth" }, "id": "http-start-container", "name": "Start Container", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1160, 200 ], "onError": "continueRegularOutput", "credentials": { "httpHeaderAuth": { "id": "unraid-api-key-credential-id", "name": "Unraid API Key" } } }, { "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 }, "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth" }, "id": "http-stop-container", "name": "Stop Container", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1160, 300 ], "onError": "continueRegularOutput", "credentials": { "httpHeaderAuth": { "id": "unraid-api-key-credential-id", "name": "Unraid API Key" } } }, { "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 }, "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth" }, "id": "http-stop-for-restart", "name": "Stop For Restart", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1160, 400 ], "onError": "continueRegularOutput", "credentials": { "httpHeaderAuth": { "id": "unraid-api-key-credential-id", "name": "Unraid API Key" } } }, { "parameters": { "jsCode": "// Format start action result\n// Get data from Route Action (works for both direct and resolved ID paths)\nconst routeData = $('Route Action').item.json;\nconst containerId = routeData.containerId;\nconst containerName = routeData.containerName;\nconst action = routeData.action;\nconst chatId = routeData.chatId;\nconst messageId = routeData.messageId;\nconst responseMode = routeData.responseMode;\nconst correlationId = routeData.correlationId || '';\n\nconst response = $input.item.json || {};\n\n// Check HTTP status codes first (Docker API with onError:continueRegularOutput)\nif (response.statusCode) {\n if (response.statusCode === 304) {\n // Already in desired state\n return {\n json: {\n success: true,\n message: `\\u2705 ${containerName} is already started`,\n correlationId,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n };\n } else if (response.statusCode === 404) {\n return {\n json: {\n success: false,\n message: `\\u274C Container ${containerName} not found`,\n error: {\n workflow: 'n8n-actions',\n node: 'Format Start Result',\n message: `Container ${containerName} not found`,\n httpCode: 404,\n rawResponse: JSON.stringify(response).substring(0, 1000)\n },\n correlationId,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n };\n } else if (response.statusCode >= 500) {\n return {\n json: {\n success: false,\n message: `\\u274C Server error starting ${containerName}: ${response.statusMessage || 'Unknown error'}`,\n error: {\n workflow: 'n8n-actions',\n node: 'Format Start Result',\n message: `Server error starting container`,\n httpCode: response.statusCode,\n rawResponse: JSON.stringify(response).substring(0, 1000)\n },\n correlationId,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n };\n }\n}\n\n// Docker API returns 204 No Content on success (empty response body)\n// Error responses contain 'message' field\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 ...(success ? {} : { error: {\n workflow: 'n8n-actions',\n node: 'Format Start Result',\n message: `Failed to start container`,\n httpCode: response.statusCode || null,\n rawResponse: JSON.stringify(response).substring(0, 1000)\n }}),\n correlationId,\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": [ 1380, 200 ] }, { "parameters": { "jsCode": "// Format stop action result\n// Get data from Route Action (works for both direct and resolved ID paths)\nconst routeData = $('Route Action').item.json;\nconst containerId = routeData.containerId;\nconst containerName = routeData.containerName;\nconst action = routeData.action;\nconst chatId = routeData.chatId;\nconst messageId = routeData.messageId;\nconst responseMode = routeData.responseMode;\nconst correlationId = routeData.correlationId || '';\n\nconst response = $input.item.json || {};\n\n// Check HTTP status codes first (Docker API with onError:continueRegularOutput)\nif (response.statusCode) {\n if (response.statusCode === 304) {\n // Already in desired state\n return {\n json: {\n success: true,\n message: `\\u2705 ${containerName} is already stopped`,\n correlationId,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n };\n } else if (response.statusCode === 404) {\n return {\n json: {\n success: false,\n message: `\\u274C Container ${containerName} not found`,\n error: {\n workflow: 'n8n-actions',\n node: 'Format Stop Result',\n message: `Container ${containerName} not found`,\n httpCode: 404,\n rawResponse: JSON.stringify(response).substring(0, 1000)\n },\n correlationId,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n };\n } else if (response.statusCode >= 500) {\n return {\n json: {\n success: false,\n message: `\\u274C Server error stopping ${containerName}: ${response.statusMessage || 'Unknown error'}`,\n error: {\n workflow: 'n8n-actions',\n node: 'Format Stop Result',\n message: `Server error stoping container`,\n httpCode: response.statusCode,\n rawResponse: JSON.stringify(response).substring(0, 1000)\n },\n correlationId,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n };\n }\n}\n\n// Docker API returns 204 No Content on success (empty response body)\n// Error responses contain 'message' field\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 ...(success ? {} : { error: {\n workflow: 'n8n-actions',\n node: 'Format Stop Result',\n message: `Failed to stop container`,\n httpCode: response.statusCode || null,\n rawResponse: JSON.stringify(response).substring(0, 1000)\n }}),\n correlationId,\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": [ 1380, 300 ] }, { "parameters": { "jsCode": "// Format restart action result\n// Get data from Route Action (works for both direct and resolved ID paths)\nconst routeData = $('Route Action').item.json;\nconst containerId = routeData.containerId;\nconst containerName = routeData.containerName;\nconst action = routeData.action;\nconst chatId = routeData.chatId;\nconst messageId = routeData.messageId;\nconst responseMode = routeData.responseMode;\nconst correlationId = routeData.correlationId || '';\n\nconst response = $input.item.json || {};\n\n// Check HTTP status codes first (Docker API with onError:continueRegularOutput)\nif (response.statusCode) {\n if (response.statusCode === 304) {\n // Already in desired state (running)\n return {\n json: {\n success: true,\n message: `\\u2705 ${containerName} is already started`,\n correlationId,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n };\n } else if (response.statusCode === 404) {\n return {\n json: {\n success: false,\n message: `\\u274C Container ${containerName} not found`,\n error: {\n workflow: 'n8n-actions',\n node: 'Format Restart Result',\n message: `Container ${containerName} not found`,\n httpCode: 404,\n rawResponse: JSON.stringify(response).substring(0, 1000)\n },\n correlationId,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n };\n } else if (response.statusCode >= 500) {\n return {\n json: {\n success: false,\n message: `\\u274C Server error restarting ${containerName}: ${response.statusMessage || 'Unknown error'}`,\n error: {\n workflow: 'n8n-actions',\n node: 'Format Restart Result',\n message: `Server error restarting container`,\n httpCode: response.statusCode,\n rawResponse: JSON.stringify(response).substring(0, 1000)\n },\n correlationId,\n action,\n containerName,\n containerId,\n chatId,\n messageId,\n responseMode\n }\n };\n }\n}\n\n// Docker API returns 204 No Content on success (empty response body)\n// Error responses contain 'message' field\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 ...(success ? {} : { error: {\n workflow: 'n8n-actions',\n node: 'Format Restart Result',\n message: `Failed to restart container`,\n httpCode: response.statusCode || null,\n rawResponse: JSON.stringify(response).substring(0, 1000)\n }}),\n correlationId,\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": [ 1380, 400 ] }, { "parameters": { "jsCode": "// GraphQL Response Normalizer - Transform Unraid GraphQL to Docker API contract\n// Input: $input.item.json = raw GraphQL response\n// Output: Array of normalized containers\n\nconst response = $input.item.json;\n\n// Validation: Check for GraphQL errors\nif (response.errors && response.errors.length > 0) {\n const messages = response.errors.map(e => e.message).join('; ');\n throw new Error(`GraphQL error: ${messages}`);\n}\n\n// Validation: Check response structure\nif (!response.data?.docker?.containers) {\n throw new Error('Invalid GraphQL response structure: missing data.docker.containers');\n}\n\n// State mapping: Unraid UPPERCASE -> Docker lowercase\nconst stateMap = {\n 'RUNNING': 'running',\n 'STOPPED': 'exited', // Docker convention: stopped = exited\n 'PAUSED': 'paused'\n};\n\nfunction normalizeState(unraidState) {\n return stateMap[unraidState] || unraidState.toLowerCase();\n}\n\n// Transform each container\nconst containers = response.data.docker.containers;\nconst normalized = containers.map(container => {\n const dockerState = normalizeState(container.state);\n \n return {\n // Core fields matching Docker API contract\n Id: container.id, // Keep full PrefixedID (registry handles translation)\n Names: container.names, // Already has '/' prefix (Phase 14 verified)\n State: dockerState, // Normalized lowercase state\n Status: dockerState, // Docker has separate Status field\n Image: '', // Not available in basic query\n \n // Debug field: preserve original Unraid ID\n _unraidId: container.id\n };\n});\n\n// Return as array of items (n8n multi-item output format)\nreturn normalized.map(container => ({ json: container }));\n" }, "id": "code-graphql-normalizer", "name": "GraphQL Response Normalizer", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 660, 400 ] }, { "parameters": { "mode": "runOnceForAllItems", "jsCode": "// Update Container ID Registry with fresh container data\n// Input: Array of containers from Normalizer\n// Output: Pass through all containers unchanged\n\nconst containers = $input.all().map(item => item.json);\n\n// Get static data registry\nconst registry = $getWorkflowStaticData('global');\nif (!registry._containerIdMap) {\n registry._containerIdMap = JSON.stringify({});\n}\n\nconst containerMap = {};\n\nfor (const container of containers) {\n // Extract container name: strip leading '/', lowercase\n const rawName = container.Names[0];\n const name = rawName.startsWith('/') ? rawName.substring(1).toLowerCase() : rawName.toLowerCase();\n \n // Map name -> {name, unraidId}\n // The container.Id field IS the PrefixedID (129-char format)\n containerMap[name] = {\n name: name,\n unraidId: container.Id\n };\n}\n\n// Store timestamp\nregistry._lastRefresh = Date.now();\n\n// Serialize (top-level assignment - this is what n8n persists)\nregistry._containerIdMap = JSON.stringify(containerMap);\n\n// Pass through all containers unchanged (multi-item output)\nreturn containers.map(c => ({ json: c }));\n" }, "id": "code-update-registry", "name": "Update Container ID Registry", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 720, 400 ] }, { "parameters": { "jsCode": "// Build Start Mutation\nconst data = $('Route Action').item.json;\nconst unraidId = data.unraidId || data.containerId;\nreturn { json: { query: `mutation { docker { start(id: \"${unraidId}\") { id state } } }` } };" }, "id": "code-build-start-mutation", "name": "Build Start Mutation", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1080, 200 ] }, { "parameters": { "jsCode": "// GraphQL Error Handler - Standardized error checking and HTTP status mapping\n// Input: $input.item.json = raw response from HTTP Request node\n// Output: { success, statusCode, alreadyInState, message, data }\n\nconst response = $input.item.json;\n\n// Check GraphQL errors array\nif (response.errors && response.errors.length > 0) {\n const error = response.errors[0];\n const code = error.extensions?.code;\n const message = error.message;\n \n // Map error codes to HTTP equivalents\n if (code === 'ALREADY_IN_STATE') {\n // Maps to Docker API HTTP 304 pattern (used in n8n-actions.json)\n return {\n json: {\n success: true,\n statusCode: 304,\n alreadyInState: true,\n message: 'Container already in desired state'\n }\n };\n }\n \n // Error codes that should throw\n if (code === 'NOT_FOUND') {\n return {\n json: {\n success: false,\n statusCode: 404,\n message: `Container not found: ${message}`\n }\n };\n }\n \n if (code === 'FORBIDDEN' || code === 'UNAUTHORIZED') {\n return {\n json: {\n success: false,\n statusCode: 403,\n message: `Permission denied: ${message}`\n }\n };\n }\n \n // Any other GraphQL error\n return {\n json: {\n success: false,\n statusCode: 500,\n message: `Unraid API error: ${message}`\n }\n };\n}\n\n// Check HTTP-level errors\nif (response.statusCode >= 400) {\n return {\n json: {\n success: false,\n statusCode: response.statusCode,\n message: `HTTP ${response.statusCode}: ${response.statusMessage}`\n }\n };\n}\n\n// Check missing data field\nif (!response.data) {\n return {\n json: {\n success: false,\n statusCode: 500,\n message: 'GraphQL response missing data field'\n }\n };\n}\n\n// Success\nreturn {\n json: {\n success: true,\n statusCode: 200,\n alreadyInState: false,\n data: response.data\n }\n};\n" }, "id": "code-start-error-handler", "name": "Start Error Handler", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1280, 200 ] }, { "parameters": { "jsCode": "// Build Stop Mutation\nconst data = $('Route Action').item.json;\nconst unraidId = data.unraidId || data.containerId;\nreturn { json: { query: `mutation { docker { stop(id: \"${unraidId}\") { id state } } }` } };" }, "id": "code-build-stop-mutation", "name": "Build Stop Mutation", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1080, 300 ] }, { "parameters": { "jsCode": "// GraphQL Error Handler - Standardized error checking and HTTP status mapping\n// Input: $input.item.json = raw response from HTTP Request node\n// Output: { success, statusCode, alreadyInState, message, data }\n\nconst response = $input.item.json;\n\n// Check GraphQL errors array\nif (response.errors && response.errors.length > 0) {\n const error = response.errors[0];\n const code = error.extensions?.code;\n const message = error.message;\n \n // Map error codes to HTTP equivalents\n if (code === 'ALREADY_IN_STATE') {\n // Maps to Docker API HTTP 304 pattern (used in n8n-actions.json)\n return {\n json: {\n success: true,\n statusCode: 304,\n alreadyInState: true,\n message: 'Container already in desired state'\n }\n };\n }\n \n // Error codes that should throw\n if (code === 'NOT_FOUND') {\n return {\n json: {\n success: false,\n statusCode: 404,\n message: `Container not found: ${message}`\n }\n };\n }\n \n if (code === 'FORBIDDEN' || code === 'UNAUTHORIZED') {\n return {\n json: {\n success: false,\n statusCode: 403,\n message: `Permission denied: ${message}`\n }\n };\n }\n \n // Any other GraphQL error\n return {\n json: {\n success: false,\n statusCode: 500,\n message: `Unraid API error: ${message}`\n }\n };\n}\n\n// Check HTTP-level errors\nif (response.statusCode >= 400) {\n return {\n json: {\n success: false,\n statusCode: response.statusCode,\n message: `HTTP ${response.statusCode}: ${response.statusMessage}`\n }\n };\n}\n\n// Check missing data field\nif (!response.data) {\n return {\n json: {\n success: false,\n statusCode: 500,\n message: 'GraphQL response missing data field'\n }\n };\n}\n\n// Success\nreturn {\n json: {\n success: true,\n statusCode: 200,\n alreadyInState: false,\n data: response.data\n }\n};\n" }, "id": "code-stop-error-handler", "name": "Stop Error Handler", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1280, 300 ] }, { "parameters": { "jsCode": "// Build Stop-for-Restart Mutation\nconst data = $('Route Action').item.json;\nconst unraidId = data.unraidId || data.containerId;\nreturn { json: { query: `mutation { docker { stop(id: \"${unraidId}\") { id state } } }`, unraidId } };" }, "id": "code-build-restart-stop-mutation", "name": "Build Stop-for-Restart Mutation", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1080, 400 ] }, { "parameters": { "jsCode": "// Handle Stop-for-Restart Result\n// Check response: if success OR statusCode 304 (already stopped) -> proceed to start\n// If error -> fail restart\n\nconst response = $input.item.json;\nconst prevData = $('Build Stop-for-Restart Mutation').item.json;\n\n// Check for errors\nif (response.errors && response.errors.length > 0) {\n const error = response.errors[0];\n const code = error.extensions?.code;\n \n // ALREADY_IN_STATE (304) is OK - container already stopped\n if (code === 'ALREADY_IN_STATE') {\n // Continue to start step\n return { json: { query: `mutation { docker { start(id: \"${prevData.unraidId}\") { id state } } }` } };\n }\n \n // Any other error - fail restart\n return {\n json: {\n error: true,\n statusCode: 500,\n message: `Failed to stop container for restart: ${error.message}`\n }\n };\n}\n\n// Check HTTP-level errors\nif (response.statusCode && response.statusCode >= 400) {\n return {\n json: {\n error: true,\n statusCode: response.statusCode,\n message: 'Failed to stop container for restart'\n }\n };\n}\n\n// Success - proceed to start\nreturn { json: { query: `mutation { docker { start(id: \"${prevData.unraidId}\") { id state } } }` } };\n" }, "id": "code-handle-stop-for-restart", "name": "Handle Stop-for-Restart Result", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1280, 400 ] }, { "parameters": { "method": "POST", "url": "={{ $env.UNRAID_HOST }}/graphql", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({query: $json.query}) }}", "options": { "timeout": 15000 }, "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth" }, "id": "http-start-after-stop", "name": "Start After Stop", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1480, 400 ], "onError": "continueRegularOutput", "credentials": { "httpHeaderAuth": { "id": "unraid-api-key-credential-id", "name": "Unraid API Key" } } }, { "parameters": { "jsCode": "// GraphQL Error Handler for Restart (after Start step)\n// Input: $input.item.json = raw response from Start After Stop\n// Output: { success, statusCode, alreadyInState, message, data }\n\nconst response = $input.item.json;\n\n// Check GraphQL errors array\nif (response.errors && response.errors.length > 0) {\n const error = response.errors[0];\n const code = error.extensions?.code;\n const message = error.message;\n \n // Map error codes to HTTP equivalents\n if (code === 'ALREADY_IN_STATE') {\n // Maps to Docker API HTTP 304 pattern (container already running)\n return {\n json: {\n success: true,\n statusCode: 304,\n alreadyInState: true,\n message: 'Container already in desired state'\n }\n };\n }\n \n // Error codes that should throw\n if (code === 'NOT_FOUND') {\n return {\n json: {\n success: false,\n statusCode: 404,\n message: `Container not found: ${message}`\n }\n };\n }\n \n if (code === 'FORBIDDEN' || code === 'UNAUTHORIZED') {\n return {\n json: {\n success: false,\n statusCode: 403,\n message: `Permission denied: ${message}`\n }\n };\n }\n \n // Any other GraphQL error\n return {\n json: {\n success: false,\n statusCode: 500,\n message: `Unraid API error: ${message}`\n }\n };\n}\n\n// Check HTTP-level errors\nif (response.statusCode >= 400) {\n return {\n json: {\n success: false,\n statusCode: response.statusCode,\n message: `HTTP ${response.statusCode}: ${response.statusMessage}`\n }\n };\n}\n\n// Check missing data field\nif (!response.data) {\n return {\n json: {\n success: false,\n statusCode: 500,\n message: 'GraphQL response missing data field'\n }\n };\n}\n\n// Success\nreturn {\n json: {\n success: true,\n statusCode: 200,\n alreadyInState: false,\n data: response.data\n }\n};\n" }, "id": "code-restart-error-handler", "name": "Restart Error Handler", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1680, 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": "Query All Containers", "type": "main", "index": 0 } ] ] }, "Resolve Container ID": { "main": [ [ { "node": "Route Action", "type": "main", "index": 0 } ] ] }, "Route Action": { "main": [ [ { "node": "Build Start Mutation", "type": "main", "index": 0 } ], [ { "node": "Build Stop Mutation", "type": "main", "index": 0 } ], [ { "node": "Build Stop-for-Restart Mutation", "type": "main", "index": 0 } ] ] }, "Start Container": { "main": [ [ { "node": "Start Error Handler", "type": "main", "index": 0 } ] ] }, "Stop Container": { "main": [ [ { "node": "Stop Error Handler", "type": "main", "index": 0 } ] ] }, "Query All Containers": { "main": [ [ { "node": "GraphQL Response Normalizer", "type": "main", "index": 0 } ] ] }, "GraphQL Response Normalizer": { "main": [ [ { "node": "Update Container ID Registry", "type": "main", "index": 0 } ] ] }, "Update Container ID Registry": { "main": [ [ { "node": "Resolve Container ID", "type": "main", "index": 0 } ] ] }, "Build Start Mutation": { "main": [ [ { "node": "Start Container", "type": "main", "index": 0 } ] ] }, "Start Error Handler": { "main": [ [ { "node": "Format Start Result", "type": "main", "index": 0 } ] ] }, "Build Stop Mutation": { "main": [ [ { "node": "Stop Container", "type": "main", "index": 0 } ] ] }, "Stop Error Handler": { "main": [ [ { "node": "Format Stop Result", "type": "main", "index": 0 } ] ] }, "Build Stop-for-Restart Mutation": { "main": [ [ { "node": "Stop For Restart", "type": "main", "index": 0 } ] ] }, "Stop For Restart": { "main": [ [ { "node": "Handle Stop-for-Restart Result", "type": "main", "index": 0 } ] ] }, "Handle Stop-for-Restart Result": { "main": [ [ { "node": "Start After Stop", "type": "main", "index": 0 } ] ] }, "Start After Stop": { "main": [ [ { "node": "Restart Error Handler", "type": "main", "index": 0 } ] ] }, "Restart Error Handler": { "main": [ [ { "node": "Format Restart Result", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "callerPolicy": "any" } }