Files
Lucas Berger 3ca89c3815 docs(09): research phase domain
Phase 9: Batch Operations
- Standard stack identified (Loop Over Items, If Error node, editMessageText)
- Sequential execution patterns documented
- Error handling with continue-on-failure
- Multi-select keyboard toggle pattern
- Rate limiting and callback_data constraints
- Fuzzy matching with exact match priority
2026-02-03 20:52:50 -05:00

40 KiB

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:

# In n8n Code node, use npm: functions for external libraries
# Or deploy as n8n community node if significant complexity

Architecture Patterns

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:

// 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:

// Edit progress after each container
const current = $json.currentIndex + 1;
const total = $json.totalCount;
const containerName = $json.currentContainer.name;

const progressText = `🔄 <b>Batch Update Progress</b>\n\n` +
  `Processing: <b>${containerName}</b>\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:

// 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:

// 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: '<b>Select containers to update:</b>',
    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:

// 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 = `<b>Batch Update Complete</b>\n\n`;

// Errors first (critical failures)
const errors = failures.filter(f => f.type === 'error');
if (errors.length > 0) {
  summaryText += `❌ <b>Failed (${errors.length}):</b>\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 += `⚠️ <b>Warnings (${warnings.length}):</b>\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:

// 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:

// 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:

// 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:

// 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:

// 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

// 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 = `🔄 <b>Batch ${action} started</b>\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

// 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)

// 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 = `🔄 <b>Batch Update Progress</b>\n\n` +
  `Current: <b>${containerName}</b>\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

// 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 = `<b>✅ Batch Update Complete</b>\n\n`;

// Errors first (critical)
if (errors.length > 0) {
  summaryText += `❌ <b>Failed (${errors.length}):</b>\n`;
  errors.forEach(err => {
    summaryText += `  • <b>${err.name}</b>: ${err.reason}\n`;
  });
  summaryText += '\n';
}

// Warnings (optional - could be omitted per Claude's discretion)
if (warnings.length > 0) {
  summaryText += `⚠️ <b>Skipped (${warnings.length}):</b>\n`;
  warnings.forEach(warn => {
    summaryText += `  • ${warn.name}: ${warn.reason}\n`;
  });
  summaryText += '\n';
}

// Success summary
const successEmoji = successCount === totalCount ? '🎉' : '✅';
summaryText += `${successEmoji} <b>Successfully updated: ${successCount}/${totalCount}</b>`;

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

// 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: `<b>Select containers to update:</b>\n\n${selected.length} selected`,
    reply_markup: { inline_keyboard: keyboard }
  }
};

Example 6: Exact Match Priority with Fuzzy Fallback

// 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)

Secondary (MEDIUM confidence)

Tertiary (LOW confidence)

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)