Модель графа
Воркфлоу во Flow — это направленный граф: объект с двумя списками, nodes и edges.
Ноды делают работу, рёбра решают, что запускается следом и какое значение туда приезжает.
- Исполнение + данные
- LLM
- Исполнение + данные + стрим
Нода
У ноды есть идентификатор, тип из реестра, позиция на холсте и конфигурация:
{
"id": "llm_response_1",
"type": "llm_response",
"position": { "x": 320, "y": 80 },
"data": { "label": "Ответ", "config": { "prompt_template": "..." } }
}
Тип определяет всё остальное: набор портов, поля конфигурации, бейджи на карточке. Полный список — в справочнике нод.
Ребро
Ребро соединяет порт источника с портом цели:
{
"id": "e1",
"source": "entry_1", "target": "llm_response_1",
"sourceHandle": "output", "targetHandle": "message",
"edgeType": "execute_data"
}
sourceHandle и targetHandle — идентификаторы портов, которые нода рисует при своей
текущей конфигурации: часть портов появляется и исчезает от переключателей (например,
on_error виден, только когда в конфигурации ноды включён вывод ошибки). edgeType —
тип провода; редактор выводит его из соединённых портов, подробности на странице
провода.
Порты
Порт — это именованная точка подключения. У него две независимые характеристики: провод (несёт ли ребро запуск, значение, поток токенов) и тип данных (строка, объект, файл, таблица — см. типы портов).
Большинство нод имеют один совмещённый выход output: он и запускает следующую ноду, и
передаёт ей значение. Переключатели «Разделить входные порты» и «Разделить выходные
порты» разводят такой порт на две ноги — <порт>_exec и <порт>_data, — когда порядок
и значение должны идти по разным маршрутам.
Вход и выход
Нода entry — единственная точка входа, и она обязана быть ровно одна. У неё нет входа исполнения и есть два выхода:
output— сообщение пользователя (строка) для чата, Telegram и вебхука; при запуске с произвольным JSON-объектом сюда приезжает весь объект целиком;data— контекст триггера: тип запуска, идентификаторы сессии и воркфлоу, блок Telegram, полезная нагрузка вебхука.
Нода exit возвращает результат пользователю или в ответ API. Нужна
хотя бы одна; вход end принимает финальное значение, необязательный вход files
отдаёт вместе с ответом файлы. Поле «Выражение вывода» в её конфигурации, если оно
непустое, заменяет пришедшее на end значение.
Как движок решает, что запускать
Здесь Flow расходится с интуицией «блок-схемы». Движок не сортирует граф топологически и не идёт по нодам сверху вниз. Он сеет только точку входа (и точки входа вложенных подграфов), а дальше работает по событиям: завершилась нода — каждое её исходящее ребро либо активирует цель, либо объявляется мёртвым.
- Нода стартует, когда сработали все её входящие зависимости. Независимые ветки при этом идут параллельно, а не по очереди.
- Если часть входов мертва (ветка
ifпошла в другую сторону), а остальные сработали, нода всё равно запускается — с полезной нагрузкой той ветки, что дошла. - Если мертвы все входы, нода не выполняется: в отчёте о запуске у неё статус «пропущена»
и причина —
branch_not_taken,upstream_failed,upstream_not_runи подобные.
Из этого следуют два практических правила. Первое: нода без активирующего входа не
запустится сама по себе, даже если у неё «нечего ждать». Второе: обратное ребро «наверх»
не делает цикл, а вешает планировщик — такой граф отбивается валидацией с кодом
graph.cycle. Повторение выражается нодами while_loop и
for_each, а не ручной петлёй.