Узлы-действия: что умеет каждый
Справочник по всем девяти действиям, которые можно положить на холст сценария. Для каждого — что делает, когда брать и что вписать в поля.
Общий порядок работы: «Добавить узел» → «Действие», в панели справа выбрать «Тип действия» и заполнить поля. Правки сразу попадают в схему — не забудьте нажать «Сохранить черновик».

Поля, которые есть у любого действия
- При ошибке — «Продолжить» или «Остановить». По умолчанию сбой шага не останавливает сценарий (например, письмо не ушло — а страница «Спасибо» посетителю всё равно покажется). «Остановить» — когда следующий шаг без этого бессмыслен.
- Запустить в фоне (не ждать) — для медленных внешних сервисов: посетитель получает ответ сразу, шаг доделывается в фоне. Работает у «Отправить email», «HTTP POST» и «Уведомление в Telegram».
Какие данные доступны в текстовых полях
Почти любое текстовое поле действия — это шаблон, куда можно подставлять данные. Основные источники:
| Запись | Откуда данные |
|---|---|
{{ form.имя }} |
Поля отправленной формы (у формы на сайте имена полей — name, email…) |
{{ json.имя }} |
Тело JSON-запроса (когда пишет программа, а не человек) |
{{ query.имя }} |
Параметры из адреса страницы (?utm_source=…). Только в GET-сценарии: у отправки формы (POST) тело раскладывается по form.* / json.* / xml.*, а query не подставляется |
{{ params.имя }} |
Куски пути маршрута: для /blog/:slug это {{ params.slug }} |
{{ results.ключ… }} |
Ответы предыдущих GraphQL-узлов — по их «Ключу результата» |
{{ member.поле }} |
Залогиненный участник сайта (id, email…) |
{{ headers.имя }} |
Заголовки запроса (для интеграций по API) |
Два языка: Liquid для шаблонов, expr для вычислений
Текстовые поля (таблица выше) — это шаблоны Liquid: подстановка {{ … }}
и логика {% … %}. Кроме них у узлов есть поля-выражения — они узнаются
по режиму «Выражение (expr-lang)» и пишутся без {{ }}. Разграничение
простое: документы шаблонируются в Liquid, значения считаются в expr.
| Поле-выражение | Где | Что считается |
|---|---|---|
| Выражение | узел «Вычисление» | Любое значение — попадает в results.ключ для следующих узлов |
| Тело (выражение) | «JSON-ответ», «HTTP POST» | Готовое тело: значение кодируется в JSON как данные |
| Переменные (выражение) | «GraphQL» | Вся карта переменных одним выражением |
| Логическое выражение | «Условие» | true/false — выбор ветки «да»/«нет» |
В конфиге сценария (и в экспорте .dynflow.json) эти поля называются
compute.expr, body_expr, variables_expr и predicate_expr — по ним
удобно искать.
Выражение видит те же данные, что и шаблоны (form.*, json.*, query.*,
results.*, member.*, params.*, headers.*…), читает секреты проекта
через secret("имя") и имеет хелперы:
| Хелпер | Что делает |
|---|---|
number(v) |
Строго приводит строку к числу: "990.50" → 990.5; мусор → nil (не 0) |
digits(v) |
Оставляет только цифры: "+7 (900) 123-45-67" → "79001234567" |
money(v) / money(v, n) |
Строка с фиксированной дробной частью: 990.5 → "990.50" |
base64_encode(v) / base64_decode(v) |
Кодирует и раскодирует base64 |
uuid() / now() |
Свежий UUID; текущее время в Unix-секундах |
trim(v) |
Обрезает пробелы по краям строки (в шаблонах Liquid то же делает фильтр strip; в выражениях его нет) |
stringmap(v) |
Мост типов: карту, которую JSON-кодировщик не принимает (такую возвращает fromPairs), превращает в обычную JSON-карту — рекурсивно; ключи обязаны быть строками, не-строковый ключ — видимая ошибка; nil-значения сохраняются |
Либо выражение считается, либо сбой узла с объяснением попадает в журнал
прогонов — тихих нулей не бывает. Сохранение проверяет компиляцию:
выражение, которое не компилируется, сценарий не сохранит (ошибка
FLOW_EXPR_INVALID назовёт узел и поле).
GraphQL — взять данные из CMS
Когда брать: странице нужны записи — каталог, блог, список сотрудников.
- Источник запроса — «Из библиотеки» (повторно используемый запрос из вкладки «Библиотека запросов») или «Вручную» (запрос пишется прямо в узле).
- Запрос — что взять. Пример:
query { services(limit: 50) { id title price } }. Текст запроса — статичный GraphQL: подстановки{{ }}внутри него больше не рендерятся (Liquid из текста запроса удалён из движка в v1.4.16), а запрос со вставками не пройдёт проверку. Значения едут переменными — «Переменные (JSON)» и «Переменные (выражение)» ниже. - Переменные (JSON) — значения для запроса-с параметрами; каждое строковое
значение тоже обрабатывается как шаблон (можно подставить
{{ form.email }}). - Переменные (выражение) — необязательная альтернатива: вся карта
переменных считается одним выражением expr, например
{q: form.email, id: member.id}. Если заполнена — заменяет «Переменные (JSON)» целиком. - Ключ результата — имя, под которым ответ станет доступен следующим узлам
и шаблону:
results.ключ. Для ключаcatalogсписок услуг в шаблоне лежит вcatalog.data.services. - Секретные переменные — имена полей формы (по одному), значения которых не должны сохраняться в «Отправках» (пароли). Сценарий их видит, а хранилище — нет.

