n8n-expressions-official について
n8n のワークフロー作成者向けに、JavaScript 式、ノード間参照、日付処理、データ整形を解説するガイドです。安定した参照方法と不要なノードの削減を重視します。
主な機能
- 01ノード名でデータを参照
- 02Luxon による日付処理
- 03インライン式でノードを削減
- 04JavaScript の配列メソッド
- 05複数行ロジックの整形とコメント
活用シーン
- 01上流ノードの特定フィールドを参照
- 02フィールド内で日付や形式を計算
- 03ノード出力のフィルタリングと集約
- 04Set ノードの必要性を判断
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ n8n-io/ skills --skill n8n-expressions-officialインストール後、Claude Code で/n8n-expressions-officialを入力して読み込まれたか確認します。
詳細
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-officialを参照してください。 Include Other Fields: falseを設定して、アイテムからフィールドを削除する必要がある。 Setは出力のデータ構造をホワイトリスト化するのに最適な方法です。これは、上記のサブワークフローの戻り値を整える項目の根底にある仕組み(内部の一時フィールドが呼び出し元に漏れないようにすること)ですが、後続で整ったデータ構造が必要なあらゆる場面で使えます。- フィールド名を変更する必要がある。 Setを使うと、すべてのコンシューマーの式に分散させず、ひとつの場所で名前の変更を確認できます。
「リクエストボディからフィールドをひとつ取り出して一度だけ使う」場合は、Setノードを使いません。式は、そのフィールドを使用するノードに記述します。
「一度取り出して、複数の後続処理で使う」場合は、Setノードを使うのが妥当です。使用するコンシューマーがひとつだけなら、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())を使ってデータセット全体を処理する場合、式自体が反復処理を行います。ノードがデフォルトのアイテムごとの実行モードだと、入力アイテムごとに一度実行されますが、そのたびに.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回実行されます。さらに、後続ノードがアイテムをひとつだけ想定している場合、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個の出力がひとつにまとめられてしまいます。
両者の違いは次のとおりです。
$source.all()のみ(データセット全体を集約する)→executeOnce: true。$source.all().filter(... matches $other.item.json.x)(現在のアイテムに対応する値を検索する)→executeOnceをオフのままにします。
簡易チェック: 式で、別のノードの.itemと組み合わせずに.all()を使用していますか? その場合、ノードはexecuteOnce: trueにすべき可能性が高いです。
反復処理と明示的なループ処理について詳しくは、n8n-loops-officialスキルを参照してください。
条件式
{{ $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~1000 ms かかります。式の IIFE は周囲の式と同じコンテキストで実行され、安定して 1~10 ms です。単一アイテムの整形だけであれば、機能上の違いはなく、速度に 100 倍の差があります。これは熟練ユーザーによく使われる方法です。
複数アイテムの集約($input.all())、外部ライブラリ、非同期処理には、引き続き Code ノードが適しています。判断フローについては n8n-code-nodes-official を、より詳しい例と書式ルールについては n8n-code-nodes-official 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 のプロンプト内での整形出力はトークンを無駄にし、モデルのコンテキストを clutter で埋めてしまいます。メール内のコンパクトな JSON は読みにくくなります。
JSON.stringify と JSON.parse:使うべき場所
JSON.stringify と JSON.parse は式でよく使われます。どちらも問題ありません。重要な原則は、文字列化と解析はストレージ層の操作であり、インターフェース層の操作ではないということです。
- 型をネイティブに保持できないストレージ列に書き込むときは、文字列化します。 典型的な例は、実際には配列またはオブジェクトを格納している、Data Tables の
_objectが末尾につく文字列列です。n8n-data-tables-officialを参照してください。 - そのストレージ列から読み出すときは、解析します。 ストレージを所有するワークフローの内部で行います。
- 文字列化した形を境界の先まで伝播させないでください。 サブワークフローの戻り値、Webhook のレスポンス、エージェントのツール結果、後続のコンシューマーには、文字列化された殻(呼び出し元が
JSON.parseする必要がある形)ではなく、自然な形(配列は配列、オブジェクトはオブジェクト)で渡す必要があります。
よくあるミスは、サブワークフローに「新規」パス(LLM が生成したばかりのデータで、すでに配列になっている)と、「キャッシュ」パス(_object 列から読み出したばかりのデータで、まだ文字列になっている)があるケースです。誤った対応は、キャッシュの形に「合わせる」ために新規パスを文字列化することです。正しい対応は、キャッシュの形を解析して、両方の分岐が出力時に同じ自然な形になるようにすることです。
ストレージ表現は、ストレージを所有するワークフローの内部に置きます。その境界の外では、自然な形でやり取りします。n8n-subworkflows-official の SKILL.md にある「Return natural shapes, not storage shapes」ではサブワークフロー側からこの点を説明しており、n8n-data-tables-official ではストレージ側から説明しています。
適切な型を返す:={{ ... }} で囲む場合
一部のノードフィールドでは、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 が本当に適切な場合(中間変換も合流もない場合)で、そのフィールドが1つのソースの直後にあるノードのアイテムごとのスロットである場合です。その場合でも、名前付き参照の方がリファクタリングに強くなります。
説明コメント付きの複数行の式
{{
// 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-official を参照してください。
判断:式、Edit Fields、Code ノードのどれを使うか?
n8n-code-nodes-official の判断ツリーに従います。
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 を使う。シークレットには認証情報システムを使う |