Wyrażenia w n8n: $json, $node, $items i Luxon pod maską
Poznaj silnik wyrażeń w n8n. Zobacz, jak działa kontekst, czym różni się $json od $items i jak zarządzać czasem z biblioteką Luxon bez błędów undefined.
Wyrażenia w n8n to absolutny fundament budowania dynamicznych, skalowalnych i elastycznych przepływów danych. Choć interfejs wizualny pozwala na proste przeciąganie zmiennych z panelu wejściowego do pól konfiguracyjnych, prawdziwa moc automatyzacji ujawnia się dopiero, gdy zrozumiemy, jak silnik ewaluacji działa pod maską. Zrozumienie różnicy między kontekstem pojedynczego elementu a całą tablicą wyników to granica między początkującym użytkownikiem a inżynierem, który potrafi zoptymalizować skomplikowane workflow i zapobiegać błędom na produkcji.
Wielu twórców automatyzacji zatrzymuje się na podstawowym mapowaniu pól, co prowadzi do frustracji, gdy struktura danych ulega zmianie, a workflow niespodziewanie się zatrzymuje z błędem o niezdefiniowanych właściwościach. Aby pisać niezawodne integracje, musisz wiedzieć, w jaki sposób środowisko uruchomieniowe interpretuje składnię z podwójnymi nawiasami klamrowymi i jak zarządza pamięcią oraz referencjami do poprzednich kroków.
W tym artykule rozłożymy na czynniki pierwsze mechanizm wyrażeń w n8n. Zobaczymy, dlaczego odwołania do innych node'ów bywają problematyczne przy przetwarzaniu wsadowym (batch processing), jak poprawnie zarządzać czasem za pomocą wbudowanej biblioteki Luxon oraz w jaki sposób debugować i zapobiegać błędom typu undefined. Po lekturze będziesz w stanie pisać bardziej odporne na błędy wyrażenia, świadomie wybierając między różnymi metodami dostępu do danych.
Silnik wyrażeń w n8n: piaskownica JavaScript pod maską
Kiedy wpisujesz {{ }} w polu konfiguracyjnym dowolnego node'a, n8n nie wykonuje zwykłego podstawiania ciągów znaków (string replacement). Zamiast tego uruchamia wyizolowaną piaskownicę (sandbox) opartą na silniku V8 JavaScript, w której parser kompiluje i ewaluuje podany kod w tzw. kontekście bieżącego wykonania (Execution Context). Oznacza to, że wewnątrz wyrażeń masz pełny dostęp do standardowych metod języka JavaScript, takich jak .split(), .map(), .reduce() czy .replace(), ale operujesz na wstrzykniętych przez n8n obiektach globalnych, które izolują Cię od głównego procesu Node.js.
Mechanizm ten działa iteracyjnie i jest ściśle powiązany z architekturą przetwarzania danych w n8n. Jeśli twój node przyjmuje na wejściu tablicę składającą się z pięciu elementów, n8n domyślnie wykona ewaluację każdego wyrażenia w tym node'a dokładnie pięć razy – oddzielnie dla każdego elementu (item). Zrozumienie tej iteracyjnej natury jest kluczowe, ponieważ wyjaśnia, dlaczego obiekty takie jak $json zmieniają swoją zawartość w ułamkach sekund podczas jednego cyklu wykonania node'a. Środowisko uruchomieniowe dynamicznie przepina referencję do kolejnych obiektów z tablicy wejściowej.
Warto jednak pamiętać o wydajności. Ponieważ każde wyrażenie to osobne wywołanie w piaskownicy, umieszczanie bardzo złożonej logiki biznesowej, pętli czy transformacji dużych tablic wewnątrz {{ }} jest antywzorcem. Do ciężkich operacji transformacyjnych znacznie lepiej nadaje się dedykowany node Code, który pozwala przetworzyć całą strukturę jednorazowo, minimalizując narzut związany z ciągłym otwieraniem i zamykaniem kontekstu ewaluacji w n8n.
Anatomia kontekstu: $json, $node i pułapka pierwszego elementu
Najczęściej używanym obiektem w n8n jest $json, który stanowi bezpośrednią referencję do ładunku danych (payload) aktualnie przetwarzanego elementu. Pod maską, każdy element w n8n jest obiektem posiadającym właściwość json, a czasem również binary lub pairedItem. Zmienna $json to po prostu wygodny skrót (alias) do właściwości json bieżącego elementu w pętli.
Sprawa komplikuje się, gdy musimy sięgnąć do danych z node'a, który wykonał się znacznie wcześniej w naszym workflow. Tradycyjnie, przez wiele wersji n8n, służyła do tego składnia $node["Nazwa Node'a"].json. Kiedy używasz tej konstrukcji, n8n próbuje dopasować indeks aktualnie przetwarzanego elementu do indeksu elementu z node'a docelowego. Jeśli aktualny node przetwarza element o indeksie 2, wyrażenie spróbuje pobrać element o indeksie 2 z docelowego node'a. Jeśli docelowy node zwrócił tylko jeden element (indeks 0), n8n zastosuje mechanizm fallbacku i pobierze ten pierwszy element, co często maskuje potencjalne problemy architektoniczne.
Jednak w przypadku bardziej złożonych rozgałęzień, filtrowania i łączenia danych, ten mechanizm dopasowywania indeksów (znany jako Item Linking) może prowadzić do nieoczekiwanych rezultatów. Zobaczmy przykład klasycznego odwołania, które zadziała poprawnie tylko wtedy, gdy struktura elementów jest ściśle liniowa i zachowuje relację 1:1:
{{ $node["Pobierz Klienta"].json["dane"]["id_klienta"] }}Jeśli node "Pobierz Klienta" zwrócił 10 rekordów, a obecny node przetwarza zaledwie 3 (bo reszta została odfiltrowana), poleganie na $node sprawia, że silnik n8n musi odtworzyć historię powiązań (lineage). Jeśli powiązania zostaną zerwane (np. przez niepoprawnie napisanego node'a Code po drodze), wyrażenie zwróci błąd lub, co gorsza, pobierze dane z niewłaściwego indeksu.
Ewolucja odwołań: dlaczego $items() zmienia zasady gry
Aby rozwiązać problemy z dopasowywaniem indeksów i zrywaniem powiązań w złożonych workflow, wprowadzono potężniejszą funkcję: $items(). Zamiast polegać na ukrytym mechanizmie iteracji i dopasowywania pojedynczych elementów, wywołanie $items("Nazwa Node'a") zwraca kompletną tablicę wszystkich elementów wyjściowych z danego node'a. Daje to inżynierowi pełną, deterministyczną kontrolę nad tym, jak dane są pobierane i przetwarzane.
Dzięki temu omijasz narzut związany z kontekstem pojedynczego elementu. Jest to absolutnie kluczowe, gdy chcesz na przykład zagregować dane z poprzedniego kroku, wyszukać konkretną wartość w całej tablicy na podstawie identyfikatora, lub po prostu masz pewność, że potrzebujesz zawsze pierwszego elementu z node'a autoryzacyjnego, niezależnie od tego, który element aktualnie przetwarzasz w pętli. Przykład użycia $items() do bezpiecznego pobrania tokena z node'a HTTP Request:
{{ $items("Logowanie API")[0].json.accessToken }}Warto również wspomnieć o nowoczesnym obiekcie $input, który standaryzuje sposób odnoszenia się do danych wejściowych bezpośrednio przed bieżącym krokiem. Używając metody $input.all(), pobierasz całą tablicę elementów wchodzących do aktualnego node'a. Z kolei $input.item pozwala na precyzyjne odniesienie się do bieżącego elementu i jego pełnych metadanych, w tym informacji o powiązaniach (paired items), co jest niezwykle przydatne przy zaawansowanym debugowaniu przepływów.
Czas to pieniądz, a w n8n to Luxon
Praca z datami, strefami czasowymi i formatowaniem to zmora każdego inżyniera integracji. Zamiast zmuszać użytkowników do walki z ubogim i problematycznym natywnym obiektem Date w JavaScript, n8n wstrzykuje do kontekstu wyrażeń potężną bibliotekę Luxon. Głównym punktem wejścia do operacji na czasie jest globalna zmienna $now, która jest gotową instancją DateTime z Luxona, zainicjowaną dokładnie w momencie rozpoczęcia ewaluacji danego wyrażenia.
Mechanizm działania $now uwzględnia hierarchię stref czasowych. W pierwszej kolejności n8n sprawdza strefę ustawioną w konfiguracji danego workflow. Jeśli jej nie zdefiniowano, dziedziczy strefę czasową z globalnych zmiennych środowiskowych instancji n8n (np. zmiennej GENERIC_TIMEZONE). To dlatego tak ważne jest poprawne skonfigurowanie środowiska w przypadku wdrożeń typu self-hosted, aby uniknąć przesunięć o kilka godzin w zapisach do bazy danych.
Luxon pozwala na eleganckie formatowanie, parsowanie i modyfikowanie dat za pomocą łańcuchowania metod (method chaining). Jeśli integrujesz system ERP, który wymaga daty w formacie ISO, ale z przesunięciem o 7 dni roboczych w przód i zablokowanej strefie, wyrażenie w n8n będzie wyglądać następująco:
{{ $now.plus({ days: 7 }).setZone('Europe/Warsaw').toFormat('yyyy-MM-dd HH:mm:ss') }}Możesz również parsować i normalizować daty przychodzące z zewnętrznych systemów za pomocą wbudowanych metod klasy DateTime, która również jest dostępna globalnie w wyrażeniach. Jeśli otrzymujesz z webhooka datę w nietypowym formacie amerykańskim i chcesz ją ustandaryzować do ISO przed wysłaniem do bazy PostgreSQL, zrobisz to w ten sposób:
{{ DateTime.fromFormat($json.orderDate, 'MM/dd/yyyy').toISODate() }}Debugowanie typowych błędów: undefined i zgubione referencje
Nawet najlepiej zaprojektowane workflow w n8n z czasem napotykają na problemy z niespójnymi danymi wejściowymi. Najczęstszym błędem podczas ewaluacji wyrażeń, który zatrzymuje całe wykonanie (execution), jest niesławny komunikat: "Expression is not valid: (...) cannot read properties of undefined". Wynika on bezpośrednio z faktu, że silnik JavaScript próbuje odwołać się do klucza w zagnieżdżonym obiekcie, który w danym cyklu iteracji po prostu nie istnieje.
Aby zrozumieć, dlaczego właściwość jest undefined, musisz przeanalizować ścieżkę wykonania. Czy API zawsze zwraca dany klucz, czy może go pomija, gdy wartość jest pusta (np. brak adresu dostawy)? W n8n najlepszą praktyką obronną, zapożyczoną wprost z nowoczesnego JavaScriptu, jest stosowanie operatorów opcjonalnego łańcuchowania (optional chaining ?.) oraz operatora koalescencji wartości null (nullish coalescing ??). Zamiast pisać wyrażenie, które wybuchnie przy braku danych:
{{ $json.customer.address.city }}Zastosuj wzorzec bezpiecznego dostępu. Poniższy kod w przypadku braku klucza address lub city nie rzuci błędem, lecz po prostu zwróci zdefiniowaną wartość domyślną, pozwalając workflow kontynuować pracę:
{{ $json.customer?.address?.city ?? 'Brak danych' }}Kolejną pułapką przy debugowaniu jest interfejs użytkownika (UI). Czasami podgląd wyrażenia w edytorze pokazuje błąd lub undefined, mimo że podczas rzeczywistego uruchomienia workflow (Run Workflow) wszystko działa poprawnie. Dzieje się tak, ponieważ podgląd w UI ewaluuje wyrażenie na podstawie ostatnio zbuforowanych danych (mock data), które mogą nie posiadać pełnego kontekstu powiązań elementów. Zawsze ufaj wynikom z panelu Executions bardziej niż podglądowi w czasie rzeczywistym.
Ostatnim częstym błędem jest zwracanie z wyrażenia obiektu [Object object] lub [Array]. Ma to miejsce, gdy próbujesz wstawić złożoną strukturę danych bezpośrednio do pola tekstowego, np. w node'ach wysyłających wiadomości na Slacka. Silnik wyrażeń n8n rzutuje wtedy obiekt na string. Aby zachować strukturę, musisz jawnie zserializować dane:
{{ JSON.stringify($json.myComplexObject, null, 2) }}Wnioski i checklista dobrych praktyk
Opanowanie wyrażeń to krok milowy w pracy z n8n. Znajomość mechanizmów działających pod maską pozwala nie tylko szybciej budować automatyzacje, ale przede wszystkim tworzyć niezawodne rozwiązania, które są odporne na błędy produkcyjne i nieoczekiwane zmiany w strukturze danych zewnętrznych API.
Zastosuj poniższą checklistę podczas audytu i refaktoryzacji swoich workflow:
- Używaj $items() zamiast $node, gdy potrzebujesz dostępu do pełnej tablicy wyników z poprzednich kroków, lub gdy struktura iteracji (ilość elementów) uległa zmianie w trakcie przepływu.
- Zawsze zabezpieczaj ścieżki do zagnieżdżonych właściwości za pomocą operatorów ?. oraz ??, aby uniknąć błędów undefined przerywających działanie całej automatyzacji.
- Konfiguruj globalną strefę czasową instancji n8n i używaj wbudowanych metod Luxon ($now, DateTime) zamiast natywnego, problematycznego obiektu Date.
- Pamiętaj o jawnej serializacji obiektów za pomocą JSON.stringify(), gdy mapujesz złożone dane do płaskich pól tekstowych w integracjach zewnętrznych.
- Unikaj pisania skomplikowanej logiki iteracyjnej wewnątrz
{{ }}. Jeśli wyrażenie staje się trudne do przeczytania, przenieś tę transformację do dedykowanego node'a Code wykorzystując $input.all().
Summary in English
A deep dive into the expression engine of n8n. This article explains the internal mechanics of evaluating JavaScript within double curly braces, focusing on the execution context and item linking. It covers the crucial differences between $json, $node, and the modern $items() function for robust array processing. Furthermore, it provides practical patterns for date and time manipulation using the injected Luxon library, including timezone handling. Finally, it addresses common debugging scenarios, explaining why 'undefined' errors occur and how to prevent them using optional chaining and nullish coalescing to build highly resilient workflows.
#n8n #automatyzacja #workflow #integracje #dane #lowcode