Skip to content
Expedify
5 min

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

`array` is the default and the one most workflows want.

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_iterations times, 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

Array mode. The first three are not optional in practice — see the warning below.

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.

Loop BodyComplete

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:

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

the loop body's json_data
{
  "title": "Follow up with {{loop_1.current_item.first_name}}",
  "entity_type": "contact",
  "entity_id": "{{loop_1.current_item.id}}"
}
Written once, resolved fresh for every contact in the list.

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:

Break-when uses the same operator set as the Condition node.

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 · count Defaults to array.

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_in Defaults to is_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

  1. Put a CRM Manager search node in a workflow and point a Loop at its {{alias.results}}.
  2. Hang one node off Loop Body that uses {{loop_1.current_item}}, and one off Complete that does not.
  3. Run it against a search that returns three records, and count how many times each arm fired.
  4. 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