Guide 09 · Developer docs

Automation, jobs and integration

A job is a trigger, conditions and an ordered list of actions, set up under Customization › Orchestration in the admin; jobs were called hooks before the module was renamed, and the API's routes keep that name. What starts a job, what it can do, branches and loops, email, scheduled work, integrations with other systems, and every event and action.

L2–L3
Unit 04 · Branches, loops and re-runs

Loops and failing iterations

A loop runs its actions once per element of a list. Where the list comes from, what each iteration can read, the settings that decide how it runs and waits, the time limit, and what happens when one iteration fails: contain it and carry on, or abort.

Who this is for. Anyone whose job works through a set of records or rows. Afterwards you can build a loop that one bad row cannot take down, and choose when it should stop instead.

Where the list comes from

Loop sourceThe elements are
ActionThe records an earlier Retrieve Records found, or a list in an earlier HTTP Request's answer, found at a Property Path such as data.items
Web EventA list in the web request's body, at a property path; the whole body when the path is empty
Site DataA list stored as JSON under a site data key
Template codeWhatever the template adds with loopHelper; its printed output is not part of the list

A template source adds arrays, strings or numbers with addElement() and addElements(), and records with addRecordById(module, entity, id) or addRecordsByIds(module, entity, ids), such as addRecordById('Products', 'Product', 12). Here the elements come from a comma-separated list in site data:

Template code
{* Loop source: Template code *}
{foreach explode(',', $siteData->get('warehouse-codes')) as $code}
  {$loopHelper->addElement(['code' => trim($code)])}
{/foreach}
{# Loop source: Template code #}
{% for code in siteData.get('warehouse-codes')|split(',') %}
  {% do loopHelper.addElement({'code': code|trim}) %}
{% endfor %}

An empty list is not an error: the loop is skipped with No loop elements found and the job carries on.

What each iteration can read

Inside the loop, every action sees the run's variables plus loopRecord and loopElement. loopRecord is the record, when the element is one (a Retrieve Records source, or addRecordById), and the element's data otherwise: {$loopRecord.code}, {{ loopRecord.code }}. An Update Record or Delete Record inside the loop can take Loop Record as its source. Results of actions inside the loop belong to their iteration: runData.get() returns the current iteration's.

How it runs: the four settings

Setting (admin label)OptionsEffect
Processing typeParallel, SequentialParallel hands each element to the background workers, several at once. Sequential runs them one after another inside the run. A synchronous job not started by an entity event always runs its loops sequentially
Wait for resultWait for success, Wait for finish, Don't waitWhether the actions after the loop wait for it: only if every element succeeded, whatever the elements did, or not at all
Retries0 to 5Parallel only: how many times a failed element is run again, whole, from its first action
Stop loop execution on errorOn, offWhether elements not yet started are skipped once one has failed

The limit is time, not count

There is no limit on the number of elements. While a parallel loop runs with a wait setting, the run is waiting; it checks every ten seconds whether the elements are done. After eight hours from the loop's start it gives up: the loop is marked as an error, Giving up on loop resume check as max wait time … has been exceeded, and the actions after it never run. A loop of a few thousand elements that each make an HTTP request is where this is met; the answer is fewer elements per run (Chunking), not a longer wait.

A sequential loop has no such check, but runs inside one worker or one request for as long as it takes, so the same advice applies.

When one iteration fails

A failed action inside the loop fails its element, with the message in the element's row. What happens next is the two settings together, and the two useful shapes are both correct, for different jobs:

  • Contain and continue: Stop loop execution on error off, Wait for finish. Each element stands alone: pushing a thousand contacts to a CRM, where one malformed address must not stop the other 999. The actions after the loop still run; end with one that reports the failures, which are in the run log with their messages.
  • Abort: Stop loop execution on error on, Wait for success, processing Sequential. Use it when later rows depend on earlier ones: a parent before its children, ledger entries in order, anything where carrying on past a failure produces wrong data rather than missing data. The first failure means the remaining elements never run, and fails the run, so nothing after the loop runs on a half-applied set.

To skip one element deliberately, without an error, put a Stop with status Skipped in a condition inside the loop: it ends that element only (Stopping a job early). Retries in a parallel loop re-run an element from its first action, so its actions must be safe to repeat (Idempotency and replay).

Go deeper

Was this page helpful?

Your answer goes to the docs team.