{
"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"
}
}