Unraid GraphQL API foundation — connectivity, authentication, and
container ID format verified for native Unraid API integration.
Phase 14: Unraid API Access (2 plans, 4 tasks)
- Established Unraid GraphQL API connectivity via myunraid.net cloud relay
- Dual credential storage (.env.unraid-api + n8n env vars)
- Container ID format: {server_hash}:{container_hash} (128-char SHA256 pair)
- Complete API contract documented in ARCHITECTURE.md
- "unraid" test command added to Telegram bot
Phases 15-16 dropped (superseded by v1.4 Unraid API Native).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
22 KiB
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)
Unraid GraphQL API
The Unraid GraphQL API is used to sync container update status back to Unraid after bot-initiated updates, resolving the "apply update" badge issue (see Known Limitations).
API Overview
- Endpoint:
{UNRAID_HOST}/graphql(POST) - Authentication:
x-api-keyheader with Unraid API key - Available in: Unraid 7.2+ (native) or 6.9-7.1 (Connect plugin)
- Purpose: Sync container update status back to Unraid after bot-initiated updates
Authentication
- Header:
x-api-key: {api_key} - Credential: n8n Header Auth credential named "Unraid API Key" - Header Name:
x-api-key- Header Value: Unraid API key
- Permission required:
DOCKER:UPDATE_ANY - Key creation:
unraid-api apikey --create --name "Docker Manager Bot" --permissions "DOCKER:UPDATE_ANY"
Alternative: Unraid WebGUI -> Settings -> Management Access -> API Keys -> Create key with DOCKER:UPDATE_ANY permission.
Network Access
Critical findings:
- Direct LAN IP (http://192.168.90.103) does NOT work — nginx redirects HTTP to HTTPS, stripping auth headers on redirect
- Direct HTTPS with standard port (https://192.168.90.103:8443) does NOT have /graphql endpoint
- Working approach: Use Unraid's myunraid.net cloud relay URL
Working configuration:
- URL format:
https://{ip-dashed}.{hash}.myunraid.net:8443/graphql- Example:
https://192-168-90-103.abc123def456.myunraid.net:8443/graphql
- Example:
- Environment variable: Set
UNRAID_HOSTon n8n container (without /graphql suffix) - SSL: Valid certs via myunraid.net — no SSL ignore needed
- Authentication: n8n Header Auth credential Why this works:
- myunraid.net cloud relay properly handles GraphQL endpoint routing
- Auth headers preserved through the cloud relay
- No nginx redirect issues
- GraphQL sandbox mode NOT required for API access
Note: The host.docker.internal and direct IP approaches documented in Phase 14 Plan 01 research did not work in production testing. The myunraid.net relay is the verified working solution for Unraid 7.2.
Container Query (Phase 14 — read-only)
query { docker { containers { id names state } } }
Expected response structure:
{
"data": {
"docker": {
"containers": [
{
"id": "1639d2f04f44841bc62fec38d18e1869a558d85071fa23e0a8bf64d374b317fa:8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925",
"names": ["/n8n"],
"state": "RUNNING"
}
]
}
}
}
Important field behaviors:
statevalues are UPPERCASE (RUNNING, notrunning)namesarray entries are prefixed with/(e.g.,/n8nnotn8n)isUpdateAvailablefield does NOT exist in Unraid 7.2 schema (research was incorrect)
Schema differences from research:
- Phase 14 research incorrectly documented
isUpdateAvailablefield - Actual schema introspection shows this field is not present in DockerContainer type
- Phase 15 will need to determine correct field for update status tracking
Container ID Format
- Type:
PrefixedIDscalar - Format:
{server_hash}:{container_hash}— two 64-character SHA256 hex strings joined by colon - Example:
1639d2f04f44841bc62fec38d18e1869a558d85071fa23e0a8bf64d374b317fa:8a9907a245766012741662a5840cefdec67af6b70e4c6f1629af7ef8f1ee2925 - Components:
- First part (server hash): Same for all containers on a given Unraid server
- Second part (container hash): Unique per container
- Note: Discovered during Phase 14 verification testing. Phase 15 mutations use this format.
n8n Container Configuration
Required environment variable:
UNRAID_HOST— Unraid myunraid.net URL (without /graphql suffix)- Example:
https://192-168-90-103.abc123def456.myunraid.net:8443 - Set via Unraid WebGUI -> Docker -> n8n container -> Edit -> Add Variable
- Example:
Required n8n credential:
- Type: Header Auth
- Name: "Unraid API Key"
- Header Name:
x-api-key - Header Value: Unraid API key (from
unraid-api apikey --create)
HTTP Request node configuration:
- Authentication:
genericCredentialTypewithhttpHeaderAuth - Credential: "Unraid API Key"- URL:
{{ $env.UNRAID_HOST }}/graphql
Update Mutation (Phase 15 — planned, not yet implemented)
mutation { docker { updateContainer(id: "<PrefixedID>") { id } } }
Status: Planned for Phase 15 — not yet implemented per phase boundary. Note: Update tracking field needs discovery via schema introspection (isUpdateAvailable does not exist).
Error Patterns
- HTTP errors: Connection refused (network), 401 (bad API key), 404 (API not available)
- GraphQL errors:
response.errors[]array with message field - Error handling: Same pattern as Docker API errors (structured return, descriptive messages)
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
- Telegram Trigger receives message
- Auth IF node checks user ID
- Correlation ID generator creates a unique request trace ID (
timestamp-random) - Keyword Router (Switch node) matches command keyword
- Parse Command (Code node) extracts parameters
- Matching sub-workflow resolves container name to Docker ID
- Domain sub-workflow executes the operation
- Result routed to Telegram response node
Callback (Inline Keyboard)
- Telegram Trigger receives callback query
- Auth IF node checks user ID
- Correlation ID generator creates a unique request trace ID
- Parse Callback Data (Code node, 441 lines) decodes callback string
- Route Callback (Switch node) dispatches by prefix
- Domain sub-workflow executes the operation
- 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.correlationIdin 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:
{
"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
- Open the n8n execution list for the main workflow
- Find the execution by timestamp or Telegram message content
- Check the Prepare Input node output for the
correlationId - Search sub-workflow executions for the same
correlationId - 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
- Import the modified sub-workflow JSON into n8n
- If main workflow changed, re-import n8n-workflow.json
- 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:
"workflowId": { "__rl": true, "mode": "list", "value": "<id>" }
Testing Checklist
After deployment, verify:
status— Shows container list with pagination- Tap container — Shows detail with action buttons
stop <name>— Confirmation dialog, confirm executes stopupdate <name>— Confirmation dialog, confirm pulls image + recreatesrestart <name>— Immediate restartlogs <name>— Shows last 50 linesstop 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.