AI-skillGratis

n8n expression-syntax

Gebaseerd op czlonkowski/n8n-skills @ 72470a0, licentie MIT

Bijgewerkt 30 augustus 2026

Dit bestand is door ToolBrain vertaald en inhoudelijk gewijzigd op 2026-08-30, op basis van n8n-expression-syntax SKILL.md uit czlonkowski/n8n-skills (MIT), zie de bron_url hierboven en ../LICENSES/n8n-skills-MIT.txt.

Gids voor het correct schrijven van n8n-expressies binnen workflows — expressies zijn de manier waarop n8n data tussen nodes doorgeeft, en de syntax verkeerd krijgen is de meest voorkomende foutbron in workflows.

Expressieformaat

Alle dynamische inhoud in n8n gebruikt dubbele accolades: {{expressie}}.

Code
✅ {{$json.email}}
✅ {{$json.body.name}}
✅ {{$node["HTTP Request"].json.data}}
❌ $json.email  (geen accolades — wordt als platte tekst behandeld)
❌ {$json.email}  (enkele accolades — ongeldig)

Kernvariabelen

$json — data van de huidige node: {{$json.fieldName}}, {{$json['veld met spaties']}}, {{$json.nested.property}}, {{$json.items[0].name}}.

$node — data van elke eerdere node: {{$node["Node Naam"].json.fieldName}}. Let op: de nodenaam moet tussen aanhalingstekens staan, is hoofdlettergevoelig, en moet exact overeenkomen met de naam in de workflow.

$now — huidig tijdstip: {{$now}}, {{$now.toFormat('yyyy-MM-dd')}}, {{$now.toFormat('HH:mm:ss')}}, {{$now.plus({days: 7})}}.

$env — omgevingsvariabelen: {{$env.API_KEY}}, {{$env.DATABASE_URL}}. Let op: sommige n8n-instanties blokkeren $env-toegang volledig via een instellingsvlag. Geeft $env fouten, gebruik dan een alternatief: waarden in credentials opslaan, een Set-node met handmatig ingevoerde waarden, of waarden meegeven via webhook-queryparameters.

Kritiek: de structuur van webhook-data

Meest voorkomende fout: webhook-data staat niet op de root! De Webhook-node wikkelt inkomende data onder een .body-property om headers, params en queryparameters te bewaren:

json
{
  "headers": {...},
  "params": {...},
  "query": {...},
  "body": {           // ⚠️ HIER STAAT DE GEBRUIKERSDATA!
    "name": "Jan",
    "email": "[email protected]",
    "message": "Hallo"
  }
}
javascript
❌ FOUT: {{$json.name}}
❌ FOUT: {{$json.email}}

✅ GOED: {{$json.body.name}}
✅ GOED: {{$json.body.email}}
✅ GOED: {{$json.body.message}}

Veelgebruikte patronen

Geneste velden benaderen: dotnotatie voor gewone paden ({{$json.user.email}}), array-toegang ({{$json.data[0].name}}), haakjesnotatie voor velden met spaties ({{$json['field name']}}, {{$json['user data']['first name']}}).

Verwijzen naar andere nodes: {{$node["Set"].json.value}} voor een node zonder spaties, {{$node["HTTP Request"].json.data}} voor een node mét spaties (heel gebruikelijk), en {{$node["Webhook"].json.body.email}} voor webhook-data via een nodeverwijzing.

