Skip to content

Node Compatibility with the Iterator

This guide explains which nodes can be used inside the Iterator → [processing] → MergeResults pattern, and which cause errors or unexpected behavior, with the exact technical reason for each case.


How the iterator works (technical summary)

Section titled “How the iterator works (technical summary)”

When the Iterator node processes an array of N items, the engine generates N independent parallel executions (limited by maxConcurrent, default 5). Each execution:

  • Receives only its item in data.
  • Gets its own metadata in _meta_ (currentIndex, totalChunks, workflowRunId).
  • Runs isolated through the following nodes.
  • When it reaches a merge node, it deposits its result and waits for the others.

The problem arises when a node maintains global state (writes to the DB with a fixed key, uses the same ID for all executions, or depends on shared resources): two concurrent items collide on the same resource.


NodeStorageWhen to use
mergeResultsIn memoryThe main option for Iterator. Fast, no DB hits.
mergeMultipleDatabaseFor convergences from multiple unrelated branches. Not for Iterator.
mergeDatabaseLegacy, doesn’t support nested iterations. Avoid with Iterator.
mergeIteratorPass-throughOnly reformats; doesn’t wait for chunks. Very specific cases.

General rule: Iterator → [nodes] → mergeResults.


These nodes work correctly inside an iteration because they are stateless: each execution operates on its own data without touching shared resources.

NodeNotes
evaluateExpressionPure JavaScript logic on data. No side effects.
condition / decisionLogical branching. No DB writes.
switch / router / multirouteRouting. No state.
editFieldsRenames and transforms fields in memory.
dataTransform / dataMapper / dataMappingData transformations. No state.
keyFilter / objectToArray / arrayFilterObject/array operations.
jsonToXml / xmlToJson / jsonToToon / toonToJsonFormat conversions.
splitStringList / schemaParserData transformation. No state.
htmlTemplateRenders template with the item’s variables. Each item generates its own HTML.
barcodeGeneratorGenerates PNG image in memory. No global state.
NodeNotes
httpEach item makes its own independent HTTP call.
graphql / graphqlQuerySame principle as HTTP.
executeQuery (SELECT)MySQL read. No collision if there’s no concurrent write on the same row.
sqlQuery / sqlQueryPostgres / sqlQueryMSSQL / sqlQueryOracleSafe reads; safe INSERT writes if there’s a unique key per item. See notes.
NodeNotes
sendMailEach item sends its own email. No collision.
slack / discord / telegram / whatsappEach item sends its own message.
ntfyNotifyNotification per item. No state.
NodeNotes
dataStore (action add)Each item inserts 1 record. No collision between items.
awsS3 / s3Compatible (upload)Each item uploads its own file. No collision if the key is unique per item.
filetransfers (upload)Same principle.
save2json / saveToExcelIf the path includes {{_meta_.currentIndex}} to avoid overwriting.
getVariableRead-only. No risk.
NodeNotes
Shopify (create order, update product)Each item operates on its own resource.
Airtable (insert)Each item inserts 1 row.
Google Sheets (append)Each item appends 1 row. No collision.
Mailchimp (subscribe contact)Individual operation per item.
Any Create* integration nodeNo shared state.

These nodes cause errors or corrupted data when executed inside an iteration.

Technical problem: The delay node records the deferral in the workflow_delays table with this unique key:

UNIQUE KEY unique_workflow_delay (workflow_id, delay_node_id)

The delay_node_id is the node’s ID in the canvas, which is the same for all items. When 5 concurrent items try to insert into workflow_delays with the same (workflow_id, delay_node_id), the second item fails with Duplicate entry ... for key 'unique_workflow_delay'.

Symptom: Only the first item to reach the Delay node is processed; the rest fail with a SQL error.

Solution: Place the Delay before the Iterator or after the Merge, never inside.

✅ Delay → Iterator → [process] → mergeResults
✅ Iterator → [process] → mergeResults → Delay
❌ Iterator → Delay → [process] → mergeResults

subWorkflow — parent_instance_id collision

Section titled “subWorkflow — parent_instance_id collision”

Technical problem: When launching a sub-workflow, the node registers:

parent_workflow_id: config.workflowId, // Same for all items
parent_instance_id: config.workflowId, // Same for all items

If 5 concurrent items execute the same subWorkflow, all 5 children share the same parent_workflow_id. Child instances overwrite each other in the workflowinstances table; the parent loses track of earlier children and the results of first items are lost.

Symptom: The execution history shows fewer child instances than expected. The first completed items don’t appear in the parent’s results.

Solution: Don’t use subWorkflow inside iterations. If you need to execute multiple child workflows in parallel, design the logic as a single workflow that receives an array and uses its own Iterator internally.


Triggers (Webhook, FormTrigger, DataStoreTrigger, MqttTrigger, RedisTrigger, AgentFollowUpTrigger)

Section titled “Triggers (Webhook, FormTrigger, DataStoreTrigger, MqttTrigger, RedisTrigger, AgentFollowUpTrigger)”

Problem: Triggers are workflow start nodes, not intermediate processors. They wait for external signals (HTTP, events, forms) and cannot act as transformers inside an iteration chain.

Symptom: Format errors or the node stays waiting for an external event that never arrives.

Solution: Triggers only go at the beginning of the workflow. Never inside an iterator.


Problem: These are special engine nodes. start is the workflow entry point; end terminates the entire execution. Placing them inside an iterator breaks the batch logic.

Solution: Do not use them inside iterators.


These work in certain cases but require special attention.

setVariable — Overwrite between concurrent items

