Миграция с n8n
Перенос скриптов узла Code и шаблонов-выражений из n8n в Triggo.
Миграция с n8n
Если вы приходите из n8n, поверхность выглядит знакомо — есть язык шаблонов со скобками {{... }}, есть узел Code и есть ссылки на узлы выше по потоку. Модель выполнения под капотом, впрочем, не та же. Эта страница проецирует уже знакомые концепции на эквиваленты Triggo и указывает места, где ментальной модели придётся сместиться.
Беглым взглядом
Два отличия определяют почти каждое изменение:
- Итерация. Узел Code в n8n по умолчанию запускается per input item — когда приходит 50 элементов, ваш код запускается 50 раз, а
$input.item.json— текущий элемент. Узел Code в Triggo запускается один раз за вызов с единым объектомinputs, построенным из сопоставлений полей. Если нужна итерация — либо делайте цикл внутри кода (inputs.items.map(...)), либо оборачивайте узел в Loop. - Выражения.
{{... }}в n8n оборачивает настоящее JavaScript-выражение —{{ $json.x + 1 }}, тернарники, вызовы методов, математика Luxon,$jmespath(...), расширения вроде.removeMarkdown().{{... }}в Triggo — только путь:{{trigger.email}},{{action_1.order.id}}. Он разрешает точечный путь и останавливается. Никакой арифметики, условий, вызовов методов. Для всего нетривиального — переходите в узел Code.
Держите эти две вещи в голове, и остальное — в основном синтаксическая подстановка.
Перевод синтаксиса выражений
Левая колонка — то, что было в n8n. Правая — эквивалент в Triggo: шаблонная форма, где возможно, иначе узел Code.
| n8n | Triggo | Заметки |
|---|---|---|
{{ $json.field }} | {{trigger.field}} или {{<stepId>.field}} | $json в n8n означает «выход предыдущего узла». В Triggo вы адресуете каждый источник явно по step id (или trigger). См. Передача данных. |
{{ $input.item.json.field }} | {{trigger.field}} / {{<stepId>.field}} | Осмысленно только в per-item-режиме n8n. У Triggo на уровне шаблонов нет per-item-контекста — если действительно нужна итерация, используйте Loop или вынесите логику в узел Code. |
{{ $input.first().json.field }} | {{<stepId>.field}} | Та же подстановка. first() нет, потому что выход шага в Triggo — один объект, а не массив элементов. |
{{ $input.all() }} | Прямого эквивалента нет | Модель данных другая — Triggo не оборачивает выходы в [{ json, binary },...]. Если узел выше по потоку возвращает массив, адресуйте его напрямую, например {{action_1.items}}. |
{{ $node["HTTP Request"].json.field }} | {{<stepId-of-HTTP-Request>.field}} | Адресация по step id, а не по отображаемому имени узла. Скопируйте step id из инспектора. |
{{ $('HTTP Request').first().json.field }} | {{<stepId>.field}} | То же, что выше. Форма $('Name') — чисто n8n. |
{{ $now.toISO() }} | Узел Code: utils.dayjs().toISOString() | $now в шаблонах нет. ISO-метод dayjs — toISOString(), а не toISO(). |
{{ DateTime.now().plus({ hours: 1 }).toISO() }} | Узел Code: utils.dayjs().add(1, 'hour').toISOString() | Dayjs вместо Luxon. Набор плагинов — тот, что инъектируется, — проверьте, прежде чем предполагать наличие фичи из Luxon. |
{{ $jmespath($json, 'results[*].id') }} | Эквивалента нет | Используйте узел Code: inputs.results.map(r => r.id) или обычный JS. |
{{ $json.x + 1 }} | Узел Code | Арифметика в шаблонах не поддерживается. |
{{ $json.x > 0 ? 'a': 'b' }} | Узел Code или узел Condition на ветви | Тернарники в шаблонах не поддерживаются. |
{{ $json.name.toUpperCase() }} | Узел Code | Вызовов методов в шаблонах нет. |
{{ $workflow.id }}, {{ $execution.id }} | Недоступно | Метаданные workflow / execution сегодня не экспонируются ни в узел Code, ни в шаблоны. |
{{ $env.MY_VAR }} | Недоступно | У песочницы нет доступа к process.env. Пробрасывайте значения через инспектор (сопоставление полей из предыдущего шага). |
{{ $vars.myVar }} | Недоступно | Фича переменных из n8n в Triggo эквивалента не имеет. |
Узел Code: форма входа
Это самый крупный сдвиг ментальной модели. Прочтите внимательно.
n8n
В режиме Run Once for Each Item (по умолчанию для многих шаблонов) узел Code запускается один раз на элемент, и вы пишете:
// n8n — per-item-режим
const qty = $input.item.json.qty;
const price = $input.item.json.price;
return { json: { total: qty * price } };В режиме Run Once for All Items вы пишете:
// n8n — all-items-режим
const items = $input.all(); // Array<{ json, binary? }>
return items.map((i) => ({ json: { total: i.json.qty * i.json.price } }));Triggo
Узел Code в Triggo запускается один раз, и точка. Никакой per-item-концепции на уровне узла. Ваша функция принимает один объект inputs — обычный JS-объект, построенный из того, что вы сопоставили в инспекторе:
function run(inputs, utils) {
// inputs — то, что вы объявили в сопоставлениях полей инспектора.
// Никаких $input, $json, $node.
return { total: inputs.qty * inputs.price };
}Если в инспекторе сопоставления полей такие:
{
"qty": "{{trigger.qty}}",
"price": "{{trigger.price}}"
}...то inputs === { qty: <number>, price: <number> }. Это и есть весь контракт. Подробности — в Справочник → Входы.
Узел Code: паттерн миграции per-item
Типичный случай — узел n8n, который запускается на каждый элемент и трансформирует его. Допустим, оригинал такой:
// n8n — Run Once for Each Item
return { json: { total: $input.item.json.qty * $input.item.json.price } };...и массив позиций приходит в триггере как trigger.lines. Два способа портирования.
Вариант A — цикл вокруг узла Code
-
Вставьте узел Loop, итерирующий
{{trigger.lines}}. -
Внутри цикла добавьте узел Code, чьи сопоставления полей привязаны к текущему элементу цикла (например,
qty: "{{loop_1.item.qty}}",price: "{{loop_1.item.price}}"). -
Тело узла Code:
function run(inputs) { return { total: inputs.qty * inputs.price }; }
Это сохраняет ментальную модель n8n «работа на каждый элемент» и подходит, когда каждой итерации нужны другие нижестоящие шаги (вызов коннектора на элемент и т. п.).
Вариант B — сделать весь map внутри узла Code
Чаще всего, если per-item-работа без побочных эффектов, делайте всё в одном узле Code:
function run(inputs) {
return {
lines: inputs.lines.map((l) => ({ ...l, total: l.qty * l.price })),
};
}Сопоставление полей в инспекторе: lines: "{{trigger.lines}}". Нижестоящие узлы адресуют результат как {{code_1.lines}}.
Вариант B проще, быстрее (один запуск изолята вместо N) и легче для рассуждения. Тянитесь за вариантом A только тогда, когда телу действительно нужен fan-out через другие узлы.
Глобалы и помощники
| n8n | Triggo |
|---|---|
$now (Luxon DateTime) | utils.dayjs() — dayjs. Цепочечный API (.add, .subtract, .format) похож по духу. Имена методов отличаются от Luxon (toISOString, valueOf, format). Доступны только плагины, вшитые в песочницу. |
$today | utils.dayjs().startOf('day') |
DateTime.now().plus({ hours: 1 }) | utils.dayjs().add(1, 'hour') |
DateTime.fromISO(str) | utils.dayjs(str) |
Duration, Interval | Не предоставляются — используйте математику dayjs или обычную арифметику над valueOf(). |
$jmespath(obj, path) | Не предоставляется — используйте обычный JS (obj.results.map(r => r.id) и т. п.). |
$vars | Недоступно. |
$env | Недоступно — у песочницы нет process.env. |
$input.context | Эквивалента нет — Triggo не экспонирует per-run-контекстный объект. |
$workflow, $execution | Недоступно в песочнице. |
$runIndex, $itemIndex | Недоступно. |
$getWorkflowStaticData(...) | Недоступно — нет состояния между запусками. Если нужна персистентность, храните значение в коннекторе (таблица, KV и т. п.). |
Утилиты Triggo, у которых в n8n нет прямого эквивалента
n8n опирается на расширения выражений (.removeMarkdown(), .isEmpty() и т. п.), привинченные к выводам выражений. У Triggo вместо этого — небольшой пояс с утилитами:
| Помощник | Назначение |
|---|---|
utils.uuid() | UUID v4. |
utils.pick(obj, keys) / utils.omit(obj, keys) | Подмножество / исключение свойств объекта. |
utils.groupBy(arr, key) / utils.keyBy(arr, key) / utils.uniqBy(arr, key) | Частые операции массив → объект / дедупликация. |
utils.base64Encode(str) / utils.base64Decode(str) | UTF-8 ↔ base64. (Buffer в песочнице нет.) |
utils.hash(str, algo?) | Hex-дайджест; algo по умолчанию sha256. |
Полные сигнатуры — в Справочник → __utils.
Пример миграции — от и до
Сценарий: вебхук срабатывает с полезной нагрузкой заказа, мы формируем запись лида для последующей вставки в CRM, проставляя временную метку и отбрасывая позиции с неположительным количеством.
Версия n8n (Run Once for All Items)
// n8n — JavaScript, all-items-режим
const items = $input.all();
const order = items[0].json;
const validLines = order.lines.filter((l) => l.qty > 0);
const subtotal = validLines.reduce((s, l) => s + l.qty * l.price, 0);
return [
{
json: {
orderId: order.id,
customerEmail: order.customer.email.toLowerCase(),
lineCount: validLines.length,
subtotal,
currency: order.currency ?? 'RUB',
capturedAt: DateTime.now().toISO(),
fingerprint: $jmespath(order, 'lines[*].sku').join('|'),
},
},
];Версия Triggo с комментариями
Сопоставление полей в инспекторе (одна запись):
{ "order": "{{trigger}}" }Тело кода:
function run(inputs, utils) {
// 1. `inputs.order` — вся полезная нагрузка триггера. Ни $input.all(),
// ни обёртки «массив элементов».
const order = inputs.order;
// 2. Обычный JS для фильтрации и свёртки — никаких расширений выражений.
const validLines = order.lines.filter((l) => l.qty > 0);
const subtotal = validLines.reduce((s, l) => s + l.qty * l.price, 0);
// 3. Возвращаем один обычный объект. Никакой обёртки `{ json: ... }`.
// Нижестоящие узлы адресуют поля как `{{code_1.orderId}}` и т. д.
return {
orderId: order.id,
customerEmail: order.customer.email.toLowerCase(),
lineCount: validLines.length,
subtotal,
currency: order.currency ?? "RUB",
// 4. Dayjs вместо Luxon DateTime. `toISOString()`, а не `toISO()`.
capturedAt: utils.dayjs().toISOString(),
// 5. Заменяем $jmespath обычным JS.
fingerprint: order.lines.map((l) => l.sku).join("|"),
};
}Механические подстановки:
$input.all()[0].json→ одинinputs.order, который вы сопоставили в инспекторе.DateTime.now().toISO()→utils.dayjs().toISOString().$jmespath(order, 'lines[*].sku')→order.lines.map((l) => l.sku).- Обёртка возврата
[{ json:... }]→ голый объект.
Что не переводится
Некоторые фичи n8n просто отсутствуют в песочнице Triggo. Не пытайтесь их портировать — нужно перепроектировать поток или принять потерю.
- Binary-данные. n8n экспонирует binary-данные на каждом элементе как
$input.item.binaryили$node["X"].binary, и узел Code может читать/писать их напрямую. У узла Code в Triggo binary-канала нет — всё идёт через JSON-сериализованные сопоставления полей, аBufferнедоступен. Если нужно работать с файлами, загружайте/скачивайте их через коннектор и пробрасывайте URL или ID между узлами. - Кросс-узловой доступ к binary.
$node["X"].binary[...]эквивалента не имеет. То же лечение: держите работу с binary внутри коннектора. - Credentials из кода.
this.getCredentials('myCred')из n8n не экспонирован — у песочницы нет хост-API. Разрешение credentials происходит вне узла Code в Triggo; действие коннектора читает credential, а в код вы через сопоставление полей пробрасываете только нужные поля. $getWorkflowStaticData()/ персистентность между запусками. Слота статических данных нет. Песочница по дизайну без состояния (см. Обзор → Модель песочницы). Для персистентности пишите во внешнее хранилище через коннектор.- Require / import. n8n можно настроить на разрешение внешних npm-модулей; Triggo — нет. Внутри изолята нет резолвера модулей.
- Сетевые вызовы.
fetch,http,axios— всё удалено. Используйте коннектор HTTP Request выше по потоку, пробрасывайте результат в узел Code как вход. - Таймеры.
setTimeoutи компания удалены. Никакого опроса, никакой задержки. Если нужна задержка, вплетите в DAG шаг Delay / Schedule.
Связанное
- Обзор узла Code — модель песочницы, значения по умолчанию, когда использовать.
- Справочник по узлу Code — функция-вход, контракт I/O, сигнатуры
__utils, лимиты, ошибки. - Сопоставление полей — язык шаблонов
{{...}}, которым вы будете пробрасывать данные в узел Code.