Циклы и итерации
Повтор во Flow не рисуется стрелкой назад. Он выражается четырьмя нодами-оболочками —
Цикл While, Для каждого, Маппинг и Фильтр, — внутри которых лежит тело: обычные
ноды, которые оболочка запускает столько раз, сколько нужно. Все четыре лежат в палитре в
разделе Logic & Flow, группа Iteration, и доступны на бесплатном тарифе.
Ребро назад — не цикл, а ошибка
Движок не идёт по нодам сверху вниз. Он считает входящие зависимости: нода стартует, когда все её входы разрешились (см. модель графа). Ребро из ноды, которая идёт позже, обратно в ноду, которая идёт раньше, означает «эта нода ждёт саму себя»: входящая зависимость не разрешится никогда, и весь участок графа встанет.
Поэтому такой граф отбивается до запуска — валидатор выдаёт ошибку graph.cycle и называет
всю замкнутую цепочку целиком. Если петля проходит через барьер Дождаться всех, код будет
graph.barrier_unsatisfiable — смысл тот же: барьер ждёт вход, который лежит ниже его самого.
Полный список кодов — на странице коды валидации.
Оболочка и тело
У всех четырёх нод одна конструкция: два порта размечают границу тела.
- выход
Start(у массивных нод он называетсяItem) — начало тела; - вход
End— конец тела; - выход
Doneсрабатывает один раз, когда повторы закончились. Всё, что идёт дальше по графу, вешайте именно на него.
Телом считаются все ноды на путях между Start и End. Обе ноги обязательны: без ребра из
Start валидатор выдаёт subgraph.missing_entry_edge, без ребра в End —
subgraph.missing_exit_edge.
- Исполнение + данные
- Исполнение
Ребро из тела обратно в 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, когда массив берётся прямо из Входа.
- Исполнение + данные
Элемент приезжает в тело обычным значением ребра и читается как {{ 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. Причина
отдельная не для красоты: у ноды тела нет ни одной обычной входящей зависимости (границы
оболочки в подсчёт не входят), и без этой ветки классификатора она была бы помечена «нечем
запустить» — то есть выглядела бы как дефект проводки там, где проводка верна.
Запуски
Выполнение нод
| Нода | Статус | Пояснение | Время |
|---|---|---|---|
| for_each_1 | завершена | 0 элементов | 0.01s |
| log_1 | пропущена | Тело цикла ни разу не выполнялось | — |
| exit_1 | завершена | 0.00s |
Сам запуск при этом успешен: невыполненное тело — это не отказ, а исход. Подробности — статусы запуска.
Пределы
- Проходы While Loop — по умолчанию 100, значение тарифное.
- Длина массива для Для каждого, Маппинг и Фильтр — 10 000 элементов; более длинный массив не обрезается, а роняет ноду.
- Время запуска — общий потолок в секундах: цикл проверяет его перед каждым проходом, поэтому долгий цикл останавливается ошибкой бюджета, а не висит.
- Свой таймаут ноды у оболочек не работает по замыслу: их время — сумма времён нод тела, каждая из которых уже ограничена. Подробности — таймауты и бюджеты.