Section titled “setVariable — Overwrite between concurrent items”

Mechanism: Writes to the memorystorage table with (client_id, key_name).

Risk: If 5 concurrent items execute setVariable('result', data), they all write to the same key. The last item to write wins; previous ones are silently overwritten.

Item 1: setVariable('result', {id:1}) → DB: result={id:1}
Item 2: setVariable('result', {id:2}) → DB: result={id:2} ← overwrites 1
Item 3: setVariable('result', {id:3}) → DB: result={id:3} ← overwrites 2

When it works without issue: When each item writes to a unique key per index:

setVariable('result_{{_meta_.currentIndex}}', data)
→ result_0, result_1, result_2 → no collision

When it fails: When all items write to the same key (to accumulate a counter, for example). For that, use the summary array returned by mergeResults and process it after the merge.


dataStore (actions filter, update, delete)

Section titled “dataStore (actions filter, update, delete)”
ActionCompatibilityDetail
add✅Inserts 1 row per item. No collision.
filter⚠️Multiple concurrent reads are safe. If another item modifies the same record between read and use → inconsistency.
update⚠️Race condition risk if two items update the same record. Only safe if each item operates on a different record.
delete⚠️Same as update. If two items try to delete the same record, the second fails silently.
bulk✅Atomic operation on a batch. No collision.

Recommendation: Inside the iterator, preferably use add. The update and delete operations are only safe if each item is guaranteed to operate on a different record (e.g., using a unique field like order_id).


executeQuery / sqlQuery (INSERT, UPDATE, DELETE)

Section titled “executeQuery / sqlQuery (INSERT, UPDATE, DELETE)”

INSERT with unique key: Safe if the unique key includes a field that varies per item (e.g., order_id). If the key doesn’t vary, the second item duplicates and fails.

UPDATE: Safe if each item updates its own row. Risk if two items compete for the same row.

DELETE: Same as UPDATE.

Recommendation: Make sure the query includes an item identifier (WHERE id = {{order_id}}) to guarantee each item operates on its own record.


Agent / agentChat — Shared conversation history

Section titled “Agent / agentChat — Shared conversation history”

Problem: The agent maintains conversation history indexed by agent_id, which is the same for all items. When 5 concurrent items use the same agent:

  • The history gets mixed: agent responses blend context from different items.
  • The agent “remembers” messages from other items as if they were from the same thread.

When it works: If each item configures a unique session_id or agent_id (e.g., including _meta_.currentIndex or a business ID from the item).

When it fails: With a fixed agent_id shared across all items.


repeater inside iterator — Nested iteration

Section titled “repeater inside iterator — Nested iteration”

Technically works: the engine assigns mergeLevel and mergeGroupKey to each nesting level, and mergeResults distinguishes them correctly.

Overhead: N items from the iterator × M repetitions from the repeater = N×M concurrent executions. This can saturate the concurrency pool and consume more memory.

Recommendation: Use it only when the logic explicitly requires it. In many cases the input array can be restructured to avoid nesting.


generatepdf / ocrExtract / pdfExtractor — Memory load

Section titled “generatepdf / ocrExtract / pdfExtractor — Memory load”

Each item processes a full file in memory. With maxConcurrent=5 and files of several MB, hundreds of MB can be consumed simultaneously.

Solution: Reduce maxConcurrent in the Iterator when nodes process large files. A value of 2-3 usually works well.


✅ COMPATIBLE ⚠️ WITH CONDITIONS ❌ INCOMPATIBLE
NodeStatusShort reason
evaluateExpression✅No state
condition / decision✅No state
http / graphql✅Each item → its own call
sendMail / slack / telegram✅1 message per item
dataStore add✅Inserts 1 row per item
getVariable✅Read-only
htmlTemplate / barcodeGenerator✅In-memory processing
awsS3 upload✅If the key is unique per item
setVariable⚠️Safe only with unique key per index
dataStore filter/update/delete⚠️Safe if each item uses its own record
executeQuery INSERT/UPDATE⚠️Safe if WHERE guarantees isolation
Agent / agentChat⚠️Requires unique session_id per item
repeater (nested)⚠️Works, but with N×M overhead
generatepdf / ocrExtract⚠️Reduce maxConcurrent with large files
delay❌Unique key violation in DB
subWorkflow❌parent_instance_id collision
Webhook / FormTrigger❌Start triggers, not processors
DataStoreTrigger / MqttTrigger❌Same
start / end❌Special engine nodes

[Source] → Iterator → [HTTP / evaluateExpression / dataStore.add] → mergeResults → [Next node]

Process list with individual actions and consolidate

Section titled “Process list with individual actions and consolidate”
[Get list] → Iterator → executeQuery → sendMail → mergeResults → [Final summary]
[Get list] → Iterator → barcodeGenerator → htmlTemplate → generatepdf → mergeResults → [Send]
// In the setVariable config inside the iterator:
keyName: "result_{{_meta_.currentIndex}}"
// → generates result_0, result_1, result_2 ... no collision
✅ [Start] → Delay → Iterator → [process] → mergeResults → [End]
✅ [Start] → Iterator → [process] → mergeResults → Delay → [End]
❌ [Start] → Iterator → Delay → [process] → mergeResults ← Fails

  • Iterator — splits an array into parallel items
  • MergeResults — recombines results in memory
  • MergeMultiple — convergence from multiple branches (don’t use with Iterator)
  • Repeater — generates N repetitions of the same payload
  • Delay — defers execution (use before or after the Iterator)
  • Good Practices — general workflow design guide