Циклы и итерации

Повтор во Flow не рисуется стрелкой назад. Он выражается четырьмя нодами-оболочками — Цикл While, Для каждого, Маппинг и Фильтр, — внутри которых лежит тело: обычные ноды, которые оболочка запускает столько раз, сколько нужно. Все четыре лежат в палитре в разделе Logic & Flow, группа Iteration, и доступны на бесплатном тарифе.

Ребро назад — не цикл, а ошибка

Движок не идёт по нодам сверху вниз. Он считает входящие зависимости: нода стартует, когда все её входы разрешились (см. модель графа). Ребро из ноды, которая идёт позже, обратно в ноду, которая идёт раньше, означает «эта нода ждёт саму себя»: входящая зависимость не разрешится никогда, и весь участок графа встанет.

Поэтому такой граф отбивается до запуска — валидатор выдаёт ошибку graph.cycle и называет всю замкнутую цепочку целиком. Если петля проходит через барьер Дождаться всех, код будет graph.barrier_unsatisfiable — смысл тот же: барьер ждёт вход, который лежит ниже его самого. Полный список кодов — на странице коды валидации.

Оболочка и тело

У всех четырёх нод одна конструкция: два порта размечают границу тела.

  • выход Start (у массивных нод он называется Item) — начало тела;
  • вход End — конец тела;
  • выход Done срабатывает один раз, когда повторы закончились. Всё, что идёт дальше по графу, вешайте именно на него.

Телом считаются все ноды на путях между Start и End. Обе ноги обязательны: без ребра из Start валидатор выдаёт subgraph.missing_entry_edge, без ребра в Endsubgraph.missing_exit_edge.

Вход
Цикл While
Выход
Тело: опрос статуса
  • Исполнение + данные
  • Исполнение
While Loop: тело подключено с двух сторон, продолжение графа висит на Done. Серая связь — чистый exec: порядок без значения.

Ребро из тела обратно в End выглядит как та самая «стрелка назад», но ею не является: границы оболочки исключены из подсчёта зависимостей, и планировщик их не видит. Именно поэтому цикл разрешён только в такой форме — движку нужно знать, где тело начинается и где заканчивается, чтобы прогонять его отдельным вложенным проходом.

Четыре ноды: что выбрать

Нода Сколько проходов Что приходит в тело Результат ноды
Цикл While пока условие истинно ничего: Start — чистый exec значение, пришедшее на её вход
Для каждого по элементу массива элемент число обработанных элементов
Маппинг по элементу массива элемент массив значений, собранных с End
Фильтр по элементу массива элемент исходные элементы, для которых тело вернуло true

Разница принципиальная. Цикл While и Для каждого повторяют работу: их результат — не данные тела, а факт «прошли столько-то раз». Маппинг и Фильтр преобразуют список: они собирают то, что тело положило на End, и порядок выходного массива совпадает с входным.

Вложение оболочек запрещено: цикл внутри цикла отвергается ошибкой subgraph.nested_iteration_shell — вложенная оболочка не смогла бы получить старт и запуск завис бы. Ставьте циклы последовательно (Done первого → вход второго) или переносите внутренний обход в одну ноду JavaScript-код.

While Loop: условие и счётчик

Условие — шаблонное выражение, оно вычисляется перед каждым проходом, включая первый. Ложно сразу — тело не выполнится ни разу, а Done всё равно сработает. Тело обязано менять то, что читает условие: значения между проходами переносят переменные сессии (нода Установить переменную) или выходы нод тела, {{ nodes.<id>.output }}.

Внутри цикла доступна служебная переменная {{ variables.loop_index }} — номер прохода, считая с нуля; после выхода из цикла она исчезает. Считать по ней повторы — ловушка, на которую наступает каждый: переменная выставляется после проверки условия, поэтому на самой первой проверке её ещё не существует. А условие, прочитавшее несуществующее значение, ноду не роняет — оно просто становится ложным. Поэтому {{ variables.loop_index < 2 }} даёт ноль проходов, а не три: тело не выполняется ни разу, Done срабатывает сразу, запуск считается успешным.

