Узлы-действия: что умеет каждый

Справочник по всем девяти действиям, которые можно положить на холст сценария. Для каждого — что делает, когда брать и что вписать в поля.

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

Панель настроек GraphQL-узла

Поля, которые есть у любого действия

  • При ошибке — «Продолжить» или «Остановить». По умолчанию сбой шага не останавливает сценарий (например, письмо не ушло — а страница «Спасибо» посетителю всё равно покажется). «Остановить» — когда следующий шаг без этого бессмыслен.
  • Запустить в фоне (не ждать) — для медленных внешних сервисов: посетитель получает ответ сразу, шаг доделывается в фоне. Работает у «Отправить 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.

Когда брать: вход участника, «запомнить меня», скрытые метки.

  • Имя, Значение (шаблон), Срок жизни в днях (0 — удалить cookie, так работает «Выход»), Путь (по умолчанию весь сайт).
  • Режим «сессионная» — cookie живёт до закрытия браузера; такая в одном сценарии может быть только одна.
  • Значение запечатывается подписью платформы — подделать его извне нельзя.

Редирект — перенаправить

Когда брать: после действия увести посетителя на другой адрес (например, с /old-page на /new-page или обратно на форму при ошибке).

  • Куда — адрес (шаблон: можно /{{ params.slug }}).
  • Статус — по умолчанию 303 «Смотри другой адрес».

Ещё два вида узлов

Кроме действий, на холсте есть узлы «Условие» (развилка «да/нет»); обязательного узла «Конец» нет: ветка просто заканчивается там, где у последней карточки не поставлена исходящая связь, — и схема считается завершённой. Условия разобраны в отдельной статье «Условия и ветвления». У условия, кроме строк «поле — оператор — значение», есть режим «Выражение (expr-lang)» — одно логическое выражение вместо набора строк; подробно в статье «Условия и ветвления».

Меню «Добавить узел»


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