n8n-expressions について
n8n-expressionsは、n8nのワークフローを構築する人向けのスキルガイドです。式の構文やノード間参照、JavaScriptによる変換、不要なノードを避ける判断方法を説明します。
主な機能
- 01ノード名でデータを参照
- 02JavaScript式によるデータ変換
- 03Luxonによる日付処理
- 04複数行の式とコメントの記述
- 05Setノードを使う場面の判断
活用シーン
- 01ノードのフィールド内で値を計算・整形
- 02上流ノードのデータを安定して参照
- 03日付、配列、条件ロジックの処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ n8n-io/ skills --skill n8n-expressionsインストール後、Claude Code で/n8n-expressionsを入力して読み込まれたか確認します。
詳細
n8n Expressions
n8n の式言語は、{{...}} ブロック内に埋め込まれた JavaScript です。式は一度に 1 つのアイテム($json)に対して同期的に実行され、上流ノード($('Name'))、日付用の Luxon、そしてネイティブ JavaScript のほとんどの機能を利用できます。
絶対に守ること
データは $json ではなく、ノード名で参照してください。 $('Node Name').item.json.field(または .first().json.field)を使います。$json も機能しますが、ノードによってアイテムのコンテキストがクリアされた場合(Aggregate、Run for All モードの Code、分岐のマージなど)や、リファクタリングで中間ノードが追加された場合に壊れます。失敗しても通知されず、下流にはエラーなしで誤ったデータが渡されます。ノード名による参照は安定しています。
基本方針
- 単一の下流フィールドに値を渡すことだけが目的の Set ノードは作らないでください。 式は利用側に直接記述してください。同じ導出値を 2 つ以上の箇所で使う場合、導出処理が複雑な場合、または Set がサブワークフローの最終的な返却値を整形する場合は、Set を使う価値があります。下記の「Set ノードのアンチパターン」を参照してください。
- 日付には DateTime ノードではなく Luxon を使ってください。 日付の計算、フォーマット、解析はすべて式で実行できます。例:
{{ DateTime.now().minus({ days: 7 }).toISO() }}。DateTime ノードは初心者のユーザーにとってキャンバス上で見つけやすいですが、ユーザーから明示的に求められない限り、使用は避けてください。 - 一般に、余分なノードを追加するより式を使ってください。 メール本文はメールノードの本文フィールドで作成し、URL は HTTP Request の URL フィールドで計算してください。変換処理を再利用する場合、またはセクションの主な目的である場合に、追加のノードを使ってください。
- 複数行の式にはインデントとコメントを付けてください。 式が複数行にわたる場合は、通常のコードのように整形してください。n8n ユーザーの多くはコーディングをしないため、簡潔なインラインコメントでコードを説明してください。
$json.x よりノード名で参照する($('Name').item.json.x)理由
$json は「このノードに流れ込む現在のアイテム」を意味します。1 つのソースの直後にノードがあり、アイテムのコンテキストをクリアするノード(Aggregate、Run for All を使う Code、分岐のマージなど)がなければ問題ありません。
次の場合に問題が起きます。
- ソースと利用側の間にノードを挿入する(利用側が 3 つ前のノードの
$json.xを読んでいたのに、新しい中間ノードが$jsonの参照先になる)。 - ノードがコンテキストをクリアする(Aggregate、一部のマージ、データの形を維持しない Code ノード)。
- 分岐が Merge 経由で合流する。
$jsonは最後に実行された分岐の値になるため、結果が一定しません。
$('Get User').item.json.id なら参照先が明確です。途中に何があっても、常に名前を指定したノードの最初のアイテムの JSON を参照できます。
このルールを適用する例外:
分岐が合流し、安定した参照点が必要な場合は、合流地点に NoOp ノードを挿入してください。内容が分かる名前を付けます(例:Combine Inputs)。
Branch A ──┐
├─→ [NoOp: Combine Inputs] ──→ Downstream nodes use $('Combine Inputs').item.json.x
Branch B ──┘
NoOp はリファクタリング後も維持されます。Combine Inputs と利用側の間に変換処理を挿入しても、$('Combine Inputs') の参照は壊れません。
中間処理によってコンテキストがクリアされるノードのデータを下流ノードが必要とする場合、このパターンは必須です。
分岐によって出力のデータ構造が異なる場合は、NoOp ではなく Set ノードを使ってください。 NoOp は受け取ったデータ構造をそのまま渡すため、下流側ではどの分岐が実行されたかを把握する必要があります。Set ノードなら両方の分岐を同じデータ構造に正規化でき、下流では統一されたフィールドを参照できます。
// Set node: "Normalize Inputs"
name: `={{ $('Lookup by Email').item.json.name || $('Lookup by ID').item.json.full_name }}`
email: `={{ $('Lookup by Email').item.json.email || $('Lookup by ID').item.json.contact_email }}`
どちらの分岐がデータを出力したかにかかわらず、下流ノードは $('Normalize Inputs').item.json.name を参照します。
Set ノードのアンチパターン
AI エージェントがよく生成するパターン:
Webhook → Set: { customer_id: $json.body.customer_id, amount: $json.body.amount }
→ Postgres: WHERE id = {{ $json.customer_id }}
→ Email: Total is {{ $json.amount }}
Set ノードは役に立つ処理をしていません。後続の各ノードは、Webhook から直接読み取れます:
Webhook → Postgres: WHERE id = {{ $('Webhook').item.json.body.customer_id }}
→ Email: Total is {{ $('Webhook').item.json.body.amount }}
Set ノードを使う意味があるのは、次の場合です:
- 同じ派生値を複数の後続コンシューマーが使用する(派生処理が単純ではない)。
- 派生処理が複雑で、名前を付けることで読みやすくなる。
- 複数のブランチで同じ形が必要で、共通の上流参照を使うほうがすっきりする。
- サブワークフローの最後のノードとして、戻り値の契約を整える。 明示的な例外です。「単一のコンシューマー」とはすべての呼び出し元を指すため、Set は API 境界になります。サブワークフローでは任意ですが、使用が推奨されます。また、直前のノードに不要なフィールドが含まれている場合は、必要になることもあります。
n8n-subworkflowsを参照してください。 Include Other Fields: falseを設定して、アイテムからフィールドを除外する必要がある。 Set は出力の形をホワイトリストで指定する最もすっきりした方法です。これは上記のサブワークフローの戻り値を整える項目の根底にある仕組み(内部の一時フィールドが呼び出し元に漏れるのを防ぐ)ですが、後続で整った形が必要なあらゆる場面で使えます。- フィールド名を変更する必要がある。 Set を使えば、各コンシューマーの式に分散させず、1 か所で名前の変更を確認できます。
「リクエスト本文からフィールドを抽出し、1 回だけ使う」場合、Set ノードは不要です。式はその値を使うフィールドに記述します。
「1 回抽出して、複数の後続処理で使う」場合、Set ノードを使うのは妥当です。使うコンシューマーが 1 つだけなら、Set は技術的負債になります(ただし、上記の戻り値を整えるケースを除きます)。
Set ノードが必要かどうかの簡易テスト
各フィールドを参照する後続ノードはいくつありますか?
- 0 または 1 → 削除し、式をインライン化します。
- 2 以上 → 特に処理が単純でない場合は、使う意味があります。
Set ノードが複数連続している場合、過剰な抽出である可能性が非常に高いです。まとめましょう。
式でできること
単一フィールドの変換
{{ $json.name.toUpperCase() }}
{{ $json.email.toLowerCase().trim() }}
{{ $json.items.length }}
{{ $json.user.first_name + ' ' + $json.user.last_name }}
{{ `(${$json.user.phone.slice(0, 3)}) ${$json.user.phone.slice(3, 6)}-${$json.user.phone.slice(6, 10)}` }}
メソッドチェーン:.map()、.filter()、.find()、.reduce()
配列メソッドは、最も便利な式ツールの一部です。数十個のノードを置き換えられます。
{{ $json.tags.filter(tag => tag.active).map(tag => tag.name).join(', ') }}
{{ Object.values($json.scores).reduce((sum, score) => sum + score, 0) }}
// Find one matching item from another node's output
{{ $('Get Models').all().find(model => model.json.id === $json.modelId).json.modelName }}
// Filter array, then check shape
{{
$('Get User\'s Entries').all()
.map(item => item.json)
.filter(entry => entry.prize_eligible === 'eligible')
.length > 0
}}
複数ステップのチェーンには必ずインデントを付け、コメントを追加する
チェーンに 2 つ以上のメソッド呼び出しがある場合、またはフィルターロジックが自明でない場合は、複数行に整形してコメントを付けます。読者が作成者とは限らないため、コメントを付けることで、技術に詳しくない読者にも意図が伝わります。
{{
// Find all entries that are still processing AFTER 1 hour
// (used to allow re-submission since something likely went wrong)
$('Get User\'s Entries').all()
.map(item => item.json)
.filter(entry =>
entry.prize_eligible === 'processing' &&
$now.diffTo(entry.created_at, 'minutes') > 60
)
.length > 0
}}
この種のロジックは、ルーティングノード(Switch、IF)でよく使われます。コメントがないと、ほとんどのユーザーには理解できません。
.all().map() を使うときは「一度だけ実行するか」を検討する
データセット全体を処理するために $('Source Node').all().map(...)(または .filter()、.reduce())を使うと、式自体が反復処理を行います。ノードが既定のアイテムごとの実行モードの場合、入力アイテムごとに 1 回実行されますが、各実行で .all() による集約全体が行われるため、無駄な処理になり、場合によっては誤った結果になります。
次の場合は、ノードを一度だけ実行するように設定します:
- 式で
.all().map()、.all().filter()、.all().reduce()のいずれかを使用する。 - 出力がアイテムごとではなく、集約された単一の結果である必要がある。
これはノードの executeOnce: true で設定します。ほとんどのノードにこの設定があります。
const aggregateNode = node({
type: 'n8n-nodes-base.set',
config: {
executeOnce: true, // important when using .all() in expressions
parameters: {
assignments: {
assignments: [
{
name: 'totalEligible',
value: `={{
$('Get Entries').all()
.map(item => item.json)
.filter(entry => entry.eligible)
.length
}}`,
type: 'number',
},
],
},
},
},
})
executeOnce を忘れても、多くの場合は動作しますが、N 個のアイテムに対して N 倍の処理が行われます。さらに、後続の処理が 1 つのアイテムを想定している場合、N 個のアイテムが出力されます。
反対のケース:集約ではなく、アイテムごとの検索に .all() を使う場合。 .all() で別のノードを読み取り、現在のアイテムの識別情報でフィルタリングする場合は、アイテムごとの実行が必要です。各反復で異なる結果が得られるため、これは無駄な処理ではありません。
// Workflow: Get Tags (200 items) → Search Posts (10 items) → this Set Fields node.
// Each post carries a `tag_ids` array. Set Fields runs per-item (10 times)
// and resolves each post's tag_ids into the full tag objects.
tags: ={{
$('Get Tags').all()
.filter(tag => $('Search Posts').item.json.tag_ids.includes(tag.json.id))
}}
ここで executeOnce: true を設定すると、10 個の出力が 1 つにまとめられてしまいます。
この 2 つを見分ける形は次のとおりです:
$source.all()だけ(データセット全体を集約する)→executeOnce: true。$source.all().filter(... matches $other.item.json.x)(現在のアイテムを使って検索する)→executeOnceは無効のままにします。
簡易テスト: 式で .all() を使い、別のノードの .item と組み合わせていませんか? その場合、ノードはおそらく executeOnce: true にする必要があります。
反復処理と明示的なループ処理の全体像については、n8n-loops スキルを参照してください。
条件分岐
{{ $json.status === 'active' ? 'Active' : 'Inactive' }}
{{ $json.amount >= 100 ? 'Large' : ($json.amount >= 10 ? 'Medium' : 'Small') }}
日付計算(Luxon)
{{ DateTime.now().toISO() }}
{{ DateTime.fromISO($json.created_at).toFormat('yyyy-MM-dd') }}
{{ DateTime.now().minus({ days: 7 }).startOf('day').toISO() }}
{{ DateTime.fromISO($json.due).diffNow('days').days }} // days from now (negative if past)
ノード間参照($json より推奨)
{{ $('Webhook Trigger').item.json.body.customer_id }}
{{ $('Lookup customer').item.json.email }}
{{ $('Combine Inputs').item.json.coupon_code }} // NoOp convergence point
単一アイテムのノードでは、.item と .first() はほぼ同じなので、どちらかを選んでください。.first() はより明示的で、.item はより短く書けます。
IIFE のアロー関数による複数行のロジック
単一アイテムを処理するロジックが1行では複雑すぎる場合は、即時実行されるアロー関数で囲みます。
{{ (() => {
// Compute total including tax
const items = $json.line_items
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0)
const tax = subtotal * 0.08
return (subtotal + tax).toFixed(2)
})() }}
関数内では、式のスコープ全体($json、$('Node Name')、$now、Luxon)に加えて、通常の関数で使う JavaScript(const/let、if/switch、try/catch、正規表現)も使えます。
引数は使えません。 式には呼び出し元がなく、値を渡せないため、(text) => text.replace(...) を呼び出すものがありません。外側のスコープから値を直接参照してください。関数を実際に実行するには、IIFE のラップ((...)())が必要です。
{{ (() => $json.text.replace(/\b(?:foo|bar)\b/gi, 'baz'))() }}
外側の ( と末尾の )() は必須です。最初の括弧は関数式を囲み、末尾の () は関数を呼び出します。どちらかを省略すると、n8n はエラーになり、ワークフローを実行しません。
Code ノードではなく、これを使う理由は? Code ノードはサンドボックス化された VM で実行されるため、最悪の場合はおよそ 500~1000ms かかります。式の IIFE は周囲の式と同じコンテキストで実行され、安定して 1~10ms です。単一アイテムの整形だけなら、機能上の違いはなく、速度には100倍の差があります。これは、上級ユーザーがよく使う方法です。
複数アイテムの集約($input.all())、外部ライブラリ、非同期処理には、引き続き Code ノードが適しています。判断基準については n8n-code-nodes を、詳しい例と書式ルールについては n8n-code-nodes ARROW_FUNCTIONS_IN_EDIT_FIELDS.md を参照してください。
使用できるネイティブ JavaScript
String、Array、Number、Object、Map、Set、JSON.parse、JSON.stringify、Math、正規表現、Date(ただし、Luxon のみを使用してください)。
便利な定型表現
フィールドが存在しない可能性がある場合のデフォルト値
{{ $json.id || "fallback-id-here" }}
または、オプショナルチェーンを使用します。
{{ $json.user?.profile?.id ?? "anonymous" }}
クエリに渡すフィルター値には、特に便利です。undefined によってクエリが失敗しないよう、該当する行がない値をデフォルトとして渡してください。
テキストフィールドに JSON を埋め込む場合:シリアライザーの選び方
2つのシリアライザーを、用途に応じて使い分けます。
.toJsonString()は、書式が重要でないコンパクトな JSON に使います。典型的な用途は AI プロンプト です。トークン数を抑えられ、プロンプトテンプレート内で見やすくなります。{{ $('Get Data').item.json.toJsonString() }}JSON.stringify(value, null, 2)は、書式が重要な整形済み JSON に使います。典型的な用途は メール本文、Slack メッセージ、デバッグ出力 など、人が結果を読む場面です。{{ JSON.stringify($('Source Node').item.json, null, 2) }}
意識して選んでください。LLM プロンプト内での整形はトークンを無駄にし、モデルのコンテキストを煩雑にします。メール内のコンパクトな JSON は読みにくくなります。
JSON.stringify と JSON.parse:適切な使用場所
JSON.stringify と JSON.parse は、式でよく使われます。どちらも問題ありません。重要な原則は、文字列化と解析はストレージ層の操作であり、インターフェース層の操作ではないということです。
- ストレージ列がその型をネイティブに保持できない場合は、書き込むときに文字列化します。 典型的な例は、実際には配列やオブジェクトを格納する、Data Tables の
_objectが末尾に付いた文字列列です。n8n-data-tablesを参照してください。 - そのストレージ列から読み戻すときに解析します。 ストレージを所有するワークフロー内で行います。
- 文字列化した形式を境界の先まで引き継がないでください。 サブワークフローの戻り値、Webhook のレスポンス、エージェントのツール結果、後続のコンシューマーには、呼び出し元が
JSON.parseする必要のある文字列化された形ではなく、自然な形(配列は配列、オブジェクトはオブジェクト)で渡してください。
よくある間違いは、サブワークフローに「新規」経路(LLM が生成したばかりのデータで、すでに配列になっている)と「キャッシュ」経路(_object 列から読み取ったばかりのデータで、まだ文字列になっている)があるケースです。誤った対応は、キャッシュ側に「合わせる」ために新規側を文字列化することです。正しくは、キャッシュ側を解析し、どちらの分岐も出力時に同じ自然な形になるようにします。
ストレージ表現は、ストレージを所有するワークフローの内側にとどめます。その境界の外では、自然な形でやり取りします。n8n-subworkflows の SKILL.md「ストレージ形式ではなく自然な形式を返す」ではサブワークフロー側から、n8n-data-tables ではストレージ側から、この点を説明しています。
正しい型を返す:={{ ... }} で囲むタイミング
一部のノードフィールドは、n8n に式として評価させるよう指定しない限り、値を文字列リテラルとして扱います。={{ ... }} で囲むと(= プレフィックスによってフィールドが式モードになり)、内側のコードが生成する実際の型が返されます。
// String literal (default behavior)
foo: 'plain string'
// Number
foo: '={{ 100 }}'
// Boolean
foo: '={{ true }}'
// Object (the `={{ ... }}` is what makes the receiver see an object, not a string)
foo: '={{ { "valid": true, "items": [] } }}'
// Array
foo: '={{ ["a", "b", "c"] }}'
// Reference to another node's value (preserves whatever type that value already is)
foo: '={{ $("Source Node").item.json.payload }}'
型が重要になるのは、Set / Edit Fields のオブジェクト/配列フィールド(列の Type が Object または Array に設定されている場合)、HTTP Request の JSON ボディパラメーター、サブワークフローの型指定された workflowInputs.values[type] への構造化入力、エージェントのツールパラメーターなど、受け取り側のノードが型を検証するあらゆる箇所です。={{ ... }} で囲まないと文字列が渡され、受け取り側で型変換されるか、エラーになります。
前述の譲れない原則 #1 に従い、$json ではなくノード名で参照してください。
// WRONG
foo: '={{ $json.payload }}'
// RIGHT
foo: '={{ $("Source Node").item.json.payload }}'
例外は、$json が本当に適切な場合(中間変換も合流もなく)、かつフィールドが単一ソースの直後にあるノードのアイテムごとのスロットである場合です。それでも、名前付き参照のほうがリファクタリングに強くなります。
説明コメント付きの複数行式
{{
// Default to avoid query errors when user_id is missing.
// The fallback UUID is a known-empty row.
$json.id || "305f7106-6988-4651-b26a-18979641b7b5"
}}
ロジックが自明でない場合は、推奨されます。コメントは次に読む人の役に立ちます。
式ではできないこと
- 外部ライブラリを使用すること(
requireは使用できません)。 - 非同期処理(
async/await)。
$json 自体が示すのは現在のアイテムだけですが、式では $input.all()、$input.all()[3]、$('Source Node').all() などを使って、複数のアイテムを参照できます。上記の「メソッドチェーン」を参照してください。
これらについては、n8n-code-nodes を参照してください。
判断:式、Edit Fields、Code ノードのどれを使うか?
n8n-code-nodes の判断フローに従います。
1. Single-field transform → expression in the field
2. Multi-step pure logic on one item → arrow function in Edit Fields
3. Multi-source aggregation, libraries, or stateful → Code node
式をデフォルトとします。入力やスコープの要件がある場合に限り、その先の手段を選びます。
「余分なノード」の匂い
式の中にとどめるべき、余分なノードを追加しがちなケース:
| 追加するノード | 代わりに使う方法 |
|---|---|
| 日付の書式設定に使う DateTime ノード | 使用側の式で DateTime.fromISO(...).toFormat(...) を使う |
| メール本文の作成に使う Set ノード | メールノードの本文フィールドに式を直接記述する |
| 一度しか使わない派生フィールドの計算に使う Set ノード | 使用側に式を直接記述する |
| 計算してから判定するための 2 つのノード(Set + IF) | 条件式で計算する 1 つの IF |
.toUpperCase() の呼び出しに使う Code ノード | 式だけを使う |
変換のためにノードを追加すると、見た目が煩雑になり、ワークフローの実行が遅くなり、読みづらくなります。
余分なノードが適切な場合:
- 変換が複数の下流の使用箇所で再利用される。
- 変換が高負荷である(Code ノードを使うべき)。
- 変換がセクションの主な目的である(「X を計算する」という明確なステップ)。
アンチパターン
| アンチパターン | 問題点 | 修正方法 |
|---|---|---|
| Webhook の本文から 1 つのフィールドを取り出し、下流の 1 か所で使うためだけに存在する Set ノード | インライン化すべき処理にノードを追加しており、リファクタリングにも弱い | Set ノードを削除し、使用側で $('Webhook').item.json.body.x を直接参照する |
| それぞれ 1 つのフィールドを定義する Set ノードが連続している | ワークフローの水増し | まとめる。ほとんどは不要で、必要な場合も 1 つの Set ノードにまとめる |
複数の分岐と中間変換があるワークフローの深い位置で $json.x を使う | 中間ノードの追加やコンテキストのクリアで参照が壊れる | $('Source Node').item.json.x を使う。分岐が合流する場合は NoOp の合流ポイントを追加する |
| タイムスタンプの書式設定に DateTime ノードを追加する | 1 行の Luxon 式で済む処理にノードを追加している | {{ DateTime.fromISO($('Source').item.json.x).toFormat('yyyy-MM-dd') }} |
| メールの HTML を作成してから Email ノードで読み込む Set ノード | 1 つの式で済む処理に 2 つのノードを使っている | HTML をメールノードの本文フィールドに直接記述する |
Luxon の代わりに new Date($json.created_at) を使う | 書式設定や操作の機能が失われる | DateTime.fromISO($('Source').item.json.created_at) |
| 実際には 200 文字ある 1 行の式 | 読みにくい | アロー関数を使い、インデントとコメントを加えて複数行にする |
$json.foo が存在するか確認せずに $json.foo.bar.baz を使う | 中間の値がない場合にエラーになる | ?. チェーンを使う:$('Source').item.json.foo?.bar?.baz |
| 設定にすべき値を式にハードコードする | マジックストリングになる | $vars.X(n8n Variables、有料プラン)または Data Table を使う |
分岐の合流後に $json で参照する | 最後に実行された分岐が勝つため、結果が非決定的になる | 合流地点に NoOp(「Combine Inputs」)を挿入し、名前で参照する |
あらゆる式で $env.X を使う | 動作せず、実行時にエラーになる | 設定には $vars.X(有料プラン)または Data Table を使う。シークレットには認証情報システムを使う |