Рабочая форма — с фильтром default, который подставляет значение на первой проверке: {{ variables.loop_index | default(-1) < 2 }} даёт три прохода, с индексами 0, 1 и 2. Правило общее и касается любого значения, которое появляется только внутри тела: на первой проверке его ещё нет, поэтому либо пишите | default(...), либо инициализируйте переменную нодой Установить переменную до входа в цикл. И помните про сдвиг на шаг: начиная со второй проверки условие видит индекс уже завершённого прохода, а не будущего — если число повторов должно быть точным, надёжнее задать его полем «Макс. итераций», чем арифметикой в условии.

Число проходов ограничено дважды: полем ноды и потолком движка — по умолчанию 100 проходов, тариф может изменить это значение. Значение в поле ноды может только опустить потолок, а не поднять его выше тарифного.

Массивы: For each, Map, Filter

На вход этих трёх нод подаётся JSON-массив (или строка, в которой записан массив). Объект {"items": [...]} массивом не является — нода упадёт с сообщением «expected a list». Между входом и оболочкой обычно ставят Шаблон с выражением {{ inputs.input.items | tojson }}; валидатор предупреждает об этой проводке заранее кодом array.input_shape, когда массив берётся прямо из Входа.

Вход
Достать массив
Маппинг
Выход
Тело: преобразование
  • Исполнение + данные
Map: элемент уходит в тело по порту Item, преобразованное значение возвращается в End, собранный массив — на Done.

Элемент приезжает в тело обычным значением ребра и читается как {{ inputs.<входной порт> }} — для большинства нод это {{ inputs.input }}. Переключатель «Включить индекс» меняет форму: вместо голого элемента на Item уходит объект {<ключ>: элемент, index: N}, и в теле это {{ inputs.input.item }} и {{ inputs.input.index }}. Имя ключа задаётся соседним полем «Ключ элемента».

  • Для каждого значение с End игнорирует — нода нужна ради побочного действия: письмо на адрес, строка в таблицу, сообщение в чат. Её результат — количество элементов.
  • Маппинг кладёт в выходной массив то, что пришло на End, как есть.
  • Фильтр требует на End строго логическое значение: true оставляет исходный элемент, false выбрасывает. Любое другое значение — ошибка ноды, а не «считаем истиной».

{{ loop.* }} — зарезервировано и пусто

В шаблонах есть пространство имён loop, и оно занято движком, но сегодня его не заполняет ни одна нода: {{ loop.item }} внутри тела отрендерится пустой строкой. Элемент читается с порта ({{ inputs.input }}), номер прохода в Цикл While — из {{ variables.loop_index }}. Полный список пространств имён — на странице шаблоны.

Что видно в отчёте о запуске

Каждый проход тела попадает в отчёт отдельной группой — «Итерация 1», «Итерация 2» и так далее, с входами и выходами всех нод тела на этом проходе. Снаружи цикла {{ nodes.<нода тела>.output }} содержит значение последнего прохода: пространство имён одно на все итерации, и каждая следующая перезаписывает предыдущую.

Если тело не выполнилось ни разу — пустой массив, условие ложно с самого начала — ноды тела попадают в отчёт со статусом «пропущена» и причиной loop_body_not_entered. Причина отдельная не для красоты: у ноды тела нет ни одной обычной входящей зависимости (границы оболочки в подсчёт не входят), и без этой ветки классификатора она была бы помечена «нечем запустить» — то есть выглядела бы как дефект проводки там, где проводка верна.

app.iterna.ai

Запуски

Выполнение нод

НодаСтатусПояснениеВремя
for_each_1завершена0 элементов0.01s
log_1пропущенаТело цикла ни разу не выполнялось
exit_1завершена0.00s
Отчёт о запуске: цикл дошёл до Done, но тело не выполнялось ни разу.

Сам запуск при этом успешен: невыполненное тело — это не отказ, а исход. Подробности — статусы запуска.

Пределы

  • Проходы While Loop — по умолчанию 100, значение тарифное.
  • Длина массива для Для каждого, Маппинг и Фильтр — 10 000 элементов; более длинный массив не обрезается, а роняет ноду.
  • Время запуска — общий потолок в секундах: цикл проверяет его перед каждым проходом, поэтому долгий цикл останавливается ошибкой бюджета, а не висит.
  • Свой таймаут ноды у оболочек не работает по замыслу: их время — сумма времён нод тела, каждая из которых уже ограничена. Подробности — таймауты и бюджеты.

Что дальше