feat(10.1-04): create confirmation dialogs sub-workflow
- New n8n-confirmation.json with 16 nodes - Handle show_stop/show_update dialog generation - Handle confirm_stop with expiry checking - Handle confirm_update (returns action for main workflow) - Handle cancel and expired actions - Calls n8n-actions.json for confirmed stop execution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,603 @@
|
||||
{
|
||||
"name": "Confirmation Dialogs",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"inputSource": "passthrough",
|
||||
"schema": {
|
||||
"schemaType": "fromFields",
|
||||
"inputFieldName": "",
|
||||
"fields": [
|
||||
{
|
||||
"fieldName": "chatId",
|
||||
"fieldType": "number"
|
||||
},
|
||||
{
|
||||
"fieldName": "messageId",
|
||||
"fieldType": "number"
|
||||
},
|
||||
{
|
||||
"fieldName": "action",
|
||||
"fieldType": "string"
|
||||
},
|
||||
{
|
||||
"fieldName": "containerId",
|
||||
"fieldType": "string"
|
||||
},
|
||||
{
|
||||
"fieldName": "containerName",
|
||||
"fieldType": "string"
|
||||
},
|
||||
{
|
||||
"fieldName": "confirmAction",
|
||||
"fieldType": "string"
|
||||
},
|
||||
{
|
||||
"fieldName": "confirmationToken",
|
||||
"fieldType": "string"
|
||||
},
|
||||
{
|
||||
"fieldName": "expired",
|
||||
"fieldType": "boolean"
|
||||
},
|
||||
{
|
||||
"fieldName": "responseMode",
|
||||
"fieldType": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "trigger-confirm-workflow",
|
||||
"name": "When executed by another workflow",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
240,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"values": [
|
||||
{
|
||||
"id": "show-stop",
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "is-show-stop",
|
||||
"leftValue": "={{ $json.action }}",
|
||||
"rightValue": "show_stop",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "show_stop"
|
||||
},
|
||||
{
|
||||
"id": "show-update",
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "is-show-update",
|
||||
"leftValue": "={{ $json.action }}",
|
||||
"rightValue": "show_update",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "show_update"
|
||||
},
|
||||
{
|
||||
"id": "confirm-stop",
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "is-confirm",
|
||||
"leftValue": "={{ $json.action }}",
|
||||
"rightValue": "confirm",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "is-stop-action",
|
||||
"leftValue": "={{ $json.confirmAction }}",
|
||||
"rightValue": "stop",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "confirm_stop"
|
||||
},
|
||||
{
|
||||
"id": "confirm-update",
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "is-confirm",
|
||||
"leftValue": "={{ $json.action }}",
|
||||
"rightValue": "confirm",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "is-update-action",
|
||||
"leftValue": "={{ $json.confirmAction }}",
|
||||
"rightValue": "update",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "confirm_update"
|
||||
},
|
||||
{
|
||||
"id": "cancel",
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "is-cancel",
|
||||
"leftValue": "={{ $json.action }}",
|
||||
"rightValue": "cancel",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "cancel"
|
||||
},
|
||||
{
|
||||
"id": "expired",
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "is-expired",
|
||||
"leftValue": "={{ $json.expired }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "expired"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"fallbackOutput": "none"
|
||||
}
|
||||
},
|
||||
"id": "switch-confirm-action",
|
||||
"name": "Route Confirmation Action",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
460,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Build Stop Confirmation dialog with timestamp for 30-second timeout\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Stop', callback_data: `confirm:stop:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F <b>Stop ${containerName}?</b>\\n\\nThis will stop the container immediately.\\n\\n<i>Confirmation expires in 30 seconds.</i>`;\n\nreturn {\n json: {\n action: 'show_stop',\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};"
|
||||
},
|
||||
"id": "code-build-stop-confirm",
|
||||
"name": "Build Stop Confirmation",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
700,
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Build Update Confirmation dialog with timestamp for 30-second timeout\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Update', callback_data: `confirm:update:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F <b>Update ${containerName}?</b>\\n\\nThis will pull the latest image and recreate the container.\\n\\n<i>Confirmation expires in 30 seconds.</i>`;\n\nreturn {\n json: {\n action: 'show_update',\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};"
|
||||
},
|
||||
"id": "code-build-update-confirm",
|
||||
"name": "Build Update Confirmation",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
700,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Confirmation expired - return expired response\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// Build keyboard for expired message\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]\n ]\n};\n\nreturn {\n json: {\n action: 'expired',\n chatId,\n messageId,\n text: `\\u23F0 Confirmation for <b>${containerName}</b> has expired.\\n\\nPlease try again.`,\n reply_markup: keyboard\n }\n};"
|
||||
},
|
||||
"id": "code-handle-expired",
|
||||
"name": "Handle Expired",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
700,
|
||||
600
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Cancel confirmation - return to container submenu\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nreturn {\n json: {\n action: 'cancel',\n containerName,\n chatId,\n messageId\n }\n};"
|
||||
},
|
||||
"id": "code-handle-cancel",
|
||||
"name": "Handle Cancel",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
700,
|
||||
500
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Check if expired BEFORE executing confirm action\nconst data = $('When executed by another workflow').item.json;\nconst expired = data.expired === true;\n\nif (expired) {\n // Return expired action - main workflow will handle display\n return {\n json: {\n ...data,\n action: 'expired',\n expired: true\n }\n };\n}\n\n// Not expired, proceed with stop confirmation\nreturn {\n json: {\n ...data,\n action: 'confirm_stop',\n expired: false\n }\n};"
|
||||
},
|
||||
"id": "code-check-stop-expired",
|
||||
"name": "Check Stop Expired",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
700,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "is-expired",
|
||||
"leftValue": "={{ $json.expired }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
"id": "if-stop-expired",
|
||||
"name": "Is Stop Expired?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
920,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Prepare input for container actions sub-workflow (stop action)\nconst data = $('When executed by another workflow').item.json;\n\nreturn {\n json: {\n containerId: data.containerId || '',\n containerName: data.containerName,\n action: 'stop',\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: data.responseMode || 'inline'\n }\n};"
|
||||
},
|
||||
"id": "code-prepare-stop-action",
|
||||
"name": "Prepare Stop Action",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1140,
|
||||
350
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"source": "database",
|
||||
"workflowId": {
|
||||
"__rl": true,
|
||||
"mode": "list",
|
||||
"value": "fYSZS5PkH0VSEaT5"
|
||||
},
|
||||
"mode": "once",
|
||||
"options": {
|
||||
"waitForSubWorkflow": true
|
||||
}
|
||||
},
|
||||
"id": "execute-actions-stop",
|
||||
"name": "Execute Stop Action",
|
||||
"type": "n8n-nodes-base.executeWorkflow",
|
||||
"typeVersion": 1.2,
|
||||
"position": [
|
||||
1360,
|
||||
350
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Format stop result with keyboard\nconst result = $input.item.json;\nconst success = result.success;\nconst message = result.message;\nconst containerName = result.containerName;\nconst chatId = result.chatId;\nconst messageId = result.messageId;\n\n// Build keyboard based on result\nlet keyboard;\nif (success) {\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n // Error: retry and back buttons\n const timestamp = Math.floor(Date.now() / 1000);\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `confirm:stop:${containerName}:${timestamp}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n action: 'confirm_stop_result',\n success,\n chatId,\n messageId,\n text: message,\n reply_markup: keyboard\n }\n};"
|
||||
},
|
||||
"id": "code-format-stop-result",
|
||||
"name": "Format Stop Result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1580,
|
||||
350
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Build expired response for stop confirmation\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]\n ]\n};\n\nreturn {\n json: {\n action: 'expired',\n chatId,\n messageId,\n text: `\\u23F0 Confirmation for <b>${containerName}</b> has expired.\\n\\nPlease try again.`,\n reply_markup: keyboard\n }\n};"
|
||||
},
|
||||
"id": "code-stop-expired-response",
|
||||
"name": "Stop Expired Response",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1140,
|
||||
250
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Check if expired BEFORE executing update confirm action\nconst data = $('When executed by another workflow').item.json;\nconst expired = data.expired === true;\n\nif (expired) {\n // Return expired action - main workflow will handle display\n return {\n json: {\n ...data,\n action: 'expired',\n expired: true\n }\n };\n}\n\n// Not expired, proceed with update confirmation\nreturn {\n json: {\n ...data,\n action: 'confirm_update',\n expired: false\n }\n};"
|
||||
},
|
||||
"id": "code-check-update-expired",
|
||||
"name": "Check Update Expired",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
700,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "is-expired",
|
||||
"leftValue": "={{ $json.expired }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
"id": "if-update-expired",
|
||||
"name": "Is Update Expired?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
920,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Build expired response for update confirmation\nconst data = $('When executed by another workflow').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]\n ]\n};\n\nreturn {\n json: {\n action: 'expired',\n chatId,\n messageId,\n text: `\\u23F0 Confirmation for <b>${containerName}</b> has expired.\\n\\nPlease try again.`,\n reply_markup: keyboard\n }\n};"
|
||||
},
|
||||
"id": "code-update-expired-response",
|
||||
"name": "Update Expired Response",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1140,
|
||||
350
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Return confirm_update action for main workflow to handle\n// Main workflow will call the update sub-workflow\nconst data = $('When executed by another workflow').item.json;\n\nreturn {\n json: {\n action: 'confirm_update',\n containerId: data.containerId || '',\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId,\n responseMode: data.responseMode || 'inline'\n }\n};"
|
||||
},
|
||||
"id": "code-return-update-action",
|
||||
"name": "Return Update Action",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1140,
|
||||
450
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When executed by another workflow": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Route Confirmation Action",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Route Confirmation Action": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Build Stop Confirmation",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Build Update Confirmation",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Check Stop Expired",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Check Update Expired",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Handle Cancel",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Handle Expired",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check Stop Expired": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is Stop Expired?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is Stop Expired?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Stop Expired Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Prepare Stop Action",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Prepare Stop Action": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Execute Stop Action",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Execute Stop Action": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Format Stop Result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check Update Expired": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is Update Expired?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is Update Expired?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update Expired Response",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Return Update Action",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"callerPolicy": "any"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user