Узлы
Узлы — это строительные блоки воркфлоу Moira. Каждый узел представляет шаг в процессе со специфическим поведением, определяемым его типом.
Типы узлов
Ниже перечислены встроенные типы узлов Moira. Установленные расширения могут добавлять типы с
пространством имён, например extension-name.node-name. Live-реестр расширений передаёт их схемы
конфигурации валидатору, а каталог типов узлов Moira описывает их средству просмотра workflow.
Start
Точка входа для выполнения воркфлоу.
End
Терминальный узел, отмечающий завершение воркфлоу.
Agent Directive
Задача агента с директивой и условием завершения.
Condition
Ветвление выполнения на основе структурированных условий.
Expression
Вычисление значений с помощью арифметических выражений.
Subgraph
Делегирование другому воркфлоу.
User Notification
Отправка уведомления через все включённые каналы связи, настроенные текущим пользователем.
Telegram Notification (устаревшая)
Telegram-only нода совместимости для существующих provider-specific workflow.
Teleport
Цель перехода, доступная только через явный teleport.
Lock
PIN-блокировка с подтверждением через Telegram.
Materialize
Доставка файлов из реестра через пятиминутное разрешение, привязанное к ноде.
Start Node
Точка входа для выполнения воркфлоу. Каждый воркфлоу должен иметь ровно один start node.
{ "id": "start", "type": "start", "connections": { "default": "first-task" }}| Свойство | Обязательно | Описание |
|---|---|---|
id | Да | Соглашение: должен быть “start” |
type | Да | Должен быть "start" |
connections.default | Да | ID следующего узла |
Start узел заполняет глобальные переменные воркфлоу из значений default в variableRegistry. Объявляйте глобальные переменные в variableRegistry уровня воркфлоу, а не на start узле.
End Node
Терминальный узел, отмечающий завершение воркфлоу. Нет исходящих соединений.
{ "id": "end", "type": "end", "finalOutput": ["result", "summary"]}| Свойство | Обязательно | Описание |
|---|---|---|
id | Да | Соглашение: должен быть “end” |
type | Да | Должен быть "end" |
finalOutput | Нет | Ключи контекста для включения в финальный результат |
note: End узлы не имеют соединений — это терминальные узлы.
Agent Directive Node
Основной тип узла для задач агента. Содержит директиву (что делать) и условие завершения (когда готово).
{ "id": "analyze-requirements", "type": "agent-directive", "directive": "Проанализируй документ требований и определи ключевые фичи", "completionCondition": "Фичи перечислены с приоритетами", "inputSchema": { "type": "object", "globalInputs": ["analysis_done"], "properties": { "features": { "type": "array", "items": { "type": "string" } } }, "required": ["analysis_done", "features"] }, "connections": { "success": "next-step" }}| Свойство | Обязательно | Описание |
|---|---|---|
directive | Да | Что агент должен сделать |
completionCondition | Да | Когда шаг завершен |
inputSchema | Нет | JSON Schema для валидации ответа |
inputSchema.globalInputs | Нет | Имена глобальных переменных variableRegistry, которые пишет узел |
inputSchema.properties | Нет | Локальные выводы узла (используются как node-id.name) |
connections.success | Да | Следующий узел при успехе |
В примере analysis_done — объявленная глобальная переменная (должна существовать в variableRegistry), которую пишет этот узел и которая доступна в других местах как {{analysis_done}}; features — локальный вывод узла, доступный как {{analyze-requirements.features}}. Возвращённый ключ, не являющийся ни объявленной глобальной, ни описанным локальным выводом, отклоняется.
Используйте inputSchema для структурированных ответов. globalInputs перечисляет глобальные
переменные, которые пишет узел; properties описывает его локальные выводы. Движок автоматически
валидирует и маршрутизирует ответ.
Если ответ не соответствует inputSchema, движок записывает ошибку и снова приостанавливается на
том же узле. Сообщение объясняет ожидаемую схему и ошибки, но не повторяет отклонённый payload.
Per-node retry-поля из старых definitions поддерживаются только для совместимости и не ограничивают
и не перенаправляют этот цикл валидации; бизнес-логику retry и эскалации нужно явно моделировать в
графе workflow.
Condition Node
Ветвление выполнения на основе структурированных условий:
{ "id": "check-result", "type": "condition", "condition": { "operator": "eq", "left": { "contextPath": "status" }, "right": "success" }, "connections": { "true": "success-path", "false": "retry-step" }}| Свойство | Обязательно | Описание |
|---|---|---|
condition | Да | Объект структурированного условия |
connections.true | Да | Следующий узел когда условие true |
connections.false | Да | Следующий узел когда условие false |
Структурированные условия
Условия используют структурированный формат (не строковую оценку):
{ "operator": "and", "conditions": [ { "operator": "gt", "left": { "contextPath": "score" }, "right": 80 }, { "operator": "eq", "left": { "contextPath": "validated" }, "right": true } ]}Поддерживаемые операторы:
eq,neq— равно, не равноgt,gte,lt,lte— операторы сравненияcontains— вхождение в строку/массивexists— проверка существованияand,or,not— логические операторы
Expression Node
Вычисление значений с помощью арифметических выражений. Полезно для счётчиков, расчётов и трансформации переменных:
{ "id": "increment-counter", "type": "expression", "expressions": ["counter = counter + 1", "result = counter * multiplier"], "connections": { "default": "next-step", "error": "error-handler" }}| Свойство | Обязательно | Описание |
|---|---|---|
expressions | Да | Массив выражений присваивания |
connections.default | Да | Следующий узел после успешного вычисления |
connections.error | Нет | Следующий узел при ошибке вычисления |
Синтаксис выражений
Выражения поддерживают базовые арифметические операции:
- Арифметика:
+,-,*,/ - Скобки:
(a + b) * c - Присваивание:
result = a + b - Пути в контексте:
step.index,plan.items[0].value,tasks[current_index].action
{ "expressions": ["total = price * quantity", "tax = total * 0.1", "final_price = total + tax"]}Выражения вычисляются собственным изолированным парсером, а НЕ через JavaScript eval. Чтение использует только собственные свойства; постоянный или переменный индекс массива должен быть целым, неотрицательным и находиться в границах. Цель присваивания — безопасное простое имя. При наличии реестра присваивание обязано обращаться к объявленной глобальной переменной и соответствовать её JSON Schema; при ошибке нода ничего не публикует.
Обработка ошибок
Expression node может завершиться с ошибкой в двух случаях:
- Деление на ноль
- Некорректное, небезопасное, неразрешимое или выходящее за границы чтение пути
- Необъявленная цель присваивания или значение вне схемы реестра
При ошибке выполнение переходит к connection error если он определён, иначе воркфлоу завершается с ошибкой.
Subgraph Node
Делегирование выполнения другому воркфлоу:
{ "id": "run-tests", "type": "subgraph", "graphId": "test-workflow", "inputMapping": { "codeDir": "projectPath" }, "outputMapping": { "testResults": "results" }, "connections": { "success": "next-step" }}| Свойство | Обязательно | Описание |
|---|---|---|
graphId | Да | ID ссылаемого воркфлоу |
inputMapping | Да | Родительский контекст -> контекст подграфа |
outputMapping | Да | Контекст подграфа -> родительский контекст |
connections.success | Да | Следующий узел при успехе |
Подграфы обеспечивают композицию и повторное использование воркфлоу. Агент видит это как непрерывный воркфлоу.
connections.error необязателен и в текущем runtime не используется для исключений выполнения
дочернего workflow или ошибок mapping. Такие ошибки записываются, а выполнение приостанавливается
на subgraph node, чтобы агент исправил причину и повторил шаг.
User Notification Node
Отправляет сообщение через все корректные включённые каналы пользователя выполнения. Workflow не может выбирать provider, получателя или credential:
{ "id": "notify-complete", "type": "user-notification", "message": "Воркфлоу {{workflowName}} успешно завершен", "format": "markdown", "silent": false, "connections": { "default": "next-step", "error": "notification-failed" }}| Свойство | Обязательно | Описание |
|---|---|---|
message | Да | Текст уведомления с поддержкой шаблонов |
format | Нет | Переносимый формат: plain, markdown или html |
silent | Нет | Запрос тихой доставки, если provider её поддерживает |
attachProgressImage | Нет | Приложить текущий ограниченный PNG прогресса workflow |
attachment | Нет | Одно bounded base64-вложение image или document с именем/MIME |
connections.default | Да | Путь при полной, частичной доставке или отсутствии подходящих каналов |
connections.error | Нет | Путь при полном провале попыток; иначе используется default |
attachment и включённый attachProgressImage взаимоисключающие. Имя вложения должно
соответствовать безопасному переносимому allowlist, а MIME — форме type/subtype. До provider
delivery сервис применяет общие per-user/provider rate limits, ограничения concurrency пользователя
и provider, deadlines, пределы текста и байтов. Настроенный канал Telegram поддерживает текст,
PNG/JPEG-изображения и документы.
Результат ноды хранится под её ID. userNotificationStatus принимает delivered, partial,
no_configured_channels или all_failed; configuredChannels, deliveredChannels и channels
содержат счётчики и очищенные per-channel status/reason. Настроенный канал, который не поддерживает
тип запрошенного вложения, получает статус unsupported и пропускается; если другого подходящего
канала или ошибки проверки доступности нет, общий результат — no_configured_channels, а не
all_failed. Credentials, получатели, тело сообщения и байты вложения в результаты и ошибки не
попадают.
Устаревшая Telegram-specific нода
telegram-notification остаётся исполняемой для существующих workflow. Она отправляет только через
Telegram, а заданный chatId сохраняет точное provider-specific значение получателя. Для новых
обычных уведомлений используйте user-notification; переход explicit legacy recipient к generic
fan-out требует намеренной миграции workflow.
Teleport Node
Цель перехода, доступная только через явный teleport, а не через обычные соединения. Работает как agent-directive: приостанавливается для ввода и валидирует его схему, но достигается только по явному запросу агента.
{ "id": "teleport-replan", "type": "teleport", "directive": "Перепиши план разработки", "completionCondition": "Новый план создан и проверен", "hint": "Используй, когда текущий план нужно перестроить", "inputSchema": { "type": "object", "properties": { "reason": { "type": "string" } }, "required": ["reason"] }, "connections": { "success": "plan-node" }}| Свойство | Обязательно | Описание |
|---|---|---|
hint | Да | Понятное человеку описание, когда нужен teleport |
directive | Да | Инструкция, показанная агенту после teleport |
completionCondition | Да | Критерии успешного завершения шага teleport |
inputSchema | Нет | JSON Schema для валидации ответа агента |
connections.success | Да | Следующий узел после ввода для teleport |
connections.error | Нет | Узел обработки ошибки |
У teleport-узлов не должно быть входящих соединений от других узлов. При валидации они исключены из предупреждений о недостижимых узлах.
Использование Teleport во время выполнения
Если воркфлоу содержит teleport-узлы, их подсказки добавляются к каждому ответу шага в разделе
«Available Teleport Jumps». Для перехода передайте параметр teleportTo в step():
step({ processId: "abc123", attemptId: "attempt-current", teleportTo: "teleport-replan" })- Целью может быть только узел типа teleport
- Используйте идентификатор попытки шага из текущего предъявления
- Не передавайте
inputпри переходе: teleport-узел сначала покажет собственную директиву - Контекст выполнения сохраняется целиком
- После ввода для teleport-узла выполнение продолжается через
connections.success
Lock Node
PIN-блокировка выполнения. Она доставляет PIN в настроенный Telegram-чат текущего пользователя с inline-клавиатурой Approve и приостанавливает workflow только после успешной доставки.
{ "type": "lock", "id": "approval-gate", "reason": "Деплой на продакшен для {{workflow_name}}", "connections": { "unlocked": "proceed-node" }}| Свойство | Обязательно | Описание |
|---|---|---|
reason | Да | Причина блокировки (поддерживает шаблоны {{var}}) |
connections.unlocked | Да | Следующий узел после разблокировки |
Поведение:
- Для запуска любого workflow с lock node текущему пользователю нужны корректные bot token и chat ID. Ни
skipNotificationCheck, ни его устаревший aliasskipTelegramCheckне обходят это требование. - При первом посещении Moira сохраняет только хеш ожидающего доставки PIN, отправляет открытый PIN в настроенный chat, активирует блокировку, сохраняет
_lockIdи приостанавливает выполнение. - При отсутствующих или некорректных настройках и при ошибке доставки не возникает доступной активной блокировки или context reference. Повторное посещение запускает новую попытку доставки с новым PIN.
- Следующие посещения проверяют активную блокировку или введённый пользователем PIN и после разблокировки переходят через
connections.unlocked.
MCP-ответы и ответы workflow никогда не содержат сгенерированный PIN. Пользователь может передать PIN отдельно или подтвердить блокировку в Telegram.
Materialize Node
Доставляет объявленные автором файлы в файловую систему агента, удерживая их отрендеренное
содержимое вне контекста агента. Сервер создаёт пятиминутное разрешение на tar-загрузку, привязанное
к ноде; агент выполняет или повторяет точную команду из сгенерированной директивы и завершает шаг
значением null или {}. Хост, который не может выполнить эту команду, использует указанный в
директиве fallback-маршрут доставки в контекст: он расходует контекст и завершает шаг так же.
{ "id": "materialize-standards", "type": "materialize", "basePath": "{{workspace_path}}", "files": [ { "path": "standards/planning.md", "from": "planning_standards" }, { "path": "plans/.keep", "content": "" } ], "connections": { "success": "create-plan", "error": "materialize-failed" }}| Свойство | Обязательно | Описание |
|---|---|---|
basePath | Да | Результат рендеринга должен быть непустым и не содержать NUL |
files | Да | От 1 до 100 записей архива |
files[].path | Да | Безопасный шаблонизируемый путь относительно basePath |
files[].from | Одно из | Строковая запись variableRegistry, чей текущий default служит исходником |
files[].content | Одно из | Должен быть ровно ""; создаёт каркасный файл |
connections.success | Да | Следующая нода после пустого completion input |
connections.error | Нет | Путь при ошибке валидации, конфигурации или выдачи разрешения |
from и content взаимоисключающие. from должен ссылаться на объявленную строковую переменную
со строковым default; произвольный inline-текст отклоняется. При показе шага сервер рендерит
basePath и сводку путей файлов в директиве. Отрендеренный basePath становится частью выданной
команды. При HTTP-запросе архива сервер заново загружает текущее определение workflow и рендерит
каждый путь записи архива и содержимое из реестра с привязанным контекстом выполнения. Поэтому
изменение workflow после выдачи команды может изменить скачанные пути или содержимое, но не каталог
назначения в уже выданной команде. Повторный показ приостановленного шага использует актуальное
определение и выдаёт новые команду и разрешение вместо снимка, сохранённого при старте выполнения.
Сгенерированная POSIX-команда имеет следующую форму; все аргументы уже безопасно заключены в кавычки для shell:
mkdir -p -- '<basePath>' && curl -sSf -- '<reusable-url>' | tar -x -C '<basePath>'Копируйте выданную команду без изменений. Записи архива относительны к basePath; сам basePath
в tar не входит. Пути должны быть нормализованными, относительными, непустыми и уникальными после
рендеринга; запрещены пустые сегменты, ., .., абсолютный или начинающийся с обратной косой
черты путь и NUL. Лимиты: 100 файлов, 1 MiB UTF-8-содержимого на файл и 10 MiB несжатого содержимого
суммарно.
URL живёт пять минут и привязан к текущим пользователю, выполнению и ноде. Его можно скачивать
повторно, пока выполнение ожидает на этой ноде; сразу после перехода выполнения URL становится
недействительным. Сгенерированная директива объясняет окно повторов и напоминает, что доставка не
доказывает чтение; каждый последующий потребитель всё равно должен явно требовать чтения нужных ему
файлов. Вызов session({ action: "current_step" }) на паузе выдаёт свежий URL без продвижения графа.
Текстового fallback намеренно нет.
Полный контракт архива, путей, разрешений и ошибок, а также применение в Workflow Management Flow описаны в разделе Материализация файлов.
Ноды расширений
Установленные расширения добавляют типы в собственном пространстве имён, например
corporate-messenger.send. Манифест задаёт форму конфигурации и схемы валидации; браузер строит по
ним универсальный редактор и не загружает frontend-код расширения. Успешный результат сохраняется
под ID ноды. Ошибки входа, обработчика, срока, runner и выхода переходят по connections.error,
если эта связь есть; иначе Moira записывает диагностику и приостанавливается на ноде для повтора.
Живой каталог типов отличает недоступный runner от доступного реестра, в котором не установлено конкретное расширение. Контракты манифеста, SDK и исполнения описаны в Написании расширения.
Input Schema
Определение ожидаемой структуры ответа с использованием JSON Schema:
{ "inputSchema": { "type": "object", "properties": { "summary": { "type": "string", "description": "Краткое резюме находок" }, "items": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "priority": { "type": "number" } } } } }, "required": ["summary"] }}Автоматические типы узлов
Автоматические узлы выполняются на сервере без участия агента и сразу переходят к следующему узлу. Они используются для операций с постоянными заметками.
Read Note Node
Читает заметки по фильтру в переменную контекста:
{ "type": "read-note", "id": "load-notes", "outputVariable": "projectNotes", "filter": { "tag": "{{projectTag}}", "keyPattern": "project-" }, "singleMode": false, "connections": { "default": "next-node", "error": "error-handler" }}| Свойство | Обязательно | Описание |
|---|---|---|
outputVariable | Да | Переменная контекста для результатов |
filter.tag | Нет | Фильтр по точному тегу |
filter.keyPattern | Нет | Фильтр по префиксу ключа |
filter.keySearch | Нет | Поиск подстроки в ключе |
singleMode | Нет | Вернуть объект вместо массива |
connections.error | Нет | Узел обработки ошибки |
Write Note Node
Записывает данные из контекста в заметки:
{ "type": "write-note", "id": "save-results", "key": "results-{{timestamp}}", "source": "analysisResults", "tags": ["analysis"], "connections": { "default": "next-node" }}| Свойство | Обязательно | Описание |
|---|---|---|
key | Нет* | Ключ заметки, обязательный в одиночном режиме |
source | Да | Переменная контекста со значением |
tags | Нет | Назначаемые теги |
batchMode | Нет | Обрабатывать массив заметок |
Upsert Note Node
Ищет существующую заметку или создаёт новую:
{ "type": "upsert-note", "id": "upsert-config", "search": { "tag": "config" }, "keyTemplate": "{{projectId}}-config", "value": "configData", "connections": { "default": "next-node" }}| Свойство | Обязательно | Описание |
|---|---|---|
search.tag | Нет | Поиск по тегу |
search.keyPattern | Нет | Поиск по префиксу ключа |
keyTemplate | Да | Ключ новой заметки, если она не найдена |
value | Да | Переменная контекста со значением |
Все параметры фильтра и ключа поддерживают шаблонные выражения {{ variable }}.
Лучшие практики
- Начинайте со start — Каждый воркфлоу должен иметь ровно один start узел
- Заканчивайте end — Используйте end узлы для отметки точек завершения
- Четкие директивы — Будьте конкретны в том, что агент должен сделать
- Проверяемые условия — Условия завершения должны быть объективно измеримы
- Валидация схемы — Используйте
inputSchemaдля структурированных ответов - Пути ошибок — Используйте error connection только там, где его маршрутизирует документированное поведение конкретного типа узла