# Phase 9: Batch Operations - Research
**Researched:** 2026-02-03
**Domain:** Batch container operations with sequential execution and progress reporting
**Confidence:** MEDIUM
## Summary
This phase implements batch container operations enabling users to update, start, stop, or restart multiple containers in a single command. The implementation must execute operations sequentially with individual error handling, provide clear progress feedback, and support both text commands ("update plex sonarr radarr") and inline keyboard multi-select patterns.
**Key constraint from CONTEXT.md:** "Update all" targets only containers with available updates (not all containers). Named batches run immediately without confirmation except for stop operations which confirm due to fuzzy matching risks. Continue executing remaining containers after individual failures.
**State management approach:** n8n Loop Over Items node provides sequential execution with per-item error handling. Batch state (selected containers, progress) encoded in callback_data or passed through workflow items.
**Primary recommendation:** Use Loop Over Items node with batch size 1 for strict sequential execution, implement error handling within loop using If Error node to continue on failures, and use editMessageText for single updating progress message showing current operation + summary.
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| n8n Loop Over Items | Built-in | Sequential batch execution | Native n8n node for strict one-at-a-time processing with sub-workflow completion before next item |
| n8n If Error node | Built-in | Individual item error handling | Enables continue-on-error pattern within loops without aborting entire batch |
| Telegram editMessageText | Bot API 7.0+ | Progress updates | Updates single message in-place; rate limit ~5 edits/minute per message |
| Fuse.js (optional) | 7.0+ | Fuzzy name matching | Lightweight fuzzy search library for container name disambiguation |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| n8n Code node | Built-in | Batch result aggregation | Consolidate success/failure counts and format summary message |
| n8n Set node | Built-in | Initialize batch state | Set up batch context (total count, current index) before loop |
| fast-fuzzy (alternative) | 1.12+ | High-performance fuzzy matching | If Fuse.js performance insufficient for large container lists (>100) |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Loop Over Items | HTTP Request batch processing | Batch sends parallel requests (not sequential); harder to track individual progress |
| editMessageText (single) | sendMessage (multiple) | Multiple messages clutter chat but avoid rate limits; user preference is single message |
| Fuzzy matching library | Levenshtein distance hand-rolled | Edge cases (abbreviations, prefixes) poorly handled; library provides battle-tested algorithms |
### Installation
Not applicable — using n8n built-in nodes and Telegram Bot API. If fuzzy matching library needed:
```bash
# In n8n Code node, use npm: functions for external libraries
# Or deploy as n8n community node if significant complexity
```
## Architecture Patterns
### Recommended Workflow Structure
```
Parse Batch Command
├─ Extract container names (space-separated)
├─ Match containers (with fuzzy matching + disambiguation)
└─ Build batch items array
↓
Batch Confirmation (if needed)
├─ "Update all" → Show count, require confirmation
├─ Named batch stop → Show disambiguation if multiple matches
└─ Other named batches → Execute immediately
↓
Initialize Batch State (Set node)
├─ Total count
├─ Success/failure counters
└─ Send initial progress message
↓
Loop Over Items (batch size 1)
├─ Edit message with current progress
├─ Execute Docker action (HTTP Request)
├─ If Error → Log failure, continue
└─ Update counters
↓
Format Batch Summary
├─ Emphasize failures (container name + reason)
├─ Show warnings separately (already stopped, no update)
└─ Success count at end
↓
Send Final Result (editMessageText)
```
### Pattern 1: Sequential Execution with Loop Over Items
**What:** Use Loop Over Items node with batch size 1 to process containers one at a time, waiting for each Docker operation to complete before proceeding to next.
**When to use:** All batch operations (update, start, stop, restart) to ensure deterministic execution order and accurate progress tracking.
**Example:**
```javascript
// Set node: Initialize Batch
return {
json: {
containers: ['plex', 'sonarr', 'radarr'],
totalCount: 3,
successCount: 0,
failureCount: 0,
failures: [],
progressMessageId: $json.progressMessage.message_id
}
};
```
**Loop Over Items configuration:**
- **Batch Size:** 1
- **Input:** Array of container objects `[{ name: 'plex', action: 'update' }, ...]`
- **On Error:** Continue using error output (route to error handler)
**Confidence:** HIGH (verified via n8n community consensus and official Loop Over Items documentation)
### Pattern 2: Individual Error Handling with Continue-on-Failure
**What:** Route error output from HTTP Request node to If Error node, which logs the failure and allows loop to continue to next container.
**Why:** User requirement: "One container failure doesn't abort remaining batch" (BAT-05).
**Structure:**
```
Loop Over Items
↓
[Edit Progress Message: "Updating plex..."]
↓
Execute Docker Action (HTTP Request)
├─ Success → Increment successCount
└─ Error → If Error node
↓
Log Failure (Code node)
- Extract error reason
- Add to failures array: { name: 'plex', reason: 'Image pull failed' }
- Increment failureCount
↓
Continue to next item
```
**Critical note:** n8n community reports issues with "Continue (using error output)" option in Loop Over Items where data doesn't pass correctly. Workaround: Use Try/Catch pattern with If node checking for error property.
**Confidence:** MEDIUM (pattern is sound; n8n implementation has known issues requiring workarounds)
### Pattern 3: Single Updating Progress Message
**What:** Send one progress message at batch start, then edit it after each container operation to show current status.
**Why:** User preference implied by "Summary emphasizes failures" — single message with final summary. Avoids chat clutter from multiple messages.
**Rate limit consideration:** Telegram allows ~5 edits/minute per message. For batches larger than 5 containers, edit only every Nth container or show final summary only.
**Example:**
```javascript
// Edit progress after each container
const current = $json.currentIndex + 1;
const total = $json.totalCount;
const containerName = $json.currentContainer.name;
const progressText = `🔄 Batch Update Progress\n\n` +
`Processing: ${containerName}\n` +
`Progress: ${current}/${total} containers\n\n` +
`✅ Success: ${$json.successCount}\n` +
`❌ Failed: ${$json.failureCount}`;
return {
json: {
chat_id: $json.chatId,
message_id: $json.progressMessageId,
text: progressText,
parse_mode: 'HTML'
}
};
```
**Alternative approach:** Skip intermediate edits for small batches (<5 containers), show final summary only. Reduces API calls and avoids rate limits.
**Confidence:** MEDIUM (pattern is sound; rate limit requires careful handling)
### Pattern 4: Fuzzy Matching with Exact Match Priority
**What:** When parsing container names from command, prioritize exact matches before fuzzy matches. If fuzzy match required, show disambiguation when multiple matches found.
**Why:** User requirement: "if 'plex' matches both 'plex' and 'jellyplex', the user should be able to specify they want only 'plex'" (from CONTEXT specifics).
**Algorithm:**
1. **Normalize input:** Lowercase, trim whitespace, remove common prefixes (linuxserver-, ...)
2. **Exact match first:** Check if normalized input exactly matches any container name
3. **Fuzzy match fallback:** If no exact match, use fuzzy matching with threshold (e.g., 0.8)
4. **Disambiguation:** If multiple fuzzy matches above threshold, show list for user selection
**Example:**
```javascript
// Code node: Match Container Names
const input = 'plex';
const containers = ['plex', 'jellyplex', 'sonarr'];
// Step 1: Exact match
const exactMatch = containers.find(c => c.toLowerCase() === input.toLowerCase());
if (exactMatch) {
return { json: { match: exactMatch, type: 'exact' } };
}
// Step 2: Fuzzy match (using simple string includes for MVP)
const fuzzyMatches = containers.filter(c =>
c.toLowerCase().includes(input.toLowerCase())
);
if (fuzzyMatches.length === 1) {
return { json: { match: fuzzyMatches[0], type: 'fuzzy' } };
}
if (fuzzyMatches.length > 1) {
return { json: { matches: fuzzyMatches, type: 'disambiguation_needed' } };
}
return { json: { match: null, type: 'no_match' } };
```
**For production:** Consider Fuse.js for more sophisticated fuzzy matching with configurable thresholds and token-based matching.
**Confidence:** HIGH (requirement is clear; implementation straightforward)
### Pattern 5: Inline Keyboard Multi-Select (Claude's Discretion)
**What:** Provide batch selection UI via inline keyboard buttons where users toggle checkmarks to select multiple containers.
**Implementation approach:** Since n8n workflows are stateless, encode selected containers in callback_data with compact format.
**Toggle pattern:**
1. User clicks container button → Add checkmark emoji, update callback_data to include container in selection
2. User clicks again → Remove checkmark, update callback_data to remove container
3. "Execute" button appears when at least one container selected
**Callback data format (optimized for 64-byte limit):**
```
batch:update:plex,sonarr,radarr
batch:update:✓plex,sonarr,radarr // ✓ indicates currently selected for toggle
```
**Limitation:** With 64-byte limit, approximately 8-10 container names can be selected (assuming average 6-7 chars per name + commas). For larger selections, fall back to "update all" command.
**Example:**
```javascript
// Code node: Build Multi-Select Keyboard
const containers = $json.containers;
const selected = $json.selectedContainers || []; // from callback_data parsing
const keyboard = [];
containers.forEach(container => {
const isSelected = selected.includes(container.name);
const checkmark = isSelected ? '✅ ' : '';
const newSelected = isSelected
? selected.filter(s => s !== container.name)
: [...selected, container.name];
keyboard.push([{
text: `${checkmark}${container.name}`,
callback_data: `batch:toggle:${newSelected.join(',')}`
}]);
});
// Execute button (only if selections exist)
if (selected.length > 0) {
keyboard.push([{
text: `⬆️ Update ${selected.length} containers`,
callback_data: `batch:execute:update:${selected.join(',')}`
}]);
}
return {
json: {
text: 'Select containers to update:',
reply_markup: { inline_keyboard: keyboard }
}
};
```
**Note on leaked Telegram features:** Telegram v11.0.0 TestFlight (Nov 2025) contains native ToggleButton type that keeps state client-side. Not yet in stable API; monitor for Bot API 7.1+ announcement. If released, eliminates need for callback_data state encoding.
**Confidence:** MEDIUM (pattern is established; 64-byte limit constrains implementation; native toggle button may arrive soon)
### Pattern 6: Failure-Emphasizing Summary
**What:** Format final batch result to emphasize failures with actionable error information, distinguish warnings from errors, and minimize success noise.
**User requirement:** "Summary emphasizes failures over successes — user cares about what broke and why"
**Structure:**
```javascript
// Code node: Format Batch Summary
const failures = $json.failures; // Array of { name, reason, type: 'error'|'warning' }
const successCount = $json.successCount;
const totalCount = $json.totalCount;
let summaryText = `Batch Update Complete\n\n`;
// Errors first (critical failures)
const errors = failures.filter(f => f.type === 'error');
if (errors.length > 0) {
summaryText += `❌ Failed (${errors.length}):\n`;
errors.forEach(err => {
summaryText += ` • ${err.name}: ${err.reason}\n`;
});
summaryText += '\n';
}
// Warnings second (non-critical issues)
const warnings = failures.filter(f => f.type === 'warning');
if (warnings.length > 0) {
summaryText += `⚠️ Warnings (${warnings.length}):\n`;
warnings.forEach(warn => {
summaryText += ` • ${warn.name}: ${warn.reason}\n`;
});
summaryText += '\n';
}
// Success summary (count only, not individual names)
summaryText += `✅ Successfully updated: ${successCount}/${totalCount}`;
return { json: { text: summaryText } };
```
**Error vs Warning classification:**
- **Error:** Image pull failed, container start timeout, network error, permission denied
- **Warning:** Already stopped (for stop command), no update available (for update command), already running (for start command)
**Confidence:** HIGH (requirement is explicit; implementation straightforward)
### Anti-Patterns to Avoid
**1. Parallel Execution of Batch Operations**
- **Why bad:** Cannot track individual progress accurately; failure handling becomes complex; Docker API may throttle concurrent requests
- **Instead:** Use Loop Over Items with batch size 1 for strict sequential execution
**2. Aborting Entire Batch on First Failure**
- **Why bad:** User requirement explicitly states continue on failures (BAT-05)
- **Instead:** Use If Error node to log failure and continue loop
**3. Editing Progress Message on Every Container**
- **Why bad:** Telegram rate limits ~5 edits/minute per message; large batches trigger 429 errors
- **Instead:** Edit only on milestones (every 5 containers) or show final summary only
**4. Hand-Rolling Fuzzy String Matching**
- **Why bad:** Edge cases (transpositions, prefix matching, token ordering) poorly handled; high bug risk
- **Instead:** Use established library (Fuse.js, fast-fuzzy) or simple substring matching for MVP
**5. Storing Batch State in External Database**
- **Why bad:** Adds complexity, latency, and failure points; n8n workflows can pass state through items
- **Instead:** Pass batch state through Loop Over Items as item properties or use Set node to maintain context
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Sequential execution with error handling | Custom retry queue with state management | Loop Over Items + If Error node | n8n built-in pattern handles sub-workflow completion and error routing |
| Fuzzy string matching | Custom Levenshtein distance implementation | Fuse.js or simple substring matching | Libraries handle edge cases (transpositions, token order, scoring); substring matching sufficient for MVP |
| Progress message rate limiting | Custom throttle with timestamp tracking | Edit on milestones (every 5th item) or final summary only | Telegram enforces server-side limits; respect 5 edits/minute ceiling |
| Multi-select state management | Redis/database for selection tracking | Encode in callback_data (up to 64 bytes) | Stateless design; external storage adds complexity and latency |
| Batch confirmation keyboards | Dynamic button generation with complex layouts | Existing Build Batch Keyboard pattern (already in workflow) | Workflow already has batch keyboard infrastructure from Phase 8 |
**Key insight:** n8n's Loop Over Items node is purpose-built for sequential batch processing with error handling. Using it eliminates need for custom queue management, state tracking, and retry logic.
## Common Pitfalls
### Pitfall 1: Loop Over Items Error Handling Data Loss
**What goes wrong:** When using "Continue (using error output)" option in Loop Over Items, data doesn't pass to subsequent nodes as expected, causing workflow to lose context after errors.
**Why it happens:** Known n8n issue reported in community where error output doesn't maintain item structure correctly when looping.
**How to avoid:**
- Use Try/Catch pattern with If node checking for error property instead of relying on error output
- Alternatively, use HTTP Request node's "Continue On Fail" option and check response status code in subsequent Code node
- Pass batch state through all paths (success and error) to maintain context
**Example workaround:**
```javascript
// Code node after HTTP Request (Continue On Fail enabled)
const response = $input.first().json;
const containerName = $json.containerName;
if (!response || response.statusCode >= 400) {
// Treat as error but maintain data flow
return {
json: {
...$$json, // Preserve original item data
error: true,
reason: response?.message || 'Unknown error'
}
};
}
return {
json: {
...$$json,
error: false,
result: response
}
};
```
**Warning signs:** Batch executes first item successfully but subsequent items don't process; workflow ends prematurely after first error
**Confidence:** HIGH (documented community issue with established workarounds)
### Pitfall 2: Telegram editMessageText Rate Limiting (429 Errors)
**What goes wrong:** Batch operations on many containers (>10) edit progress message too frequently, triggering 429 "Too Many Requests" errors and causing updates to fail.
**Why it happens:** Telegram enforces ~5 edits per minute per message (undocumented but empirically verified). Batch operations editing on every container exceed this limit.
**How to avoid:**
- **For small batches (<5 containers):** Edit after each container (within rate limit)
- **For medium batches (5-15 containers):** Edit every 3-5 containers + final summary
- **For large batches (>15 containers):** Show initial "Processing..." message, then only final summary
- **Always respect retry_after header:** If 429 received, wait specified seconds before retrying
**Example:**
```javascript
// Code node: Decide Whether to Update Progress
const currentIndex = $json.currentIndex;
const totalCount = $json.totalCount;
const updateInterval = totalCount <= 5 ? 1 : 5; // Edit every Nth container
const shouldUpdate = (currentIndex + 1) % updateInterval === 0 || currentIndex === totalCount - 1;
return {
json: {
...$$json,
shouldUpdateProgress: shouldUpdate
}
};
```
**Warning signs:** 429 errors in workflow execution logs; progress messages not updating; "message not modified" errors
**Confidence:** HIGH (Telegram API rate limits documented; edit-specific limit empirically verified)
### Pitfall 3: callback_data Size Exceeded in Multi-Select
**What goes wrong:** Multi-select keyboard with many containers selected causes "400 BUTTON_DATA_INVALID" error when callback_data exceeds 64-byte limit.
**Why it happens:** UTF-8 encoding; container names average 6-10 characters. Format `batch:update:plex,sonarr,radarr,prowlarr,jellyfin,overseerr,tautulli` = 72 bytes (exceeds limit).
**How to avoid:**
- Limit multi-select to 8-10 containers (depending on name lengths)
- Use abbreviated names in callback_data: `batch:upd:p,s,r,pr,j,o,t`
- Show "Too many selected, use 'update all' command" message if limit approached
- Alternative: Use multi-step selection (select up to 8, then add more in next screen)
**Example:**
```javascript
// Code node: Validate callback_data Size
const selected = $json.selectedContainers;
const callbackData = `batch:update:${selected.join(',')}`;
const byteLength = Buffer.byteLength(callbackData, 'utf8');
if (byteLength > 60) { // Leave 4-byte safety margin
return {
json: {
error: true,
message: '⚠️ Too many containers selected. Use "update all" command for large batches.'
}
};
}
return {
json: {
error: false,
callback_data: callbackData
}
};
```
**Warning signs:** Inline keyboard buttons not appearing; 400 errors when sending/editing messages with keyboards
**Confidence:** HIGH (Telegram API 7.0 specification; limit is enforced)
### Pitfall 4: Ambiguous Fuzzy Match Without Disambiguation
**What goes wrong:** User types "update plex" intending to update only "plex" container, but fuzzy matching also includes "jellyplex" and both get updated without confirmation.
**Why it happens:** Fuzzy matching algorithm uses substring/similarity without exact match priority or disambiguation step.
**How to avoid:**
- **Always prioritize exact matches** before fuzzy matching
- **Show disambiguation UI** when multiple fuzzy matches found (above threshold)
- **For destructive operations (stop, restart):** Confirm with list of matched containers before executing
- **For safe operations (logs):** Use best match but inform user: "Showing logs for plex (also matched: jellyplex)"
**Example:**
```javascript
// Code node: Fuzzy Match with Disambiguation
const input = 'plex';
const containers = ['plex', 'jellyplex', 'sonarr'];
// Exact match first
const exactMatch = containers.find(c => c.toLowerCase() === input.toLowerCase());
if (exactMatch) {
return { json: { match: exactMatch, needsConfirmation: false } };
}
// Fuzzy match
const fuzzyMatches = containers.filter(c => c.toLowerCase().includes(input.toLowerCase()));
if (fuzzyMatches.length === 0) {
return { json: { match: null, error: 'No matching containers found' } };
}
if (fuzzyMatches.length === 1) {
return { json: { match: fuzzyMatches[0], needsConfirmation: false } };
}
// Multiple matches - need disambiguation
return {
json: {
matches: fuzzyMatches,
needsDisambiguation: true,
question: `Did you mean:\n${fuzzyMatches.map((m, i) => `${i + 1}. ${m}`).join('\n')}`
}
};
```
**Warning signs:** Users report wrong containers being affected by commands; complaints about "unpredictable" matching behavior
**Confidence:** HIGH (user requirement is explicit; implementation pattern is established)
### Pitfall 5: Warnings Cluttering Failure Summary
**What goes wrong:** Final summary shows long list of "warnings" for non-critical issues (e.g., "sonarr: already stopped"), making it hard to identify actual failures.
**Why it happens:** No distinction between errors (require attention) and warnings (informational).
**How to avoid:**
- **Classify failures as error or warning** based on actionability
- **Show errors prominently at top** of summary
- **Claude's discretion:** Hide warnings from summary (only show errors + success count) OR show warnings in collapsed section
- **User focus:** "What broke and why" (errors) not "What was skipped" (warnings)
**Example:**
```javascript
// Code node: Classify Failure Type
const httpResponse = $json.response;
const containerName = $json.containerName;
const action = $json.action;
// Determine if error or warning
let failureType = 'error';
let reason = httpResponse.message || 'Unknown error';
if (httpResponse.statusCode === 304) {
failureType = 'warning';
reason = action === 'stop' ? 'Already stopped' : 'No update available';
} else if (httpResponse.statusCode === 409) {
failureType = 'warning';
reason = 'Already in requested state';
}
return {
json: {
name: containerName,
reason: reason,
type: failureType
}
};
```
**Recommendation (Claude's discretion):** Show warnings count but not individual warnings: "⚠️ 3 warnings (containers already in desired state)". User can run verbose flag if details needed.
**Warning signs:** Users miss critical errors because summary is too long; complaints about "too much information"
**Confidence:** MEDIUM (classification logic straightforward; presentation format requires UX judgment)
## Code Examples
### Example 1: Initialize Batch Execution
**Source:** Synthesized from n8n Loop Over Items pattern and project requirements
```javascript
// Code node: Initialize Batch State
const containers = $json.matchedContainers; // Array of container names
const action = $json.action; // 'update', 'start', 'stop', 'restart'
const chatId = $json.message.chat.id;
// Send initial progress message
const progressText = `🔄 Batch ${action} started\n\n` +
`Processing ${containers.length} containers...\n` +
`This may take a few minutes.`;
// Prepare batch items for loop
const batchItems = containers.map((name, index) => ({
containerName: name,
action: action,
currentIndex: index,
totalCount: containers.length,
chatId: chatId
}));
return batchItems.map(item => ({ json: item }));
```
**Usage:** Place before Loop Over Items node; outputs array of items for sequential processing.
### Example 2: Execute Container Action with Error Handling
**Source:** Existing workflow pattern + continue-on-error workaround
```javascript
// HTTP Request node: Execute Docker Action
// URL: http://docker-socket-proxy:2375/containers/{{ $json.containerName }}/{{ $json.action }}
// Method: POST
// Options: Continue On Fail = true
// Code node: Handle Response (place after HTTP Request)
const response = $input.first().json;
const containerName = $json.containerName;
const action = $json.action;
// Check for errors
if (!response || response.statusCode >= 400) {
let reason = 'Unknown error';
let failureType = 'error';
if (response?.statusCode === 304) {
reason = action === 'stop' ? 'Already stopped' : 'No update available';
failureType = 'warning';
} else if (response?.statusCode === 404) {
reason = 'Container not found';
} else if (response?.statusCode === 500) {
reason = response.message || 'Docker daemon error';
} else if (response?.message) {
reason = response.message;
}
return {
json: {
...$$json, // Preserve original item data
success: false,
failure: {
name: containerName,
reason: reason,
type: failureType
}
}
};
}
// Success
return {
json: {
...$$json,
success: true
}
};
```
### Example 3: Update Progress Message (Rate-Limited)
```javascript
// Code node: Decide and Format Progress Update
const currentIndex = $json.currentIndex;
const totalCount = $json.totalCount;
const containerName = $json.containerName;
const successCount = $('Set Batch State').item.json.successCount || 0;
const failureCount = $('Set Batch State').item.json.failureCount || 0;
// Only update every 5 containers or on last item
const updateInterval = totalCount <= 5 ? 1 : 5;
const shouldUpdate = (currentIndex + 1) % updateInterval === 0 || currentIndex === totalCount - 1;
if (!shouldUpdate) {
return { json: { skipUpdate: true } };
}
const progressText = `🔄 Batch Update Progress\n\n` +
`Current: ${containerName}\n` +
`Progress: ${currentIndex + 1}/${totalCount}\n\n` +
`✅ Success: ${successCount}\n` +
`❌ Failed: ${failureCount}`;
return {
json: {
skipUpdate: false,
chat_id: $json.chatId,
message_id: $('Set Batch State').item.json.progressMessageId,
text: progressText,
parse_mode: 'HTML'
}
};
```
**HTTP Request node (conditional on skipUpdate):**
```
POST https://api.telegram.org/bot{{ $credentials.telegramApi.token }}/editMessageText
Body: {{ $json }}
```
### Example 4: Format Final Batch Summary
```javascript
// Code node: Aggregate Batch Results and Format Summary
const items = $input.all();
const successes = items.filter(item => item.json.success);
const failures = items.filter(item => !item.json.success).map(item => item.json.failure);
const errors = failures.filter(f => f.type === 'error');
const warnings = failures.filter(f => f.type === 'warning');
const totalCount = items.length;
const successCount = successes.length;
let summaryText = `✅ Batch Update Complete\n\n`;
// Errors first (critical)
if (errors.length > 0) {
summaryText += `❌ Failed (${errors.length}):\n`;
errors.forEach(err => {
summaryText += ` • ${err.name}: ${err.reason}\n`;
});
summaryText += '\n';
}
// Warnings (optional - could be omitted per Claude's discretion)
if (warnings.length > 0) {
summaryText += `⚠️ Skipped (${warnings.length}):\n`;
warnings.forEach(warn => {
summaryText += ` • ${warn.name}: ${warn.reason}\n`;
});
summaryText += '\n';
}
// Success summary
const successEmoji = successCount === totalCount ? '🎉' : '✅';
summaryText += `${successEmoji} Successfully updated: ${successCount}/${totalCount}`;
return {
json: {
chat_id: items[0].json.chatId,
message_id: $('Set Batch State').item.json.progressMessageId,
text: summaryText,
parse_mode: 'HTML'
}
};
```
### Example 5: Multi-Select Keyboard with Toggle Pattern
```javascript
// Code node: Build Multi-Select Keyboard
const containers = $json.containers; // From Docker list
const callbackData = $json.callback_query?.data || '';
const selected = callbackData.split(':')[2]?.split(',').filter(Boolean) || [];
const keyboard = [];
// Container toggle buttons (limit to 15 for readability)
containers.slice(0, 15).forEach(container => {
const name = container.Names[0].replace(/^\//, '').replace(/^linuxserver-/, '');
const isSelected = selected.includes(name);
const checkmark = isSelected ? '✅ ' : '';
// Toggle logic: add if not selected, remove if selected
const newSelected = isSelected
? selected.filter(s => s !== name)
: [...selected, name];
// Validate callback_data size before creating button
const newCallback = `batch:toggle:${newSelected.join(',')}`;
if (Buffer.byteLength(newCallback, 'utf8') <= 60) {
keyboard.push([{
text: `${checkmark}${name}`,
callback_data: newCallback
}]);
} else {
// Skip this container if would exceed limit
keyboard.push([{
text: `${checkmark}${name} ⚠️ (limit reached)`,
callback_data: 'batch:limit_reached'
}]);
}
});
// Action buttons (only if selections exist)
if (selected.length > 0) {
keyboard.push([
{
text: `⬆️ Update ${selected.length}`,
callback_data: `batch:execute:update:${selected.join(',')}`
},
{
text: '❌ Clear Selection',
callback_data: 'batch:toggle:'
}
]);
}
return {
json: {
text: `Select containers to update:\n\n${selected.length} selected`,
reply_markup: { inline_keyboard: keyboard }
}
};
```
### Example 6: Exact Match Priority with Fuzzy Fallback
```javascript
// Code node: Match Container Names with Priority
const input = $json.commandArgs[0]; // e.g., 'plex'
const containers = $json.allContainers.map(c =>
c.Names[0].replace(/^\//, '').replace(/^linuxserver-/, '')
);
// Normalize input
const normalized = input.toLowerCase().trim();
// Step 1: Exact match (highest priority)
const exactMatch = containers.find(c => c.toLowerCase() === normalized);
if (exactMatch) {
return {
json: {
matched: [exactMatch],
matchType: 'exact',
needsDisambiguation: false
}
};
}
// Step 2: Prefix match (e.g., 'plex' matches 'plex' but not 'jellyplex')
const prefixMatches = containers.filter(c => c.toLowerCase().startsWith(normalized));
if (prefixMatches.length === 1) {
return {
json: {
matched: prefixMatches,
matchType: 'prefix',
needsDisambiguation: false
}
};
}
if (prefixMatches.length > 1) {
return {
json: {
matched: prefixMatches,
matchType: 'prefix_ambiguous',
needsDisambiguation: true
}
};
}
// Step 3: Substring match (fuzzy - e.g., 'plex' matches both 'plex' and 'jellyplex')
const substringMatches = containers.filter(c => c.toLowerCase().includes(normalized));
if (substringMatches.length === 1) {
return {
json: {
matched: substringMatches,
matchType: 'fuzzy',
needsDisambiguation: false
}
};
}
if (substringMatches.length > 1) {
return {
json: {
matched: substringMatches,
matchType: 'fuzzy_ambiguous',
needsDisambiguation: true
}
};
}
// No match found
return {
json: {
matched: [],
matchType: 'none',
error: `No container found matching "${input}"`
}
};
```
## State of the Art
| Old Approach | Current Approach (2026) | When Changed | Impact |
|--------------|-------------------------|--------------|--------|
| Parallel batch execution | Sequential with Loop Over Items | n8n best practices 2024+ | Deterministic order, accurate progress tracking, simpler error handling |
| Abort batch on first error | Continue with If Error node | User requirement (BAT-05) | Maximizes batch completion; requires failure aggregation |
| Multiple progress messages | Single editing message | User preference + Telegram UX best practices | Cleaner chat; requires rate limit awareness |
| Hand-rolled fuzzy matching | Libraries (Fuse.js, fast-fuzzy) | Library maturity 2020+ | Better edge case handling; but simple substring may suffice for MVP |
| Emoji state encoding in buttons | Native ToggleButton (Telegram v11.0 leak) | Pending Bot API 7.1+ (Q1 2026?) | Would eliminate callback_data state encoding; not yet stable |
**Deprecated/outdated:**
- **Parallel HTTP batch requests for Docker operations:** Docker daemon may throttle; progress tracking impossible
- **Storing batch state in Redis/database:** n8n workflow items can carry state through loop; external storage adds complexity
- **Showing every container in progress updates:** Rate limits make this infeasible for large batches; milestone updates only
## Open Questions
### 1. Should Warnings Appear in Final Summary?
**What we know:** User wants failures emphasized with actionable error info. Warnings (e.g., "already stopped") are non-critical.
**What's unclear:** Whether to show warnings at all, or only show error count + success count.
**Options:**
- **Show warnings separately:** Errors at top, warnings below, success at bottom
- **Show warnings count only:** "⚠️ 3 warnings (non-critical)" without details
- **Hide warnings entirely:** Only show errors and success count
**Recommendation:** Show warnings count only (middle option). Reduces clutter while informing user some operations were skipped. User can investigate if concerned.
**Confidence:** MEDIUM (UX judgment; user preference is "emphasize failures" but doesn't explicitly address warnings)
### 2. Cancel Button During Batch Execution?
**What we know:** Context notes "Claude's discretion on cancel button during batch."
**What's unclear:** Whether complexity of implementing cancellation is worth the feature.
**Tradeoffs:**
- **With cancel:** User can stop long-running batch; requires callback handling, batch state tracking, and "cancelled" summary
- **Without cancel:** Simpler implementation; batch always runs to completion or failure
**Recommendation:** Skip cancel button for MVP. Rationale:
1. Complexity: Requires checking for cancel callback on each loop iteration; adds branching logic
2. Limited value: Most batches complete quickly (<1 min for 5-10 containers); user can wait
3. Workaround exists: User can stop individual containers manually if batch causes issues
**If implemented in future:** Add "❌ Cancel" button to progress message, check `callback_query.data === 'batch:cancel'` in loop condition, and exit early with partial summary.
**Confidence:** MEDIUM (implementation pattern is clear; value proposition is uncertain)
### 3. How to Handle "Update All" When No Updates Available?
**What we know:** "Update all" targets containers with updates available only.
**What's unclear:** What happens if no containers have updates available when user runs command?
**Options:**
- **Show message:** "No containers have updates available"
- **Require confirmation showing 0:** "Update 0 containers?" (seems silly)
- **Skip confirmation:** Just return message immediately
**Recommendation:** Check update availability before confirmation. If 0 containers, return "✅ All containers are up to date" message immediately. If >0, show confirmation "Update X containers?" with count.
**Implementation note:** Requires fetching update status for all containers before confirmation step. May need to cache this or accept delay while checking.
**Confidence:** MEDIUM (requirement logic is clear; implementation requires update detection which may be deferred)
### 4. Multi-Select UI: Checkbox Style or Toggle Style?
**What we know:** Context says "Claude's discretion on selection UX — pick approach that fits existing keyboard flow."
**What's unclear:** Whether to use inline toggle (checkmark appears/disappears in same message) or separate confirmation screen.
**Options:**
- **Toggle style (recommended):** Click container → checkmark appears, click again → checkmark disappears, "Execute" button at bottom
- **Checkbox style:** Click container → new message with "Selected: plex. Select more or execute?"
- **Hybrid:** Container list screen with checkmarks, separate "Review selection" confirmation screen
**Recommendation:** Toggle style (first option). Matches Phase 8 keyboard infrastructure pattern where editMessageText is used for all interactions. Single message updated in-place, minimal chat clutter.
**Confidence:** HIGH (fits existing patterns; user preference for clean UX)
## Sources
### Primary (HIGH confidence)
- [n8n Docs - Looping](https://docs.n8n.io/flow-logic/looping/)
- Loop Over Items node configuration and batch processing patterns
- [n8n Docs - Error Handling](https://docs.n8n.io/flow-logic/error-handling/)
- Error workflows, Stop And Error node, error data access
- [Telegram Bot API - Official Documentation](https://core.telegram.org/bots/api)
- editMessageText method, rate limits, callback_data constraints
- [Telegram Bots FAQ](https://core.telegram.org/bots/faq)
- Rate limits: 1 message/second per chat, 20 messages/minute in groups, ~5 edits/minute per message
### Secondary (MEDIUM confidence)
- [When to use n8n's HTTP batch request vs Loop Over Items nodes](https://blog.julietedjere.com/posts/when-to-use-n8ns-http-batch-request-vs-loop-over-items-nodes)
- Comparison of batch vs sequential execution; Loop Over Items for strict sequential processing
- [n8n Community - Loop Over Items stopped when Continue configured](https://community.n8n.io/t/loop-over-items-stopped-when-there-is-continue-using-error-output-configured/34066)
- Known issue with error output in loops; workaround required
- [How to solve rate limit errors from Telegram Bot API](https://gramio.dev/rate-limits)
- 429 error handling, retry_after header, editMessageText rate limits
- [Enhancing User Engagement with Multiselection Inline Keyboards in Telegram Bots](https://medium.com/@moraneus/enhancing-user-engagement-with-multiselection-inline-keyboards-in-telegram-bots-7cea9a371b8d)
- Toggle pattern for multi-select with checkmarks; leaked ToggleButton feature in Telegram v11.0
- [AI Chatbot UX: 2026's Top Design Best Practices](https://www.letsgroto.com/blog/ux-best-practices-for-ai-chatbots)
- Batch processing as permanent UX pattern; progress tracking with pause/stop/retry controls
- [Fuzzy Matching 101: The Complete Guide to Accurate Data Matching](https://dataladder.com/fuzzy-matching-101/)
- Exact vs fuzzy match best practices; hybrid approach; normalization techniques
### Tertiary (LOW confidence)
- [Fuse.js - Fuzzy Search Library](https://www.fusejs.io/)
- Lightweight fuzzy search library; viable for small datasets only
- [fast-fuzzy npm package](https://www.npmjs.com/package/fast-fuzzy)
- High-performance fuzzy matching with trie-based optimization
- [Telegram Inline Keyboard UX Design Guide](https://wyu-telegram.com/blogs/444/)
- Button layout, pagination, Bot API 7.0 constraints (64-byte callback_data, 100-button limit for edits)
- [n8n Community - Does multiple URLs execute sequentially or in parallel?](https://community.n8n.io/t/does-multiple-urls-as-input-to-http-node-execute-sequentially-or-in-parallel/112951)
- Clarification that HTTP node processes items sequentially by default (but not wait-for-completion like Loop Over Items)
## Metadata
**Confidence breakdown:**
- Standard stack: **HIGH** - Loop Over Items and Telegram API patterns well-documented and verified
- Architecture patterns: **MEDIUM** - Sequential execution and error handling patterns verified; multi-select UI and rate limiting require careful implementation
- Pitfalls: **HIGH** - n8n loop error handling issue documented in community; Telegram rate limits verified in multiple sources; callback_data limit is API specification
**Research date:** 2026-02-03
**Valid until:** ~30 days (stable domain; watch for Telegram Bot API 7.1 with native ToggleButton)
**Key research limitations:**
1. Multi-select inline keyboard pattern is established but 64-byte callback_data limit constrains implementation significantly
2. n8n Loop Over Items error handling has known issues requiring workarounds (not officially documented)
3. Telegram editMessageText rate limit (~5/minute) is empirically verified but not officially documented
4. Native ToggleButton feature leaked in Telegram v11.0 TestFlight but not yet in stable API; timeline for Bot API 7.1 unclear
5. Container update detection API not researched (deferred or out of scope)