Модель графа

Воркфлоу во Flow — это направленный граф: объект с двумя списками, nodes и edges. Ноды делают работу, рёбра решают, что запускается следом и какое значение туда приезжает.

Вход
Ответ LLM
LLM
Выход
  • Исполнение + данные
  • 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, а не ручной петлёй.

Что дальше