Módulo 3: Loops e Iteraciones
Loops Complejos: Paginación y Patrones Avanzados
Descripción de la cápsula
Los patrones de cápsula 04 cubren lo más común. Pero hay casos donde los loops son genuinamente complejos: paginación de APIs con cursors no obvios, retry con backoff sofisticado, loops que mantienen estado entre iteraciones, recursivos. En esta cápsula vas a ver los patrones avanzados que aparecen en integraciones reales con APIs profesionales.
Estos patterns son menos frecuentes pero esenciales cuando aparecen. Saber identificarlos y resolverlos te diferencia de quien "solo sabe SplitInBatches con batch fijo".
Lo que vas a aprender
Al terminar esta cápsula serás capaz de:
- ✅ Implementar paginación real con cursors, offsets, tokens
- ✅ Manejar APIs con estructuras de paginación distintas
- ✅ Usar Code node para loops que SplitInBatches no cubre bien
- ✅ Mantener estado entre iteraciones
- ✅ Combinar SplitInBatches + Code estratégicamente
Paginación: los 3 estilos
APIs paginan de 3 maneras principales:
Estilo 1: Offset-based (más común antiguo)
GET /api/users?offset=0&limit=100
GET /api/users?offset=100&limit=100
GET /api/users?offset=200&limit=100
...
Pasas offset que indica desde qué item empezar. Sigues hasta que la respuesta tenga menos items que limit (significa última página).
Pros: simple Contras: items nuevos pueden cambiar la paginación entre requests
Estilo 2: Page-number based
GET /api/users?page=1&per_page=100
GET /api/users?page=2&per_page=100
...
Pasas page que es el número de página. Algunos APIs te devuelven total_pages para saber cuándo parar.
Pros: intuitivo Contras: mismos issues que offset
Estilo 3: Cursor-based (más moderno)
GET /api/users?cursor=null → response: { items: [...], next_cursor: "abc123" }
GET /api/users?cursor=abc123 → response: { items: [...], next_cursor: "def456" }
GET /api/users?cursor=def456 → response: { items: [...], next_cursor: null }
Pasas el cursor que la respuesta anterior te devolvió. Cuando next_cursor === null, terminaste.
Pros: estable ante cambios concurrentes, más rápido en backend Contras: no puedes "saltar a página 5" — debes recorrer
Implementación de paginación con SplitInBatches
Para cualquiera de los 3 estilos, el patrón es similar (Loop until condition de cápsula 04):
Cursor-based con SplitInBatches
[Set: cursor = null]
│
[SplitInBatches batch=1]
│ (loop)
▼
[HTTP Request: /api/users?cursor={{ cursor }}]
│
[Set: acumular response.items en array global, actualizar cursor = response.next_cursor]
│
[IF: cursor === null]
├─ TRUE → [Done] → fin
└─ FALSE → loop back a SplitInBatches
El acumulador
El truco: necesitas acumular los items de cada página en un array global. Opciones:
Opción A: Acumular en Sheet (durable)
Después de cada página, append a un Sheet:
[HTTP page X] → [Sheets: append items]
Al final del loop, lees el Sheet completo. Pros: durable, sobrevive a fallos. Contras: I/O extra.
Opción B: Acumular en variable del workflow
Usar Set + expressions para mantener una variable que crece:
[Set: accumulated_items = accumulated_items + response.items]
Pros: simple. Contras: se pierde si el workflow falla.
Opción C: Code node (más control)
Cuando la lógica es compleja, Code node te da JavaScript completo:
// Code node — loop completo en un solo nodo
let allItems = [];
let cursor = null;
do {
const response = await fetch(`/api/users?cursor=${cursor || ''}`);
const data = await response.json();
allItems = allItems.concat(data.items);
cursor = data.next_cursor;
} while (cursor !== null);
return [{ json: { all_items: allItems } }];
Pros: lógica en un solo lugar, mucho más rápido. Contras: requiere conocer JS, debugging diferente.
Cuándo Code node es mejor que SplitInBatches
A pesar de la promesa "sin código" de n8n, a veces Code es la opción correcta:
Code es mejor para:
- Paginación donde la lógica de cursor es compleja
- Loops con condiciones que dependen de cálculos
- Procesamiento que mezcla loops y transformaciones
- Cuando SplitInBatches "no cuadra" naturalmente
SplitInBatches es mejor para:
- Loops con rate limit (Wait integrado)
- Cuando cada iteración hace algo distinto (HTTP a APIs diferentes)
- Cuando necesitas observar cada iteración en Executions (debugging)
- Cuando la lógica del loop es simple
Regla práctica: si tu loop requiere 5+ nodos diferentes en el procesamiento, SplitInBatches. Si es 1-2 nodos + lógica de cursor, considera Code.
Patrón: paginación + procesamiento del batch en stream
A veces no quieres acumular todos los items antes de procesar — quieres procesar cada página apenas la recibes (stream processing).
Estructura
[Set: cursor = null]
│
[SplitInBatches batch=1]
│
[HTTP: get page]
│
[Procesar items de la página] ← procesamiento real aquí
│
[IF: cursor null?]
├─ TRUE → [Done]
└─ FALSE → loop back
Ventaja: memoria usage bajo (no acumulas), puede empezar a procesar antes de tener todos.
Cuándo: items grandes, listas inmensas, o el procesamiento incluye guardar a DB (no necesitas acumular).
Patrón: retry con exponential backoff sofisticado
Para retries más sofisticados, Code node suele ser más limpio:
// Code node — retry con backoff
let retries = 0;
const maxRetries = 5;
while (retries < maxRetries) {
try {
const response = await fetch(url);
if (response.ok) return [{ json: await response.json() }];
if (response.status === 429) {
const waitSec = parseInt(response.headers.get('Retry-After') || '60');
await new Promise(r => setTimeout(r, waitSec * 1000));
} else if (response.status >= 500) {
const waitSec = Math.min(Math.pow(2, retries), 60);
await new Promise(r => setTimeout(r, waitSec * 1000));
} else {
throw new Error(`Non-retryable error: ${response.status}`);
}
} catch (e) {
if (retries === maxRetries - 1) throw e;
}
retries++;
}
Hace retry inteligente: respeta Retry-After, exponential backoff para 5xx, no retry para 4xx no recuperables.
Patrón: dynamic batch size
A veces el batch size óptimo depende del tiempo de día, carga, response del API.
Implementación
[Set: batch_size inicial = 10]
│
[SplitInBatches]
│
[procesar batch]
│
[Set: si último batch tardó <2s, batch_size += 5; si >10s, batch_size -= 5]
│
└─→ loop back
Adaptativo: aumenta batch cuando la API responde rápido, lo reduce cuando se ralentiza.
(Requiere SplitInBatches con Reset = ON para que respete el cambio de batch_size — no siempre disponible. Alternativa: Code node con loop manual.)
Patrón: parallel batches (con control)
Por default SplitInBatches procesa batches secuencialmente (el batch 2 espera al 1). A veces quieres N batches en paralelo.
Solución 1: Webhook fan-out
[Master workflow]
│
[Split items into N chunks]
│
[N webhook calls a sub-workflows (paralelo)]
│
[Aggregate when all return]
Cada chunk se procesa en su propio workflow, paralelo.
Solución 2: Workflow con concurrency configurada
En n8n self-hosted, puedes configurar concurrencia. Ver docs de tu versión.
Cuándo: procesamientos muy grandes (10,000+ items) donde la paralelización vale la complejidad.
Trampas comunes
Trampa 1: Loop sin condición de salida real
Qué pasa: Loop con IF cursor !== null pero el cursor de la API nunca llega a null (bug del API o tu interpretación). Loop infinito.
Cómo evitar:
- Siempre incluir contador de seguridad (max 1000 iteraciones, lanza error)
- Test con escenarios donde el cursor sí termina
Trampa 2: Acumular en memoria sin límite
Qué pasa: Acumular 100,000 items en una variable. Memoria de n8n se queda corta.
Cómo evitar:
- Para volumen masivo, stream processing (procesa cada página y descarta)
- O usar DB como acumulador
Trampa 3: Code node con await fetch que falla
Qué pasa: Code node con await fetch(). Funciona en tu local pero falla en n8n cloud porque fetch global no está disponible siempre.
Cómo evitar:
- Usar HTTP Request node dentro del workflow (no fetch en Code)
- Si necesitas fetch en Code, verificar disponibilidad en tu versión de n8n
Trampa 4: Cursor "reset" entre páginas
Qué pasa: Cada llamada al SplitInBatches re-evalúa cursor desde el inicio. Loop infinito procesando la primera página.
Cómo evitar:
- Asegurar que
cursorse persiste en items que vuelven al SplitInBatches - Set node que mantiene
cursoractualizado en el item
Trampa 5: API que cambia formato entre páginas
Qué pasa: Primera página devuelve { items, next_cursor }. Última página devuelve { items, next_cursor: "" } (string vacío, no null). Tu IF compara con null, sigue infinitamente.
Cómo evitar: Inspeccionar la respuesta del API en cada caso edge. Tratar tanto null como "" como "fin".
Ejercicio: implementar paginación
Objetivo: practicar con un API real (o mock).
Tu tarea
Usa un API de prueba con paginación como JSONPlaceholder (no tiene paginación real) o un mock que controles.
Implementa:
- Workflow que hace GET a
/api/items?page=N - Si la respuesta tiene 100 items, hay más páginas
- Si tiene <100, es la última
- Acumula todos los items y al final manda total a Slack
Detalles del API mock:
- 350 items total
- 4 páginas (100 + 100 + 100 + 50)
Tu workflow debería hacer 4 requests y reportar 350.
Resumen y siguiente paso
- 3 estilos de paginación: offset, page-number, cursor — todos manejables con loop until condition
- Acumulación: Sheet (durable), variable (rápida), Code (control completo)
- Code node es mejor para lógica de cursor compleja; SplitInBatches para rate limit + observabilidad
- Stream processing: procesar cada página apenas llega (no acumular todo)
- Retry sofisticado y batch dinámico suelen mejor en Code
- Parallel batches requieren arquitectura especial (webhook fan-out)
- 5 trampas: loop sin condición real, memoria, fetch en Code, cursor reset, formato cambiante
Antes de avanzar deberías poder:
- Identificar el estilo de paginación de un API
- Implementar paginación cursor-based con SplitInBatches o Code
- Decidir entre SplitInBatches y Code según complejidad
Lo que sigue (cápsula 08):
Cierre del módulo: mini-proyecto procesando 1000 items con todos los patrones. Vas a integrar SplitInBatches, Wait, Aggregate, manejo de errores, y reporting final — todo lo que aprendiste en una pieza coherente.
Recursos adicionales
- Pagination Patterns Comparison - Comparativa de patrones.
- Code Node Documentation - Para loops complejos.
Creado: Mayo 11, 2026 Versión: 1.0