# Architecture Technical reference for the Unraid Docker Manager workflow system. ## System Overview The bot is an n8n workflow that receives Telegram messages, routes them through authentication and keyword matching, dispatches to domain-specific sub-workflows, and sends responses back to the user. ``` Telegram | v n8n-workflow.json (166 nodes) |-- Auth check (Telegram user ID) |-- Correlation ID generation |-- Text path: Keyword Router -> Parse Command -> Match Container -> Execute |-- Callback path: Parse Callback Data -> Route -> Execute | |-- n8n-update.json (34 nodes) Container image pull + recreate |-- n8n-actions.json (11 nodes) Start / stop / restart |-- n8n-logs.json (9 nodes) Log retrieval + formatting |-- n8n-batch-ui.json (17 nodes) Batch selection keyboard UI |-- n8n-status.json (11 nodes) Container list + status display |-- n8n-confirmation.json (16 nodes) Confirmation dialogs |-- n8n-matching.json (23 nodes) Container name matching | v docker-socket-proxy (tecnativa/docker-socket-proxy) | v Docker Engine ``` **Total:** 287 nodes (166 main + 121 across 7 sub-workflows) ## Workflow Files | File | n8n ID | Purpose | Nodes | |------|--------|---------|-------| | n8n-workflow.json | `HmiXBlJefBRPMS0m4iNYc` | Main orchestrator | 166 | | n8n-update.json | `7AvTzLtKXM2hZTio92_mC` | Container update (pull, recreate, cleanup) | 34 | | n8n-actions.json | `fYSZS5PkH0VSEaT5` | Container start/stop/restart | 11 | | n8n-logs.json | `oE7aO2GhbksXDEIw` | Container log retrieval | 9 | | n8n-batch-ui.json | `ZJhnGzJT26UUmW45` | Batch selection keyboard | 17 | | n8n-status.json | `lqpg2CqesnKE2RJQ` | Container list and status | 11 | | n8n-confirmation.json | `fZ1hu8eiovkCk08G` | Confirmation dialogs | 16 | | n8n-matching.json | `kL4BoI8ITSP9Oxek` | Container name matching | 23 | ## Request Flow ### Text Commands 1. Telegram Trigger receives message 2. Auth IF node checks user ID 3. Correlation ID generator creates a unique request trace ID (`timestamp-random`) 4. Keyword Router (Switch node) matches command keyword 5. Parse Command (Code node) extracts parameters 6. Matching sub-workflow resolves container name to Docker ID 7. Domain sub-workflow executes the operation 8. Result routed to Telegram response node ### Callback (Inline Keyboard) 1. Telegram Trigger receives callback query 2. Auth IF node checks user ID 3. Correlation ID generator creates a unique request trace ID 4. Parse Callback Data (Code node, 441 lines) decodes callback string 5. Route Callback (Switch node) dispatches by prefix 6. Domain sub-workflow executes the operation 7. Result routed to Telegram response node (editMessageText) ## Observability ### Correlation IDs Every request gets a unique correlation ID generated at the entry point of the main workflow. This ID flows through all sub-workflow calls via Prepare Input nodes, enabling request tracing across workflow boundaries in the n8n execution log. **How it works:** - Two generator nodes in the main workflow: one for the text path, one for the callback path - Format: `timestamp-randomString` (no external dependencies) - All 19 Prepare Input nodes include `correlationId: $json.correlationId` in their output - Sub-workflows receive the ID as an input field and can reference it in their logs **Where to find it:** Open any execution in the n8n UI, inspect the output of a Prepare Input node — the `correlationId` field traces back to the original user request. **Limitations:** Correlation IDs are only visible in the n8n execution log. There is no persistent storage or user-facing output. n8n's workflow static data is execution-scoped (not workflow-scoped), so ring buffers and cross-execution logging are not possible on this platform. ### Structured Error Returns All 7 sub-workflows return structured error objects on failure: ```json { "success": false, "error": "Container not found: foo", "correlationId": "1707400000-abc123" } ``` This provides a consistent error shape for the main workflow to route and format error messages to the user. ### Debugging a Request 1. Open the n8n execution list for the main workflow 2. Find the execution by timestamp or Telegram message content 3. Check the Prepare Input node output for the `correlationId` 4. Search sub-workflow executions for the same `correlationId` 5. Trace the full request path: main workflow -> sub-workflow -> Docker API -> response ## Sub-workflow Contracts Each sub-workflow has a defined input/output contract. The main workflow communicates with sub-workflows through Prepare Input (Code) nodes that build the input object, and Route Result (Code) nodes that interpret the response. ### n8n-update.json (Container Update) **Input:** | Field | Type | Required | Description | |-------|------|----------|-------------| | containerId | string | yes* | Docker container ID (empty string to resolve by name) | | containerName | string | yes | Container display name | | chatId | number | yes | Telegram chat ID | | messageId | number | yes | Telegram message ID (0 for text mode) | | responseMode | string | yes | `"text"`, `"inline"`, or `"batch"` | | correlationId | string | no | Request trace ID | *containerId can be empty — the sub-workflow resolves by name via its Resolve Container ID node. **Output:** | Outcome | Fields | |---------|--------| | Updated | `success: true, updated: true, message, oldDigest, newDigest` | | No update needed | `success: true, updated: false, message` | | Error | `success: false, updated: false, message` | **Callers:** Execute Text Update, Execute Callback Update, Execute Batch Update --- ### n8n-actions.json (Container Actions) **Input:** | Field | Type | Required | Description | |-------|------|----------|-------------| | containerId | string | yes | Docker container ID | | containerName | string | yes | Container display name | | action | string | yes | `"start"`, `"stop"`, or `"restart"` | | chatId | number | yes | Telegram chat ID | | messageId | number | yes | Telegram message ID (0 for text mode) | | responseMode | string | yes | `"text"`, `"inline"`, or `"batch"` | | correlationId | string | no | Request trace ID | **Output:** `success, message, action, containerName, containerId, chatId, messageId, responseMode` HTTP status codes are checked before message content: 204 = success, 304 = already in state (treated as success), others = error. **Callers:** Execute Container Action, Execute Inline Action, Execute Batch Action Sub-workflow --- ### n8n-logs.json (Container Logs) **Input:** | Field | Type | Required | Description | |-------|------|----------|-------------| | containerName | string | yes* | Container name for lookup | | containerId | string | no | Docker container ID (optional) | | lineCount | number | no | Lines to retrieve (default: 50, max: 1000) | | chatId | number | yes | Telegram chat ID | | messageId | number | no | Telegram message ID (default: 0) | | responseMode | string | no | `"text"` or `"inline"` (default: "text") | | correlationId | string | no | Request trace ID | *Either containerId or containerName is required. **Output:** `success: true, message, containerName, lineCount` Errors (container not found, Docker error) throw exceptions handled by n8n's error system. **Callers:** Execute Text Logs, Execute Inline Logs --- ### n8n-batch-ui.json (Batch Selection UI) **Input:** | Field | Type | Required | Description | |-------|------|----------|-------------| | chatId | number | yes | Telegram chat ID | | messageId | number | yes | Telegram message ID | | callbackData | string | yes | Raw callback data string | | queryId | string | yes | Telegram callback query ID | | action | string | yes | `"mode"`, `"toggle"`, `"nav"`, `"exec"`, `"clear"`, `"cancel"` | | batchPage | number | no | Current page number (default: 0) | | selectedCsv | string | no | Comma-separated selected container names | | toggleName | string | no | Container name being toggled | | batchAction | string | no | Action for batch execution (stop/restart/update) | | correlationId | string | no | Request trace ID | **Output:** | action | Description | |--------|-------------| | `"keyboard"` | Rendered selection keyboard with checkmarks | | `"execute"` | User confirmed — includes containerNames and batchAction | | `"cancel"` | User cancelled batch selection | Batch selection uses bitmap encoding (base36 BigInt) to fit container selections within Telegram's 64-byte callback data limit. **Callers:** Execute Batch UI --- ### n8n-status.json (Container Status/List) **Input:** | Field | Type | Required | Description | |-------|------|----------|-------------| | chatId | number | yes | Telegram chat ID | | messageId | number | yes | Telegram message ID | | action | string | yes | `"list"`, `"status"`, `"paginate"` | | containerId | string | no | Docker container ID (for status lookup) | | containerName | string | no | Container name (for status lookup) | | page | number | no | Page number for pagination (default: 0) | | queryId | string | no | Telegram callback query ID | | searchTerm | string | no | Container name search term | | correlationId | string | no | Request trace ID | **Output:** | action | Description | |--------|-------------| | `"list"` | Container list with pagination keyboard | | `"status"` | Single container detail with action buttons | | `"paginate"` | Updated page of container list | The container list keyboard includes an "Update All :latest" button after the navigation row. **Callers:** Execute Container Status, Execute Select Status, Execute Paginate Status, Execute Batch Cancel Status --- ### n8n-confirmation.json (Confirmation Dialogs) **Input:** | Field | Type | Required | Description | |-------|------|----------|-------------| | chatId | number | yes | Telegram chat ID | | messageId | number | yes | Telegram message ID | | action | string | yes | `"show_stop"`, `"show_update"`, `"confirm"`, `"cancel"`, `"expired"` | | containerId | string | no | Docker container ID | | containerName | string | yes | Container display name | | confirmAction | string | no | `"stop"` or `"update"` (for confirm action) | | confirmationToken | string | no | Timestamp token for 30-second expiry check | | expired | boolean | no | Whether confirmation has expired | | responseMode | string | no | `"inline"` (default) | | correlationId | string | no | Request trace ID | This sub-workflow internally calls n8n-actions.json for confirmed stop actions. **Output:** | action | Description | |--------|-------------| | `"show_stop"` | Stop confirmation dialog rendered | | `"show_update"` | Update confirmation dialog rendered | | `"confirm_stop_result"` | Stop executed, result returned | | `"confirm_update"` | Update confirmed, containerId/name returned for update sub-workflow | | `"cancel"` | Confirmation cancelled | | `"expired"` | Confirmation token expired (30-second timeout) | **Callers:** Execute Confirmation (fed by 3 Prepare Input nodes: Prepare Confirm Input, Prepare Show Stop Input, Prepare Show Update Input) --- ### n8n-matching.json (Container Matching/Disambiguation) **Input:** | Field | Type | Required | Description | |-------|------|----------|-------------| | action | string | yes | `"match_action"`, `"match_update"`, or `"match_batch"` | | containerList | string | yes | Raw Docker API JSON output (container list) | | searchTerm | string | yes | Container name query to match | | selectedContainers | string | no | Comma-separated names for batch matching | | chatId | number | yes | Telegram chat ID | | messageId | number | yes | Telegram message ID | | correlationId | string | no | Request trace ID | Matching priority: exact match > single substring match > multiple matches (disambiguation) > no match (suggestion keyboard). **Output (action match):** | action | Description | |--------|-------------| | `"matched"` | Single container matched — includes containerId, containerName | | `"multiple"` | Multiple matches — includes matches array for disambiguation | | `"no_match"` | No match found | | `"suggestion"` | Suggestion keyboard with close matches | | `"error"` | Matching error | **Output (update match):** Same as action match with `_update` suffix on action values. **Output (batch match):** | action | Description | |--------|-------------| | `"batch_matched"` | All names resolved — includes matchedContainers array | | `"disambiguation"` | Some names ambiguous — disambiguation keyboard | | `"not_found"` | Some names not found | **Callers:** Execute Action Match, Execute Update Match, Execute Batch Match ## Execute Workflow Node Map 17 Execute Workflow nodes in the main workflow dispatch to 7 sub-workflows: | Target | Execute Nodes | |--------|---------------| | n8n-update.json | Execute Text Update, Execute Callback Update, Execute Batch Update | | n8n-actions.json | Execute Container Action, Execute Inline Action, Execute Batch Action Sub-workflow | | n8n-logs.json | Execute Text Logs, Execute Inline Logs | | n8n-batch-ui.json | Execute Batch UI | | n8n-status.json | Execute Container Status, Execute Select Status, Execute Paginate Status, Execute Batch Cancel Status | | n8n-confirmation.json | Execute Confirmation | | n8n-matching.json | Execute Action Match, Execute Update Match, Execute Batch Match | ## Main Workflow Internals ### Node Breakdown (166 nodes) | Category | Count | Purpose | |----------|-------|---------| | Code | 60 | Command parsing, input preparation, result routing, response building, batch orchestration | | HTTP Request | 40 | Docker API and Telegram API calls | | Telegram | 23 | User-facing response nodes | | Execute Workflow | 17 | Sub-workflow dispatch | | Switch | 13 | Keyword Router, Route Callback, result routing | | If | 8 | Auth checks, batch completion, expiry, status routing | | Execute Command | 6 | Docker CLI (container list, exec) | | Telegram Trigger | 1 | Entry point | ### Code Node Categories The 60 Code nodes break down into orchestration categories. All are infrastructure — none contain extractable domain logic: | Category | Count | Role | |----------|-------|------| | prepare-input | 27 | Build input objects for sub-workflow calls | | route-result | 12 | Interpret sub-workflow responses for routing | | build-response | 8 | Build Telegram messages and keyboards | | orchestration | 6 | Batch loop control and state management | | parse-command | 5 | Parse text commands into structured data | | domain-logic | 2 | Legacy candidates (net-negative extraction) | ### Callback Data Encoding Telegram limits callback data to 64 bytes. The system uses two encoding schemes: **Colon-delimited** for single operations: `s:containerId` (status), `stop:containerId` (action), `cfm:stop:containerId:token` (confirmation) **Bitmap encoding** for batch selection: `b:0:1a3:5` where the middle segment is a base36 BigInt representing selected container indices. Supports 50+ containers within the 64-byte limit. Legacy parsers (`batch:toggle:`, `batch:nav:`, `batch:exec:`) are retained for graceful migration of in-flight messages. ## Deployment ### Redeploying After Changes 1. Import the modified sub-workflow JSON into n8n 2. If main workflow changed, re-import n8n-workflow.json 3. Activate the workflow Workflow IDs are stable — n8n preserves them across re-imports of the same workflow. ### Execute Workflow Node Format All Execute Workflow nodes use typeVersion 1.2: ```json "workflowId": { "__rl": true, "mode": "list", "value": "" } ``` ### Testing Checklist After deployment, verify: - [ ] `status` — Shows container list with pagination - [ ] Tap container — Shows detail with action buttons - [ ] `stop ` — Confirmation dialog, confirm executes stop - [ ] `update ` — Confirmation dialog, confirm pulls image + recreates - [ ] `restart ` — Immediate restart - [ ] `logs ` — Shows last 50 lines - [ ] `stop plex sonarr` — Batch selection keyboard - [ ] Select multiple, execute — Batch processes all selected - [ ] `update all` — Lists :latest containers, confirm updates all - [ ] Update All button in keyboard — Same flow via inline keyboard ## Known Limitations ### Unraid Update Badges After the bot updates a container, Unraid's Docker tab may show "apply update" on the next check. The bot uses Docker API directly while Unraid tracks containers through its XML template system — it doesn't know the container was updated externally. **Resolution:** Click "Apply Update" in Unraid. It completes instantly since the image is already cached. **Why not automated:** Clearing the badge would require calling Unraid's internal web API (authentication, template parsing) for a cosmetic issue that takes one click. ### n8n Static Data n8n workflow static data (`$getWorkflowStaticData('global')`) is execution-scoped, not workflow-scoped. Data written in one execution is not available in the next. This prevents persistent cross-execution features like error ring buffers or debug command history. ### Orphan Nodes 3 legacy Code nodes remain in the main workflow (Build Action Command, Build Immediate Action Command, Prepare Cancel Return). They are unreachable dead code from pre-modularization inline action paths. They have no incoming connections and do not affect functionality.