Loop
Do the same thing for every item in a list. The two loop modes, the two output handles, the variables available inside the body, and how to stop a loop early.
Everything so far has handled one record. One contact changed, one message arrived, one decision got made. But plenty of work arrives as a list: fifty leads that went quiet, every unpaid invoice, the rows in a spreadsheet somebody just uploaded.
You could build fifty copies of the same three nodes. The Loop node is the alternative — it runs the steps you connect to it once per item, then hands you everything they produced.
Two modes, two different jobs
| Mode | Runs | Reach for it when |
|---|---|---|
array | Once per item in a list. | You have a collection — search results, rows, a parsed file. |
count | Up to max_iterations times, stopping early when a condition is met. | You are retrying something, or polling until an answer arrives. |
array
- Runs
- Once per item in a list.
- Reach for it when
- You have a collection — search results, rows, a parsed file.
count
- Runs
- Up to
max_iterationstimes, stopping early when a condition is met. - Reach for it when
- You are retrying something, or polling until an answer arrives.
The rest of this lesson is about array mode. count mode is covered at the end, because it behaves differently enough to be worth separating.
The fields that make it run
| Field | What it holds |
|---|---|
array_source | Source array path or variable reference |
input_array | Input array field name Defaults to items. |
output_field | Name of the output field containing results Defaults to results. |
continue_on_error | Continue processing if an item fails Defaults to true. |
concurrent | Enable concurrent processing of array items Defaults to false. |
max_concurrent | Maximum concurrent items to process Defaults to 5. |
array_source
- What it holds
- Source array path or variable reference
input_array
- What it holds
- Input array field name Defaults to
items.
output_field
- What it holds
- Name of the output field containing results Defaults to
results.
continue_on_error
- What it holds
- Continue processing if an item fails Defaults to
true.
concurrent
- What it holds
- Enable concurrent processing of array items Defaults to
false.
max_concurrent
- What it holds
- Maximum concurrent items to process Defaults to
5.
Two fields, one box. The config panel shows a single Input Array field, and it writes input_array. The engine reads array_source — but falls back to input_array when that holds a {{template}}, so filling in the panel works. If you author through the API, set both, plus output_field, or the builder flags “Input array path is required” on a loop that would have run perfectly well.
A worked example
A nightly schedule pulls a list of warm contacts, the loop creates one follow-up task per contact, and a single note gets written at the end saying how many there were.
One list in, one task per item out
The loop has two outgoing arms, and they run at different times.
Scroll for all 5 steps →
The two arms are the thing to understand here, because they are not two branches in the sense the Condition node means.
- Loop Body runs once per item. With fifty contacts, everything hanging off this handle runs fifty times.
- Complete runs once, after every item is done, with all the collected results. This is where a summary, a report or a single notification goes.
A Complete arm is worth adding even when you think you don't need one. It is the only place that knows the whole batch finished, and it is where “the nightly sweep processed 0 contacts” becomes visible instead of silent.
Reading the current item
Inside the body, the loop exposes three variables that change on every pass:
| Variable | Holds |
|---|---|
{{loop_1.current_item}} | The item being processed right now. Reach into it as usual — `.first_name`, `.id`. |
{{loop_1.current_index}} | Its position, counting from zero. |
{{loop_1.total_items}} | How many there are altogether. The same on every pass. |
{{loop_1.current_item}}
- Holds
- The item being processed right now. Reach into it as usual — `.first_name`, `.id`.
{{loop_1.current_index}}
- Holds
- Its position, counting from zero.
{{loop_1.total_items}}
- Holds
- How many there are altogether. The same on every pass.
So the task title in the example above is written like this:
{
"title": "Follow up with {{loop_1.current_item.first_name}}",
"entity_type": "contact",
"entity_id": "{{loop_1.current_item.id}}"
}Watch out: Set array_source AND input_array AND output_field, or the builder shows 'Input array path is required'.
The loop body hangs off the `loop-body` handle; inside it read {{alias.current_item.data.<Col>}}.
When one item fails
continue_on_error is on by default, and that default is right for most batch work: one contact with a missing phone number should not stop the other forty-nine. The cost is that failures become quiet — the loop finishes reporting success while some items did nothing.
So check the collected results rather than assuming. If a nightly sweep is supposed to produce fifty tasks and produces forty-one, nothing in the run will have told you.
Doing it faster, and when not to
Turn on concurrent and the loop processes max_concurrent items at a time instead of one after another. On fifty API calls that is the difference between a workflow that takes a minute and one that takes seconds.
Leave it off when order matters, or when the body writes to the same record. Parallel iterations that update one contact will overwrite each other, and nothing in the log will look wrong. Leave it off too when the body calls an external service with a rate limit — twenty simultaneous requests is how you get throttled.
count mode, and stopping early
Switch loop_mode to count and the loop stops iterating a list and starts repeating an attempt. It runs up to max_iterations times — a hard cap, so the loop always terminates — and you stop it sooner with the break-when fields:
| Field | What it holds |
|---|---|
loop_mode | How the loop decides how many times to run. 'array' = iterate once per item in an array (for-each). 'count' = repeat up to max_iterations times (while / retry-until); combine with an early-break edge from a body condition node into the loop's Complete output to stop as soon as a condition is met. One of: array · count Defaults to array. |
max_iterations | Maximum number of attempts in 'count' mode — a hard safety cap so the loop always terminates even if the break condition never fires. Ignored in 'array' mode (the array length is the count). Defaults to 5. |
break_variable | Break-when: value/template to evaluate after each iteration, e.g. {{reviewer_1.approved}}. The loop stops when the comparison below is true. |
break_operator | Break-when: comparison operator (same set as the Condition node) One of: == · != · > · < · >= · <= · contains · not_contains · starts_with · ends_with · regex_match · is_empty · is_not_empty · in · not_in Defaults to is_not_empty. |
break_value | Break-when: target value to compare against (blank for is_empty / is_not_empty) |
loop_mode
- What it holds
- How the loop decides how many times to run. 'array' = iterate once per item in an array (for-each). 'count' = repeat up to max_iterations times (while / retry-until); combine with an early-break edge from a body condition node into the loop's Complete output to stop as soon as a condition is met. One of:
array · countDefaults toarray.
max_iterations
- What it holds
- Maximum number of attempts in 'count' mode — a hard safety cap so the loop always terminates even if the break condition never fires. Ignored in 'array' mode (the array length is the count). Defaults to
5.
break_variable
- What it holds
- Break-when: value/template to evaluate after each iteration, e.g. {{reviewer_1.approved}}. The loop stops when the comparison below is true.
break_operator
- What it holds
- Break-when: comparison operator (same set as the Condition node) One of:
== · != · > · < · >= · <= · contains · not_contains · starts_with · ends_with · regex_match · is_empty · is_not_empty · in · not_inDefaults tois_not_empty.
break_value
- What it holds
- Break-when: target value to compare against (blank for is_empty / is_not_empty)
The shape it exists for: call something that might not be ready, check whether it is, and go round again if not — with a ceiling, so a service that never answers cannot spin forever.
Try it
- Put a CRM Manager
searchnode in a workflow and point a Loop at its{{alias.results}}. - Hang one node off Loop Body that uses
{{loop_1.current_item}}, and one off Complete that does not. - Run it against a search that returns three records, and count how many times each arm fired.
- Now narrow the search so it returns nothing, and run it again. Notice that the body never runs and Complete still does — that is the arm that can tell you.
Next: Delay — for when the next step should happen later rather than immediately.
Related lessons
Base rates — what a piece of evidence is actually worth
A face-recognition system that is 99.9% accurate and almost entirely wrong, and a number that sent an innocent woman to prison. Both are the same arithmetic, and it is the arithmetic that decides what any piece of evidence is worth.
ReadConfirmation and survivorship — what you never looked for
Two questions about evidence you did not go looking for. One is a rule you have to discover, and one is a pattern in five famous people — and in both, the thing that would have told you the truth is the thing nobody checks.
ReadLoss aversion, sunk cost and regression — what it costs you
Four questions you answer about yourself rather than about a scenario, and your own answers are the finding. Then the pattern that makes praise look useless and criticism look like it works, whatever you actually do.
Read
