Migrating from n8n
Port n8n Code node scripts and expression templates to Triggo.
Migrating from n8n
If you're coming from n8n, the surface area looks familiar — there's a template language with {{... }} braces, there's a Code node, and there are references to upstream nodes. The underlying execution model is not the same, though. This page maps the concepts you already know to Triggo equivalents and points out the places where the mental model has to shift.
At a glance
Two differences drive almost every change:
- Iteration. n8n's Code node runs per input item by default — when 50 items come in, your code runs 50 times and
$input.item.jsonis the current item. Triggo's Code node runs once per invocation with a singleinputsobject built from field mappings. If you need to iterate, you either loop inside the code (inputs.items.map(...)) or wrap the node in a Loop construct. - Expressions. n8n's
{{... }}wraps a real JavaScript expression —{{ $json.x + 1 }}, ternaries, method calls, Luxon math,$jmespath(...), extensions like.removeMarkdown(). Triggo's{{... }}is path-only:{{trigger.email}},{{action_1.order.id}}. It resolves a dot path and stops. No arithmetic, no conditionals, no method calls. For anything non-trivial, drop into a Code node.
Keep those two in your head and the rest is mostly syntax substitution.
Expression syntax translation
The left column is what you had in n8n. The right column is the Triggo equivalent — template form where possible, Code node where not.
| n8n | Triggo | Notes |
|---|---|---|
{{ $json.field }} | {{trigger.field}} or {{<stepId>.field}} | $json means "output of the previous node" in n8n. In Triggo you address each source explicitly by step id (or trigger). See Passing data. |
{{ $input.item.json.field }} | {{trigger.field}} / {{<stepId>.field}} | Only meaningful in n8n's per-item mode. Triggo has no per-item context at the template layer — if you genuinely need iteration, use a Loop or move logic into a Code node. |
{{ $input.first().json.field }} | {{<stepId>.field}} | Same substitution. There is no first() because a Triggo step output is a single object, not an array-of-items. |
{{ $input.all() }} | No direct equivalent | The data model differs — Triggo doesn't wrap outputs in [{ json, binary },...]. If an upstream step returns an array, reference it directly, e.g. {{action_1.items}}. |
{{ $node["HTTP Request"].json.field }} | {{<stepId-of-HTTP-Request>.field}} | Addressing is by step id, not node display name. Copy the step id from the inspector. |
{{ $('HTTP Request').first().json.field }} | {{<stepId>.field}} | Same as above. The $('Name') form is purely n8n. |
{{ $now.toISO() }} | Code node: utils.dayjs().toISOString() | There is no $now in templates. Dayjs' ISO method is toISOString(), not toISO(). |
{{ DateTime.now().plus({ hours: 1 }).toISO() }} | Code node: utils.dayjs().add(1, 'hour').toISOString() | Dayjs instead of Luxon. The plugin set is whatever injects — check before assuming a Luxon feature exists. |
{{ $jmespath($json, 'results[*].id') }} | No equivalent | Use the Code node with inputs.results.map(r => r.id) or plain JS. |
{{ $json.x + 1 }} | Code node | Arithmetic is not supported in templates. |
{{ $json.x > 0 ? 'a': 'b' }} | Code node, or a Condition node on the branch | Ternaries are not supported in templates. |
{{ $json.name.toUpperCase() }} | Code node | No method calls in templates. |
{{ $workflow.id }}, {{ $execution.id }} | Not available | No workflow / execution metadata is exposed to the Code node or to templates today. |
{{ $env.MY_VAR }} | Not available | The sandbox has no process.env access. Inject values via the inspector (field mappings from a prior step) instead. |
{{ $vars.myVar }} | Not available | n8n's variables feature has no Triggo equivalent. |
Code node: input shape
This is the biggest mental-model shift. Read it carefully.
n8n
In Run Once for Each Item mode (the default for many templates), the Code node runs once per input item and you write:
// n8n — per-item mode
const qty = $input.item.json.qty;
const price = $input.item.json.price;
return { json: { total: qty * price } };In Run Once for All Items mode, you write:
// n8n — all-items mode
const items = $input.all(); // Array<{ json, binary? }>
return items.map((i) => ({ json: { total: i.json.qty * i.json.price } }));Triggo
Triggo's Code node runs once, period. No per-item concept at the node level. Your function receives a single inputs object — a plain JS object built from what you mapped in the inspector:
function run(inputs, utils) {
// inputs is whatever you declared in the inspector's field mappings.
// There is no $input, no $json, no $node.
return { total: inputs.qty * inputs.price };
}If the inspector has field mappings:
{
"qty": "{{trigger.qty}}",
"price": "{{trigger.price}}"
}...then inputs === { qty: <number>, price: <number> }. That's the whole contract. See Reference → Inputs for the full story.
Code node: per-item migration pattern
The common case — an n8n node that runs per item and transforms each one. Say the original is:
// n8n — Run Once for Each Item
return { json: { total: $input.item.json.qty * $input.item.json.price } };...and the array of line items arrives on the trigger as trigger.lines. Two ways to port.
Option A — Loop the Code node
-
Insert a Loop node iterating over
{{trigger.lines}}. -
Inside the loop, add a Code node whose field mappings bind to the current loop item (e.g.
qty: "{{loop_1.item.qty}}",price: "{{loop_1.item.price}}"). -
The Code node body:
function run(inputs) { return { total: inputs.qty * inputs.price }; }
This preserves the n8n mental model of "per-item work" and is the right choice when each iteration needs other downstream steps (a connector call per item, etc.).
Option B — Do the whole map inside the Code node
Most of the time, if the per-item work has no side effects, just do it all in one Code node:
function run(inputs) {
return {
lines: inputs.lines.map((l) => ({ ...l, total: l.qty * l.price })),
};
}Inspector field mapping: lines: "{{trigger.lines}}". Downstream nodes address the result as {{code_1.lines}}.
Option B is simpler, faster (one isolate spin-up instead of N), and easier to reason about. Reach for Option A only when the body genuinely needs to fan out through other nodes.
Globals and helpers
| n8n | Triggo |
|---|---|
$now (Luxon DateTime) | utils.dayjs() — dayjs. Chained API (.add, .subtract, .format) similar in spirit. Method names differ from Luxon (toISOString, valueOf, format). Only the plugins bundled into the sandbox are available. |
$today | utils.dayjs().startOf('day') |
DateTime.now().plus({ hours: 1 }) | utils.dayjs().add(1, 'hour') |
DateTime.fromISO(str) | utils.dayjs(str) |
Duration, Interval | Not provided — use dayjs math or plain arithmetic on valueOf(). |
$jmespath(obj, path) | Not provided — use plain JS (obj.results.map(r => r.id) etc.). |
$vars | Not available. |
$env | Not available — sandbox has no process.env. |
$input.context | No equivalent — Triggo does not expose a per-run context object. |
$workflow, $execution | Not available in the sandbox. |
$runIndex, $itemIndex | Not available. |
$getWorkflowStaticData(...) | Not available — no cross-run state. If you need persistence, put the value in a connector (a spreadsheet, a KV, etc.). |
Triggo utilities n8n doesn't have a direct equivalent for
n8n relies on expression extensions (.removeMarkdown(), .isEmpty(), etc.) bolted onto expression outputs. Triggo has a small utility belt instead:
| Helper | Purpose |
|---|---|
utils.uuid() | v4 UUID. |
utils.pick(obj, keys) / utils.omit(obj, keys) | Object subset / removal. |
utils.groupBy(arr, key) / utils.keyBy(arr, key) / utils.uniqBy(arr, key) | Common array → object / dedupe operations. |
utils.base64Encode(str) / utils.base64Decode(str) | UTF-8 ↔ base64. (No Buffer in the sandbox.) |
utils.hash(str, algo?) | Hex digest; algo defaults to sha256. |
See Reference → __utils for full signatures.
Worked migration example — end to end
Scenario: a webhook fires with an order payload, we shape a lead record for downstream CRM insertion, timestamping it and dropping line items with non-positive quantities.
n8n version (Run Once for All Items)
// n8n — JavaScript, all-items mode
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 version, annotated
Inspector field mapping (one entry):
{ "order": "{{trigger}}" }Code body:
function run(inputs, utils) {
// 1. `inputs.order` is the whole trigger payload — no $input.all(),
// no array-of-items wrapper.
const order = inputs.order;
// 2. Plain JS for filtering and reducing — no expression extensions needed.
const validLines = order.lines.filter((l) => l.qty > 0);
const subtotal = validLines.reduce((s, l) => s + l.qty * l.price, 0);
// 3. Return a single plain object. No `{ json: ... }` wrapper.
// Downstream nodes address fields as `{{code_1.orderId}}` etc.
return {
orderId: order.id,
customerEmail: order.customer.email.toLowerCase(),
lineCount: validLines.length,
subtotal,
currency: order.currency ?? "RUB",
// 4. Dayjs instead of Luxon DateTime. `toISOString()`, not `toISO()`.
capturedAt: utils.dayjs().toISOString(),
// 5. Replace $jmespath with plain JS.
fingerprint: order.lines.map((l) => l.sku).join("|"),
};
}The mechanical substitutions:
$input.all()[0].json→ the singleinputs.orderyou mapped in the inspector.DateTime.now().toISO()→utils.dayjs().toISOString().$jmespath(order, 'lines[*].sku')→order.lines.map((l) => l.sku).- The
[{ json:... }]return wrapper → a bare object.
Things that don't translate
Some n8n features simply aren't in Triggo's sandbox. Don't try to port these — you'll need to redesign the flow or accept the loss.
- Binary data. n8n exposes binary data on every item as
$input.item.binaryor$node["X"].binary, and the Code node can read/write it directly. Triggo's Code node has no binary channel — everything goes through JSON-serialized field mappings, andBufferis not available. If you need to handle files, upload/download them through a connector and pass URLs or IDs between nodes. - Cross-node binary access.
$node["X"].binary[...]has no equivalent. Same remedy: keep binary handling inside a connector. - Credentials from code. n8n's
this.getCredentials('myCred')is not exposed — the sandbox has no host API. Credential resolution happens outside the Code node in Triggo; a connector action reads the credential, and you pass only the fields you need into the code via field mapping. $getWorkflowStaticData()/ cross-run persistence. There is no static data slot. The sandbox is stateless by design (see Overview → Sandbox model). For persistence, write to an external store through a connector.- Require / import. n8n can be configured to allow external npm modules; Triggo cannot. There is no module resolver inside the isolate.
- Network calls.
fetch,http,axios— all gone. Use the HTTP Request connector upstream, map the result into the Code node as an input. - Timers.
setTimeoutand friends are deleted. No polling, no delay. If you need a delay, wire a Delay / Schedule step into the DAG.
Related
- Code node overview — sandbox model, defaults, when to use.
- Code node reference — entry function, I/O contract,
__utilssignatures, limits, errors. - Field mapping — the
{{...}}template language you'll use to get data into the Code node.