Variabelen combineren: concatenatie gebeurt automatisch ("Hallo {{$json.body.name}}!"), ook bruikbaar in URLs (https://api.voorbeeld.nl/gebruikers/{{$json.body.user_id}}) en in JSON-objecten ("naam": "={{$json.body.name}}").

Wanneer je GEEN expressies gebruikt

Code-nodes gebruiken directe JavaScript-toegang, geen expressies:

javascript
// ❌ FOUT in een Code-node
const email = '={{$json.email}}';

// ✅ GOED in een Code-node
const email = $json.email;
const allItems = $input.all();

Webhook-paden accepteren geen expressies — alleen statische paden.

Credential-velden accepteren geen expressies — gebruik het credential-systeem van n8n, niet $env binnen een expressie.

De transformatie-poortwachter

Loop, vóórdat je een node toevoegt — of code schrijft — om data te transformeren, deze volgorde langs en stop bij de eerste die past:

  1. Expressie ({{ ... }}) in het veld zelf. Property-toegang, methodeketens (.map().filter().join()), ternaire operatoren, string-opbouw, datumrekenen — als het "neem A, maak er B van" is zonder tussenvariabelen, is het een expressie. Dit dekt de meeste "transformeer dit even"-gevallen.

  2. Een direct-aangeroepen functie-expressie binnen een Edit Fields-veld. Heeft de logica tussenvariabelen, vertakking of commentaar nodig, maar werkt hij nog op één item, wikkel het dan in een direct-uitvoerende functie recht in de veldwaarde:

    javascript
    ={{ (() => {
        const items = $json.line_items;
        const subtotal = items.reduce((sum, it) => sum + it.price * it.qty, 0);
        const tax = subtotal * 0.08;
        return (subtotal + tax).toFixed(2);
    })() }}

    De buitenste haakjes omkaderen de functie, de haakjes erachter roepen 'm aan — laat je er één weg, dan weigert n8n de expressie. Binnenin heb je de volledige expressie-scope ($json, $('Node'), $now) plus const/let, if/switch, try/catch en regex. Geen require, geen await.

  3. Code-node — laatste redmiddel. Alleen als je over de héle dataset moet aggregeren, een toegestane bibliotheek nodig hebt, of asynchroon werk moet doen.

Waarom deze volgorde ertoe doet. Het is geen stijlkwestie maar leesbaarheid en snelheid. Een Code-node draait in een gesandboxte VM met opstartkosten per aanroep — dat kan honderden milliseconden kosten voordat je eigen logica start. Dezelfde logica in een expressie of een Edit-Fields-functie draait in-process in enkele milliseconden, zonder sandbox. Voor puur één-item-transformaties zonder functioneel verschil is dat een groot gat, dat oploopt op hot paths zoals per-request-webhooks. Grijp pas verder als de input of scope het echt vereist.

Het Set-node-antipatroon en het samenkomen van takken

Verwijder Set-nodes die maar één afnemer voeden

Een Set-/Edit-Fields-node die alleen een waarde uithaalt en doorgeeft aan één downstream node is dode last. Zet de expressie rechtstreeks in het veld van die afnemer.

Code
❌  Webhook → Set { customer_id: {{ $json.body.customer_id }} } → Postgres: WHERE id = {{ $json.customer_id }}

✅  Webhook → Postgres: WHERE id = {{ $('Webhook').item.json.body.customer_id }}

De Set-node voegt een extra stap toe, meer visuele rommel, en een refactorrisico, zonder iets te doen wat de afnemer niet zelf kan. Vuistregel: tel hoeveel downstream-nodes elk veld dat de Set produceert daadwerkelijk gebruiken — 0 of 1 → verwijderen en inline zetten; 2 of meer → de Set mag blijven.

Uitzonderingen waarbij een Set wél zinvol is: meerdere afnemers lezen dezelfde niet-triviale afgeleide waarde (naamgeving helpt de leesbaarheid en je berekent het maar één keer); het is de laatste node van een sub-workflow die de outputvorm bepaalt (de Set ís dan de API-grens); of je hernoemt/whitelist bewust velden en wilt dat op één plek zichtbaar houden.

Samenkomende takken: verankeren met een NoOp

Komen takken samen (na IF/Switch/Merge), dan wordt $json onvoorspelbaar — "welke tak het laatst vuurde" — en dat is een stille bron van verkeerde data. Zet een NoOp-node op het samenkomstpunt, geef 'm een beschrijvende naam (bijvoorbeeld "Inputs samenvoegen"), en laat downstream-nodes daarnaar verwijzen op naam in plaats van via $json. Zo'n NoOp overleeft latere refactors: een node die je er later tussenzet, breekt de verwijzing niet.

Algemener advies in vertakkende workflows: verwijs bij voorkeur naar een node op naam ($('Node').item.json.x) in plaats van naar het diepe $json.x. $json breekt zodra er een tussenliggende node wordt ingevoegd of een node de item-context wist (Aggregate, Code in "Run for All"-modus, samenkomende takken); die breuk is stil en downstream krijgt zonder foutmelding de verkeerde data. Een verwijzing op nodenaam is ondubbelzinnig, ongeacht wat er tussen bron en afnemer zit.

Validatieregels op een rij

  1. Altijd {{}} gebruiken — $json.field zonder accolades wordt platte tekst.
  2. Haakjesnotatie voor spaties en bijzondere tekens — veld- of nodenamen met spaties, diakritische tekens of speciale tekens vereisen haakjesnotatie: {{$json['field name']}}, {{$node["HTTP Request"].json}}, {{$json['Gross Price w/o shipment']}}.
  3. Exacte nodenamen — nodeverwijzingen zijn hoofdlettergevoelig; {{$node["http request"]}} werkt niet als de node echt "HTTP Request" heet.
  4. Geen geneste {{}} — {{{$json.field}}} is fout, {{$json.field}} is goed.

Snelle foutentabel

FoutFix
$json.field{{$json.field}}
{{$json.field name}}{{$json['field name']}}
{{$node.HTTP Request}}{{$node["HTTP Request"]}}
{{{$json.field}}}{{$json.field}}
{{$json.name}} (webhook){{$json.body.name}}
'={{$json.email}}' (Code-node)$json.email

Databehandeling per type

Arrays: {{$json.users[0].email}} (eerste item), {{$json.users.length}} (lengte), {{$json.users[$json.users.length - 1].name}} (laatste item).

Objecten: dotnotatie zonder spaties ({{$json.user.email}}), haakjesnotatie met spaties of dynamische keys ({{$json['user data'].email}}).

Strings: automatische concatenatie ("Hallo {{$json.name}}!"), en methoden zoals {{$json.email.toLowerCase()}}, {{$json.name.toUpperCase()}}.

Getallen: direct gebruik ({{$json.price}}) of rekenkundig ({{$json.price * 1.1}} voor 10% erbij, {{$json.quantity + 5}}).

Geavanceerde patronen

Voorwaardelijke inhoud met een ternaire operator ({{$json.status === 'active' ? 'Actieve gebruiker' : 'Inactieve gebruiker'}}) of een standaardwaarde ({{$json.email || '[email protected]'}}). Datumbewerking met Luxon: {{$now.plus({days: 7}).toFormat('yyyy-MM-dd')}}, {{$now.minus({hours: 24}).toISO()}}. Stringbewerking: substring, replace, split/join zijn allemaal beschikbaar als methode op de stringwaarde.

Performance: expressiecomplexiteit is (bijna) gratis

Een veelgehoorde zorg is dat een complexe {{ }} traag is. Dat klopt niet — wat kost is hoe vaak n8n een expressie evalueert, niet hoe uitgebreid elke expressie is. Op een moderne n8n- instantie kost een uitgebreide expressie (wortel, split, reduce, rekenkunde) ongeveer evenveel per item als een triviale {{ $json.x > 50 }} — grofweg 0,2 ms per item in beide gevallen, omdat zo'n 90% van die tijd gaat naar het opbouwen van de per-item-evaluatiecontext, niet naar het uitvoeren van je expressie.

Praktisch betekent dit: breek een werkende expressie niet op in een keten van nodes voor "snelheid" — elke extra node evalueert opnieuw per item en kopieert alle items opnieuw; één node met één rijkere expressie is sneller dan drie nodes met simpele. Een expressie is ook zo'n 3× goedkoper dan een Code-node in "per item"-modus voor dezelfde check — maar een Code-node in "alle items in één keer"-modus is nóg goedkoper, omdat die de per-item-grens één keer oversteekt in plaats van N keer. Dit speelt pas op bij duizenden items; daaronder blijft alles ruim onder de 100 ms.

Debuggen van expressies

Test in de ingebouwde expressie-editor (klik het veld, open de "fx"-preview) om live het resultaat te zien en fouten in rood gemarkeerd te krijgen. Veelvoorkomende foutmeldingen: "Cannot read property 'X' of undefined" betekent dat het bovenliggende object niet bestaat — controleer je datapad; "X is not a function" betekent dat je een methode aanroept op iets wat geen functie is — controleer het type; en verschijnt de expressie als platte tekst, dan ontbreken de {{ }}.

Samenvatting

Kernregels: wikkel expressies altijd in {{ }}; webhook-data staat onder .body; geen {{ }} in Code-nodes; citeer nodenamen met spaties; nodenamen zijn hoofdlettergevoelig. Meest voorkomende fouten: ontbrekende accolades, {{$json.name}} gebruiken op webhookdata in plaats van {{$json.body.name}}, {{$json.email}} gebruiken in een Code-node in plaats van $json.email, en {{$node.HTTP Request}} in plaats van {{$node["HTTP Request"]}}.

Praktijkvoorbeeld (NL)

Een Nederlandse evenementenorganisator gebruikt een n8n-workflow om aanmeldingen voor netwerkborrels te verwerken die via een webformulier binnenkomen. De workflow ving in eerste instantie geen enkele aanmelding correct af, omdat het team in de Slack-notificatie {{$json.naam}} gebruikte terwijl de webhook de formulierdata — precies zoals hierboven beschreven — onder body wegzet, dus de juiste expressie was {{$json.body.naam}}. Na de fix is ook de losse Set-node verwijderd die alleen het e-mailadres doorgaf aan één Mailchimp-node verderop: die expressie staat nu rechtstreeks in het Mailchimp-veld, met een verwijzing op nodenaam naar de Webhook-node zodat een latere tussenvoeging van een validatiestap de koppeling niet meer kan breken.