Рендер страницы — показать HTML
Когда брать: сценарий отвечает за страницу (вместе с маршрутом GET).
- Шаблон — файл из вкладки «Файлы» (например,
katalog.liquid). Общую обёртку сайта (шапку, подвал,<head>) шаблон подключает сам — как устроена база, разобрано в статье «Файлы сайта».
Одна и та же страница работает и как ответ на форму. В демо-проекте у «Заявки» (форма на /contacts) последняя карточка — узел «Рендер страницы» с шаблоном spasibo.liquid: посетитель отправил форму — и вместо перезагрузки видит собранную этим шаблоном страницу «Спасибо». Сам шаблон лежит во вкладке «Файлы»; страница не существует сама по себе — её каждый раз рисует узел.
JSON-ответ — ответить данными
Когда брать: адрес на сайте нужен программе, а не человеку (API для приложения или партнёра).
- Тело (шаблон) — статичный JSON-текст. Подстановок внутри больше нет:
Liquid из этого поля удалён из движка (v1.4.16), ключ остался для старых
сценариев и уходит как есть — вставьте в него
{{ … }}, и она доедет до клиента буквально. Вычисляемое тело собирайте режимом «Тело (выражение)» ниже. - Статус — код ответа (по умолчанию 200).
- Тело (выражение) — режим «Выражение (expr-lang)»: тело считает выражение expr, и значение кодируется в JSON как данные — числа остаются числами, массивы массивами, а невалидный JSON невозможен в принципе.
Как собрать JSON-тело из полей формы
Для поля «Тело (шаблон)» узла «HTTP POST». Тело — это шаблон, который должен превратиться в корректный JSON. Три правила:
Наследие двух движков. У узла «JSON-ответ» подстановки в «Теле (шаблон)» удалены из движка (v1.4.16): поле стало статичной строкой, и эти рецепты к нему больше не применяются — вычисляемое тело собирает режим «Выражение (expr-lang)» (см. «Тело (выражение)» выше и пример «до/после» в конце раздела). У «HTTP POST» шаблонное тело по-прежнему работает и поддерживается — для новых сценариев там тоже есть режим «Выражение (expr-lang)».
1. Каждое значение — через фильтр json. Он сам ставит кавычки и
экранирует кавычки и переносы внутри значения. Без него фамилия с
кавычкой сломает весь JSON.
2. Необязательные поля — с запятой внутри {% if %} — тогда
пропущенное поле не оставит висячей запятой.
3. Осторожно с blank. Проверка {% if form.lastName != blank %}
означает лишь «поле пришло»: пустая строка и строка из пробелов пройдут
её, и в CRM уедет "lastName":" ". Правильный рецепт — обрезать
пробелы и сравнить с пустой строкой:
{% assign ln = form.lastName | strip %}
{% if ln != "" %},"lastName":{{ ln | json }}{% endif %}
Присвоение обязательно: фильтр внутри самого условия
({% if form.x | strip != "" %}) — синтаксическая ошибка.
Готовый пример контакта для CRM:
{%- assign fn = form.firstName | strip -%}
{%- assign ln = form.lastName | strip -%}
{%- assign ph = form.phone | strip | digits -%}
{%- assign em = form.email | strip -%}
{"firstName":{{ fn | json }}{% if ln != "" %},"lastName":{{ ln | json }}{% endif %}{% if ph != "" %},"phones":[{{ ph | json }}]{% endif %}{% if em != "" %},"emails":[{{ em | json }}]{% endif %}}
Всё пустое (поле не пришло, пустое, из одних пробелов) пропускается, телефон очищается до цифр, кавычки внутри значений экранируются.
То же тело одним выражением
Тот же контакт для CRM в режиме «Выражение (expr-lang)»:
let fn = form.firstName != nil ? string(form.firstName) : "";
let ln = form.lastName != nil ? string(form.lastName) : "";
let ph = digits(form.phone);
{ "firstName": fn,
"lastName": ln != "" ? ln : nil,
"phones": ph != "" ? [ph] : nil }
Числа остаются числами, массивы — массивами: без экранирования, без запятых
внутри {% if %}, без ловушки blank. Пустые поля ведут себя иначе, чем в
Liquid-рецепте: незаполненное поле попадает в JSON как "lastName": null —
тело всегда валидное, но ключ остаётся, со значением null (тернарник
ln != "" ? ln : nil не выбрасывает ключ, а превращает пустую строку в
null — без него пустое поле уехало бы как ""). Большинство API принимает
null спокойно; если внешний API требует именно ОТСУТСТВИЯ ключа, разведите
тела по веткам узла «Условие»: ветка «есть lastName» ведёт к узлу с телом,
где фамилия есть, ветка «нет» — к телу без фамилии. Слить всё в одно
выражение не выйдет: оператор + над картами в expr не поддерживается (это
ошибка компиляции ещё при сохранении). let связывает одно имя за раз;
цепочка — let a = …;, let b = …;, затем финальное выражение. Значение
кодируется как данные: на JSON-запросе с "productId": 5 выражение
{"productId": form.productId} пошлёт число 5 — без трюков с plus: 0
и без сюрприза в виде строки "5".
Частичное обновление: передать только заполненные поля
Частая задача правки: клиент прислал часть полей, и пустые не должны затереть
старые значения. Режим «Переменные (выражение)» собирает такую карту целиком
(живой пример — сценарий обновления профиля: статичная мутация
mutation($id: ID!, $input: MemberUpdateInput!), вся карта $input — одно
выражение):
stringmap(fromPairs(map(filter(toPairs(json),
#[1] != nil && (type(#[1]) != "string" || trim(#[1]) != "")),
[#[0], type(#[1]) == "string" ? trim(#[1]) : #[1]])))
Поля берутся из разобранного JSON-тела запроса (json.*). Что попадёт в
карту, по каждому json.поле:
| Пришло | В карту |
|---|---|
| поля нет | пропущено — его нет в toPairs(json) |
null |
пропущено — #[1] != nil не пускает |
"" или строка из пробелов |
пропущено — после trim не остаётся ничего |
"42" (число строкой) |
строкой как есть: stringmap не конвертирует типы — число сделает number(#[1]) в паре или разбор на получателе |
| строка / число / логическое / массив / объект | как есть |
Как это работает: filter отбрасывает пустое, map собирает пары обратно,
fromPairs склеивает карту — но в «сыром» виде, который JSON-кодировщик не
принимает («unsupported type»), поэтому всё завёрнуто в stringmap: он
делает карту обычной — рекурсивно, а не-строковый ключ останавливает узел с
видимой ошибкой. Расширяйте условие filter, чтобы отбрасывать больше форм;
сам stringmap ничего не выбрасывает и не перекодирует.
Полезные фильтры для тела
| Фильтр | Пример | Что делает |
|---|---|---|
json |
{{ form.name | json }} |
Значение → корректный JSON (кавычки, экранирование); пустое поле → null |
strip |
{{ form.name | strip }} |
Обрезает пробелы по краям. Фильтра «trim» нет — это он и есть; lstrip/rstrip — с одной стороны |
digits |
{{ form.phone | digits }} |
Оставляет только цифры: +7 (900) 123-45-67 → 79001234567 |
number |
{{ form.qty | number | json }} |
Строку — в число: "990.50" → 990.5; не-число → null |
base64_encode / base64_decode |
{{ form.pair | base64_encode }} |
Кодирует/раскодирует base64 (например, заголовок Basic-auth) |
now |
{{ "" | now }} |
Текущее время в Unix-секундах |
uuid |
{{ "" | uuid }} |
Случайный UUID (ключи идемпотентности) |
Арифметика
LEGACY, работает и поддерживается. Для новых сценариев считайте арифметику в узле «Вычисление» (см. ниже): там литералы — целые числа, дробный операнд делает дробным результат, а плохой операнд падает с видимой ошибкой в журнале, а не молча превращается в
0.
plus, minus, times, divided_by, modulo, abs, ceil, floor,
round — всё есть: {{ form.price | number | times: 2 }}. Подводные
камни:
- Строки считаются как дробные числа — очень большие числа (длиннее 15 цифр) теряют точность. Для цен и количеств это неважно.
plus/timesна пустом или не-числовом поле молча дают0. Нужно честное «не пришло» — используйтеnumber.divided_byс делителем-строкой, нулём или отсутствующим полем ломает весь шаблон. Обе стороны — черезnumber, и проверяйте делитель:
{% assign total = form.total | number %}
{% assign parts = form.parts | number %}
{% if parts %}{{ total | divided_by: parts }}{% else %}0{% endif %}
Памятка: обрезать и закодировать — | strip | json (отсутствующее поле
даст ""; голый | json даст null). Числом, а не строкой —
| number | json.
Вычисление — посчитать значение в схеме
Когда брать: подготовить значение для следующих узлов — нормализовать телефон, посчитать сумму, собрать объект для HTTP POST.
-
Выражение — чистое выражение expr-lang, без Liquid:
let total = number(form.total); let qty = number(form.qty); {"amount": total * qty, "phone": digits(form.phone)} -
Ключ результата — имя, под которым значение станет доступно следующим узлам:
results.ключ. Оба поля обязательны.
Значение типизировано: числа остаются числами, объекты — объектами. Другие
поля-выражения ниже по схеме (body_expr, variables_expr, второе
«Вычисление») читают его как данные; Liquid-шаблоны — через
results.ключ…, как ответ GraphQL-узла. Вычисления соединяются в цепочку:
каждое видит результаты узлов выше. Ошибка выражения — не скомпилировалось,
обращение по цепочке к пустому результату, неизвестное secret("имя") —
останавливает узел и попадает в журнал прогонов с объяснением.
Отправить email — письмо с вашего сервера
Когда брать: узнать о заявке на почту, отправить посетителю подтверждение.
- Кому / Копия — адреса (можно несколько; подойдёт и
{{ form.email }}— ответить самому посетителю). - Тема и Содержимое письма — в поле «Содержимое письма» два режима: «Текст (Liquid)» — пишете текст письма прямо в узле, подставляя данные (
{{ form.name }},{{ results.ключ… }}); «Шаблон сайта» — берёте готовый HTML-файл из вкладки «Файлы» (так делают оформленные письма). - SMTP — ваш почтовый сервер (хост, порт, логин/пароль ящика, адрес отправителя). Платформа письма за вас не отправит — нужен собственный ящик; для тестов подойдёт любой бесплатный SMTP.
- В текстовом письме работает фильтр
signed_token— подставить одноразовую ссылку участника (используется в письмах сброса пароля).
Важно: пароли в письмы не попадают — платформа блокирует такие попытки.
HTTP POST — передать дальше
Когда брать: заявку нужно положить в CRM, таблицу или другой сервис.
- URL, Тело (шаблон), Заголовки (например, токен авторизации сервиса), Таймаут (по умолчанию 10 секунд).
- Долгие внешние сервисы — повод включить «Запустить в фоне».
- Как собрать «Тело (шаблон)» — раздел «Как собрать JSON-тело» выше.
- В режиме «Выражение (expr-lang)» тело считается выражением — см. «Тело (выражение)» и пример «То же тело одним выражением».
Уведомление в Telegram — сообщение в чат
Когда брать: быстро видеть заявки в общем чате команды.
- Webhook-URL — адрес чат-бота (токен бота уже внутри URL; отдельного поля для него нет).
- Текст сообщения — шаблон с данными заявки.
- chat_id — обязателен для официального api.telegram.org.
Установка cookie — запомнить посетителя
Когда брать: вход участника, «запомнить меня», скрытые метки.
- Имя, Значение (шаблон), Срок жизни в днях (0 — удалить cookie, так работает «Выход»), Путь (по умолчанию весь сайт).
- Режим «сессионная» — cookie живёт до закрытия браузера; такая в одном сценарии может быть только одна.
- Значение запечатывается подписью платформы — подделать его извне нельзя.
Редирект — перенаправить
Когда брать: после действия увести посетителя на другой адрес (например, с
/old-page на /new-page или обратно на форму при ошибке).
- Куда — адрес (шаблон: можно
/{{ params.slug }}). - Статус — по умолчанию 303 «Смотри другой адрес».
Ещё два вида узлов
Кроме действий, на холсте есть узлы «Условие» (развилка «да/нет»); обязательного узла «Конец» нет: ветка просто заканчивается там, где у последней карточки не поставлена исходящая связь, — и схема считается завершённой. Условия разобраны в отдельной статье «Условия и ветвления». У условия, кроме строк «поле — оператор — значение», есть режим «Выражение (expr-lang)» — одно логическое выражение вместо набора строк; подробно в статье «Условия и ветвления».

Следующая статья: Условия и ветвления →