Compatibilidad de nodos con el Iterador
Esta guía explica qué nodos pueden usarse dentro del patrón Iterator → [procesamiento] → MergeResults y cuáles causan errores o comportamientos inesperados, con la razón técnica exacta de cada caso.
Cómo funciona el iterador (resumen técnico)
Sección titulada «Cómo funciona el iterador (resumen técnico)»Cuando el nodo Iterator procesa un array de N items, el motor genera N ejecuciones independientes en paralelo (limitadas por maxConcurrent, por defecto 5). Cada ejecución:
- Recibe solo su item en
data. - Carga metadatos propios en
_meta_(currentIndex,totalChunks,workflowRunId). - Corre de forma aislada a través de los nodos siguientes.
- Al llegar a un nodo merge, deposita su resultado y espera a los demás.
El problema surge cuando un nodo mantiene estado global (escribe en BD con una clave fija, usa el mismo ID para todas las ejecuciones, o depende de recursos compartidos): dos items concurrentes colisionan sobre el mismo recurso.
Tipos de merge disponibles
Sección titulada «Tipos de merge disponibles»| Nodo | Almacenamiento | Cuándo usarlo |
|---|---|---|
| mergeResults | En memoria | La opción principal para Iterator. Rápido, sin hits a BD. |
| mergeMultiple | Base de datos | Para convergencias desde múltiples ramas no relacionadas. No para Iterator. |
| merge | Base de datos | Legacy, no soporta iteraciones anidadas. Evitar con Iterator. |
| mergeIterator | Pass-through | Solo reformatea; no espera chunks. Casos muy específicos. |
Regla general: Iterator → [nodos] → mergeResults.
Nodos compatibles ✅
Sección titulada «Nodos compatibles ✅»Estos nodos funcionan correctamente dentro de una iteración porque son sin estado global: cada ejecución opera sobre sus propios datos sin tocar recursos compartidos.
Transformación y lógica
Sección titulada «Transformación y lógica»| Nodo | Notas |
|---|---|
evaluateExpression | Lógica JavaScript pura sobre data. Sin efectos secundarios. |
condition / decision | Bifurcación lógica. Sin escritura a BD. |
switch / router / multiroute | Enrutamiento. Sin estado. |
editFields | Renombra y transforma campos en memoria. |
dataTransform / dataMapper / dataMapping | Transformaciones de datos. Sin estado. |
keyFilter / objectToArray / arrayFilter | Operaciones sobre objetos/arrays. |
jsonToXml / xmlToJson / jsonToToon / toonToJson | Conversiones de formato. |
splitStringList / schemaParser | Transformación de datos. Sin estado. |
htmlTemplate | Renderiza plantilla con variables del item. Cada item genera su propio HTML. |
barcodeGenerator | Genera imagen PNG en memoria. Sin estado global. |
Integración HTTP y APIs
Sección titulada «Integración HTTP y APIs»| Nodo | Notas |
|---|---|
http | Cada item hace su propia llamada HTTP independiente. |
graphql / graphqlQuery | Mismo principio que HTTP. |
executeQuery (SELECT) | Lectura de MySQL. Sin colisión si no hay escritura concurrente sobre el mismo row. |
sqlQuery / sqlQueryPostgres / sqlQueryMSSQL / sqlQueryOracle | Lecturas seguras; escrituras INSERT seguras si hay clave única. Ver notas. |
Comunicaciones (1 mensaje por item)
Sección titulada «Comunicaciones (1 mensaje por item)»| Nodo | Notas |
|---|---|
sendMail | Cada item envía su propio email. Sin colisión. |
slack / discord / telegram / whatsapp | Cada item envía su propio mensaje. |
ntfyNotify | Notificación por item. Sin estado. |
Almacenamiento (operaciones de escritura unitaria)
Sección titulada «Almacenamiento (operaciones de escritura unitaria)»| Nodo | Notas |
|---|---|
dataStore (acción add) | Cada item inserta 1 registro. Sin colisión entre items. |
awsS3 / s3Compatible (upload) | Cada item sube su propio archivo. Sin colisión si la clave es única por item. |
filetransfers (upload) | Mismo principio. |
save2json / saveToExcel | Si la ruta incluye {{_meta_.currentIndex}} para evitar sobreescritura. |
getVariable | Solo lectura. Sin riesgo. |
Integraciones de escritura unitaria
Sección titulada «Integraciones de escritura unitaria»| Nodo | Notas |
|---|---|
| Shopify (crear orden, actualizar producto) | Cada item opera sobre su propio recurso. |
| Airtable (insert) | Cada item inserta 1 fila. |
| Google Sheets (append) | Cada item añade 1 fila. Sin colisión. |
| Mailchimp (suscribir contacto) | Operación individual por item. |
Cualquier Create* de integración | Sin estado compartido. |
Nodos incompatibles ❌
Sección titulada «Nodos incompatibles ❌»Estos nodos causan errores o datos corruptos cuando se ejecutan dentro de una iteración.
delay — Unique key violation
Sección titulada «delay — Unique key violation»Problema técnico: El nodo delay registra el diferimiento en la tabla workflow_delays con esta clave única:
UNIQUE KEY unique_workflow_delay (workflow_id, delay_node_id)El delay_node_id es el ID del nodo en el canvas, que es el mismo para todos los items. Cuando 5 items concurrentes intentan insertar en workflow_delays con el mismo (workflow_id, delay_node_id), el segundo item ya falla con Duplicate entry ... for key 'unique_workflow_delay'.
Síntoma: Solo el primer item que llega al nodo Delay se procesa; los demás fallan con error de SQL.
Solución: Colocar el Delay antes del Iterator o después del Merge, nunca dentro.
✅ Delay → Iterator → [procesar] → mergeResults✅ Iterator → [procesar] → mergeResults → Delay❌ Iterator → Delay → [procesar] → mergeResultssubWorkflow — Colisión de parent_instance_id
Sección titulada «subWorkflow — Colisión de parent_instance_id»Problema técnico: Cuando lanza un subworkflow, el nodo registra:
parent_workflow_id: config.workflowId, // Mismo para todos los itemsparent_instance_id: config.workflowId, // Mismo para todos los itemsSi 5 items concurrentes ejecutan el mismo subWorkflow, los 5 hijos comparten el mismo parent_workflow_id. Las instancias hijo se sobreescriben en la tabla workflowinstances, el padre pierde el seguimiento de los hijos anteriores y los resultados de los primeros items se pierden.
Síntoma: El historial de ejecución muestra menos instancias hijo de las esperadas. Los primeros items completados no aparecen en los resultados del padre.
Solución: No usar subWorkflow dentro de iteraciones. Si necesitás ejecutar múltiples workflows hijos en paralelo, diseñar la lógica como un único workflow que recibe un array y usa su propio Iterator internamente.
Triggers (Webhook, FormTrigger, DataStoreTrigger, MqttTrigger, RedisTrigger, AgentFollowUpTrigger)
Sección titulada «Triggers (Webhook, FormTrigger, DataStoreTrigger, MqttTrigger, RedisTrigger, AgentFollowUpTrigger)»Problema: Los triggers son nodos de inicio de workflow, no de procesamiento intermedio. Esperan señales externas (HTTP, eventos, formularios) y no pueden operar como transformadores dentro de una cadena de iteración.
Síntoma: Errores de formato o el nodo queda esperando un evento externo que nunca llega.
Solución: Los triggers solo van al inicio del workflow. Nunca dentro de un iterador.
start / end
Sección titulada «start / end»Problema: Son nodos especiales del motor. start es el punto de entrada del workflow; end finaliza toda la ejecución. Colocarlos dentro de un iterador interrumpe la lógica del batch.
Solución: No usarlos dentro de iteradores.
Nodos en zona gris ⚠️
Sección titulada «Nodos en zona gris ⚠️»Funcionan en ciertos casos pero requieren atención especial.
setVariable — Sobrescritura entre items concurrentes
Sección titulada «setVariable — Sobrescritura entre items concurrentes»Mecanismo: Escribe en la tabla memorystorage con (client_id, key_name).
Riesgo: Si 5 items concurrentes ejecutan setVariable('resultado', data), todos escriben sobre la misma clave. El último item que escribe gana; los anteriores se sobrescriben silenciosamente.
Item 1: setVariable('resultado', {id:1}) → BD: resultado={id:1}Item 2: setVariable('resultado', {id:2}) → BD: resultado={id:2} ← sobreescribe 1Item 3: setVariable('resultado', {id:3}) → BD: resultado={id:3} ← sobreescribe 2Cuándo funciona sin problema: Cuando cada item escribe en una clave única por index:
setVariable('resultado_{{_meta_.currentIndex}}', data)→ resultado_0, resultado_1, resultado_2 → sin colisiónCuándo falla: Cuando todos los items escriben en la misma clave (para acumular un contador, por ejemplo). Para eso usá el array summary que devuelve mergeResults y procesalo después del merge.
dataStore (acción filter, update, delete)
Sección titulada «dataStore (acción filter, update, delete)»| Acción | Compatibilidad | Detalle |
|---|---|---|
add | ✅ | Inserta 1 fila por item. Sin colisión. |
filter | ⚠️ | Múltiples reads concurrentes son seguros. Si otro item modifica el mismo registro entre la lectura y el uso → inconsistencia. |
update | ⚠️ | Riesgo de race condition si dos items actualizan el mismo registro. Solo seguro si cada item opera sobre un registro distinto. |
delete | ⚠️ | Igual que update. Si dos items intentan borrar el mismo registro, el segundo falla silenciosamente. |
bulk | ✅ | Operación atómica sobre un lote. Sin colisión. |
Recomendación: Dentro del iterador, usá preferentemente add. Las operaciones update y delete solo son seguras si cada item garantiza operar sobre un registro diferente (por ejemplo, usando un campo único como order_id).
executeQuery / sqlQuery (INSERT, UPDATE, DELETE)
Sección titulada «executeQuery / sqlQuery (INSERT, UPDATE, DELETE)»INSERT con clave única: Seguro si la clave única incluye un campo que varía por item (ej. order_id). Si la clave no varía, el segundo item duplica y falla.
UPDATE: Seguro si cada item actualiza su propio row. Riesgo si dos items compiten por el mismo row.
DELETE: Igual que UPDATE.
Recomendación: Asegurarse de que la query incluye un identificador del item (WHERE id = {{order_id}}) para garantizar que cada item opera sobre su propio registro.
Agent / agentChat — Historial de conversación compartido
Sección titulada «Agent / agentChat — Historial de conversación compartido»Problema: El agente mantiene el historial de conversación indexado por agent_id, que es el mismo para todos los items. Cuando 5 items concurrentes usan el mismo agente:
- El historial se entremezcla: las respuestas del agente mezclan contexto de diferentes items.
- El agente “recuerda” mensajes de otros items como si fueran del mismo hilo.
Cuándo funciona: Si cada item configura un session_id o agent_id único (por ejemplo, incluyendo _meta_.currentIndex o un ID de negocio del item).
Cuándo falla: Con agent_id fijo igual para todos los items.
repeater dentro de iterator — Iteración anidada
Sección titulada «repeater dentro de iterator — Iteración anidada»Técnicamente funciona: el motor asigna mergeLevel y mergeGroupKey a cada nivel de anidamiento, y mergeResults los distingue correctamente.
Overhead: N items del iterador × M repeticiones del repeater = N×M ejecuciones concurrentes. Puede saturar el pool de concurrencia y consumir más memoria.
Recomendación: Usarlo solo cuando la lógica lo requiera explícitamente. En muchos casos se puede reestructurar el array de entrada para evitar el anidamiento.
generatepdf / ocrExtract / pdfExtractor — Carga de memoria
Sección titulada «generatepdf / ocrExtract / pdfExtractor — Carga de memoria»Cada item procesa un archivo completo en memoria. Con maxConcurrent=5 y archivos de varios MB, se pueden consumir cientos de MB simultáneamente.
Solución: Reducir maxConcurrent en el Iterator cuando los nodos procesan archivos pesados. Un valor de 2-3 suele ser suficiente.
Resumen visual
Sección titulada «Resumen visual»✅ COMPATIBLE ⚠️ CON CONDICIONES ❌ INCOMPATIBLE| Nodo | Estado | Motivo resumido |
|---|---|---|
evaluateExpression | ✅ | Sin estado |
condition / decision | ✅ | Sin estado |
http / graphql | ✅ | Cada item → su propia llamada |
sendMail / slack / telegram | ✅ | 1 mensaje por item |
dataStore add | ✅ | Inserta 1 fila por item |
getVariable | ✅ | Solo lectura |
htmlTemplate / barcodeGenerator | ✅ | Procesamiento en memoria |
awsS3 upload | ✅ | Si la key es única por item |
setVariable | ⚠️ | Seguro solo con clave única por index |
dataStore filter/update/delete | ⚠️ | Seguro si cada item usa su propio registro |
executeQuery INSERT/UPDATE | ⚠️ | Seguro si la WHERE garantiza aislamiento |
Agent / agentChat | ⚠️ | Requiere session_id único por item |
repeater (anidado) | ⚠️ | Funciona, pero con overhead de N×M |
generatepdf / ocrExtract | ⚠️ | Reducir maxConcurrent con archivos grandes |
delay | ❌ | Unique key violation en BD |
subWorkflow | ❌ | Colisión de parent_instance_id |
Webhook / FormTrigger | ❌ | Son triggers de inicio, no procesadores |
DataStoreTrigger / MqttTrigger | ❌ | Ídem |
start / end | ❌ | Nodos especiales del motor |
Patrones recomendados
Sección titulada «Patrones recomendados»Patrón básico (el más común)
Sección titulada «Patrón básico (el más común)»[Fuente] → Iterator → [HTTP / evaluateExpression / dataStore.add] → mergeResults → [Siguiente nodo]Procesar lista con acciones individuales y consolidar
Sección titulada «Procesar lista con acciones individuales y consolidar»[Obtener lista] → Iterator → executeQuery → sendMail → mergeResults → [Resumen final]Generar documentos por item
Sección titulada «Generar documentos por item»[Obtener lista] → Iterator → barcodeGenerator → htmlTemplate → generatepdf → mergeResults → [Enviar]Usar setVariable de forma segura en iteración
Sección titulada «Usar setVariable de forma segura en iteración»// En el config de setVariable dentro del iterator:keyName: "resultado_{{_meta_.currentIndex}}"// → genera resultado_0, resultado_1, resultado_2 ... sin colisiónDónde colocar el Delay
Sección titulada «Dónde colocar el Delay»✅ [Start] → Delay → Iterator → [procesar] → mergeResults → [End]✅ [Start] → Iterator → [procesar] → mergeResults → Delay → [End]❌ [Start] → Iterator → Delay → [procesar] → mergeResults ← FallaNodos relacionados
Sección titulada «Nodos relacionados»- Iterator — divide un array en items paralelos
- MergeResults — recombina los resultados en memoria
- MergeMultiple — convergencia desde múltiples ramas (no usar con Iterator)
- Repeater — genera N repeticiones de un mismo payload
- Delay — demora la ejecución (usar antes o después del Iterator)
- Buenas Prácticas — guía general de diseño de workflows