這篇帶你把 n8n 最容易卡關的一層打通:資料在節點之間到底長什麼樣、表達式 {{ }} 怎麼取到上一個節點的值、日期怎麼轉成台灣習慣的格式,以及 Edit Fields、If、Switch、Merge、Split Out、Aggregate 這幾個整理資料的節點各自用在哪裡,最後才談什麼時候值得打開 Code 節點寫 JavaScript。你需要一個能登入的 n8n 2.x,並且至少做過一個簡單的流程;還沒做過的話,先跟著第一個工作流程教學做一遍再回來,會好懂很多。還不確定 n8n 能拿來做什麼,可以先看 n8n 是什麼。
資料查證至 2026 年 10 月。節點名稱、選項名稱與錯誤訊息以 n8n 2.x 介面和官方文件為準,畫面上的英文照原樣保留,方便你對照。
先搞懂一件事:n8n 的資料是一筆一筆的 items
n8n 節點之間傳的永遠是一個陣列,陣列裡每一個元素叫做一個 item。每個 item 是一個物件,真正的資料放在 json 這個鍵底下,附件、圖片這類檔案則放在 binary。假設 Google Sheets 節點讀到三列訂單,往下傳的資料大概長這樣:
[
{ "json": { "order_id": "A1001", "customer": "王小明", "amount": "1200" } },
{ "json": { "order_id": "A1002", "customer": "陳美玲", "amount": 850 } },
{ "json": { "order_id": "A1003", "customer": "林志豪", "amount": "2,400" } }
]關鍵在下面這件事:大部分節點會對每一個 item 各執行一次。上面三筆資料接到 Gmail 節點,就會寄出三封信;接到發訊息的節點,就會發三則。這是 n8n 刻意的設計,你不用自己寫迴圈。反過來說,如果你其實只想發一則「今天有三筆新訂單」的彙總通知,就得先把三筆收成一筆(後面講 Aggregate),不然對方的手機會連響三次。
另一種常見的情況剛好相反:HTTP Request 呼叫外部 API,回來的是「一個 item,裡面裝著一個 orders 陣列」。這時 OUTPUT 面板只顯示 1 item,後面的節點也只跑一次。要讓每張訂單各自往下處理,就要用 Split Out 把陣列拆成多個 items。
看資料的地方在節點設定畫面(雙擊節點打開):左邊 INPUT 是進來的資料,右邊 OUTPUT 是這個節點產生的資料,兩邊都能切換 Schema、Table、JSON 三種檢視,上方會寫這次有幾個 items。養成先看筆數的習慣,n8n 裡一大半「怎麼跑出這種結果」的疑問,都是筆數跟你想的不一樣。
表達式 {{ }} 的基本寫法
節點的每個參數欄位都有兩種模式:Fixed(固定值)和 Expression(表達式)。滑鼠移到欄位上會出現切換按鈕,切到 Expression 之後,雙大括號 {{ }} 裡面寫的就是 JavaScript,括號外面的文字照原樣輸出,所以固定文字和變數可以混在一起寫:
{{ $json.customer }} 您好,您的訂單 {{ $json.order_id }} 已經出貨囉最常用的取值方式只有幾種,先把它們記熟:
| 寫法 | 拿到什麼 |
|---|---|
{{ $json.customer }} | 目前這一筆 item 的 customer 欄位,來自直接連進來的上一個節點 |
{{ $json['訂單 編號'] }} | 欄位名稱有空格、連字號或以數字開頭時,改用中括號 |
{{ $json.customer.phone }} | 巢狀欄位,一層一層用點接下去 |
{{ $('Webhook').item.json.body.email }} | 往回拿指定節點裡,和目前這筆配對的那一筆資料 |
{{ $('Google Sheets').first().json.customer }} | 拿那個節點輸出的第一筆,.last() 是最後一筆 |
{{ $('Google Sheets').all().length }} | 那個節點總共輸出了幾筆 |
{{ $input.all().map(i => i.json.customer).join('、') }} | 把目前輸入的每一筆 customer 串成一串文字 |
$json 是 $input.item.json 的簡寫,只看得到直接連進來的那個節點。資料在三、四個節點之前,就要用 $('節點名稱') 指名去拿,名稱要跟畫布上的節點名稱一字不差,大小寫和空格都算。節點改成中文名字也沒問題,例如 $('取得訂單').item.json.amount。
.item 和 .first() 的差別很重要。.item 會沿著 n8n 記錄的配對關係(pairedItem),找到跟目前這筆來自同一個源頭的那一筆,所以三筆訂單會各自拿到自己的 Email;.first() 不管你現在處理到第幾筆,永遠給你第一筆。用錯的後果是三封信都寄給同一個人,後面「常見的坑」會再細講。
拖拉欄位,讓 n8n 幫你寫
剛開始不用背語法。打開節點後,先確認左邊 INPUT 面板有資料,沒有的話按面板上的 Execute previous nodes,讓上游節點先跑一次,接著:
- 在 Schema 或 Table 檢視找到要用的欄位,按住欄位名稱,拖到右邊的參數欄位上放開。
- 欄位會自動切成 Expression 模式,填入像
{{ $json.customer }}的表達式;拖的如果是更前面節點的欄位,產生的會是$('節點名稱').item.json.欄位的寫法。 - 想拿好幾個節點以前的資料,在 INPUT 面板切換或展開那個上游節點,再從那裡拖。
- 拖進來之後可以在大括號裡繼續加工,例如在後面接
.trim()去掉空白。
Edit Fields 節點更直接:把 INPUT 的欄位拖進 Fields to Set 裡寫著 Drag input fields here 的區塊,n8n 會用原本的欄位名稱建立一個新欄位,值就是對應的表達式。
看懂表達式預覽
切成 Expression 後,欄位下方會即時顯示計算結果;按欄位旁的展開圖示可以打開完整的表達式編輯器,左邊寫,右邊的 Result 看結果。預覽常見的幾種顯示:[undefined] 代表欄位名稱打錯,或這筆資料根本沒有這個欄位;[empty] 代表欄位存在但值是空的;看到 Execute previous nodes for preview,代表上游節點還沒跑過,n8n 沒有資料可以算。如果出現 [this is a function, please add ()],就是函式忘了加括號,例如把 .first() 寫成 .first。
還有一點常被忽略:預覽只算其中一筆,通常是第一筆。第一筆欄位齊全、算得出來,不代表第五筆也沒問題。真的要確認,按 Execute step 讓節點實際跑一次,到 OUTPUT 的 Table 檢視一筆一筆看。另外,預覽是在你的瀏覽器裡計算,正式執行是在伺服器上跑,這個差別在時區問題上會再出現。
日期時間:$now、$today 與台灣日期格式
n8n 的日期處理用的是 Luxon 這套函式庫。$now 是現在這一刻,$today 是今天的午夜零點,兩個都是 Luxon 的 DateTime 物件,後面可以直接接方法。它們用哪個時區,依序看三個地方:工作流程自己的 Timezone 設定、整台 n8n 的環境變數 GENERIC_TIMEZONE,兩個都沒設時用官方預設的 America/New_York。自己架 n8n 卻沒設時區,所有時間都會跟台灣差 12 或 13 個小時,這是新手最常被整到的地方。
下面是台灣常用的寫法,假設現在是 2026 年 10 月 27 日下午 3 點 04 分(台北時間),箭頭後面是輸出結果:
{{ $now.toFormat('yyyy/MM/dd HH:mm') }}
→ 2026/10/27 15:04
{{ $now.toFormat('yyyy-MM-dd') }}
→ 2026-10-27(寫進資料庫、試算表最不容易出錯的格式)
{{ $now.setLocale('zh-TW').toFormat('yyyy年M月d日 cccc') }}
→ 2026年10月27日 星期二
{{ $now.setLocale('zh-TW').toFormat('M/d(ccc)') }}
→ 10/27(週二)
{{ ($now.year - 1911) + $now.toFormat('/MM/dd') }}
→ 115/10/27(民國年)格式字母是 Luxon 的規則:yyyy 是四位數年,MM 是補零的月、M 是不補零的月,dd 和 d 是日,HH 是 24 小時制,mm 是分鐘。小寫 m 是分鐘、大寫 M 是月份,搞反是最常見的錯。cccc 是完整的星期名稱,ccc 是簡短的,要顯示中文記得先 setLocale('zh-TW')。完整的格式字母表在 Luxon 官方文件。
加減天數、找月初月底也很常用:
{{ $today.plus(7, 'days').toFormat('yyyy/MM/dd') }}
→ 2026/11/03(7 天後)
{{ $today.minus(1, 'days').toFormat('yyyy-MM-dd') }}
→ 2026-10-26(昨天)
{{ $now.plus(2, 'hours').toFormat('HH:mm') }}
→ 17:04
{{ $today.startOf('month').toFormat('yyyy-MM-dd') }}
→ 2026-10-01
{{ $today.endOf('month').toFormat('yyyy-MM-dd') }}
→ 2026-10-31外部系統給的日期多半是字串,要先轉成 DateTime 才能加減和格式化。ISO 格式(像 2026-10-27T01:30:00Z)直接接 .toDateTime();台灣常見的 2026/10/5 這種,要用 DateTime.fromFormat() 告訴它格式長怎樣:
{{ $json.created_at.toDateTime().setZone('Asia/Taipei').toFormat('yyyy/MM/dd HH:mm') }}
→ 2026/10/27 09:30(原本是 UTC 的 01:30)
{{ DateTime.fromFormat($json['訂購日期'], 'yyyy/M/d').plus({ days: 3 }).toFormat('yyyy/MM/dd') }}
→ 2026/10/08加減時間有兩種寫法:plus(7, 'days') 是 n8n 在表達式裡加的簡寫,plus({ days: 7 }) 是 Luxon 原生的寫法,後者在表達式和 Code 節點都能用。同樣地,format() 也是 n8n 額外加的方法,Code 節點裡沒有,所以乾脆統一用 toFormat() 最省事。最後記住一件事:n8n 在節點之間傳遞時會把日期轉成字串,上一個節點輸出的日期,到了下一個節點已經是文字,要計算就得再 .toDateTime() 一次。
字串、數字與條件判斷的常用寫法
大括號裡是 JavaScript,原生的字串方法都能用,n8n 另外又加了一批實用函式。整理表單和聯絡人資料時,最常用的是這些:
{{ $json.email.trim().toLowerCase() }}
→ 去掉前後空白、轉成小寫
{{ $json.phone.replace(/\D/g, '') }}
→ 只留數字,0912-345-678 變成 0912345678
{{ $json.address.includes('台北') }}
→ true 或 false
{{ $json.message.extractEmail() }}
→ 從一段文字裡抓出 Email
{{ $json.email.isEmail() }}
→ 檢查格式是不是 Email
{{ $json.tags.split(',') }}
→ 'A,B,C' 拆成陣列數字要特別小心。從 Webhook、表單或 Google Sheets 進來的數字,很多其實是字串,而 "1200" + 100 在 JavaScript 會得到 "1200100",不是 1300。先轉成數字再算:
{{ $json.amount.toNumber() + 100 }}
→ 1300(amount 是字串 "1200" 時)
{{ Number(String($json.amount).replace(/,/g, '')) }}
→ 2400("2,400" 這種有千分位的也能轉)
{{ ($json.total * 1.05).round() }}
→ 加 5% 之後四捨五入到整數
{{ $json.total.format('zh-TW') }}
→ 1,234,567.toNumber() 只能接在字串後面,欄位本來就是數字的話不用再轉,硬轉反而會出錯。不確定是哪一種時,用 Number() 包起來最保險。
條件判斷用三元運算子,格式是 condition ? valueIfTrue : valueIfFalse,適合在一個欄位裡依條件放不同的文字:
{{ $json.amount > 3000 ? 'VIP 客戶' : '一般客戶' }}
{{ $json.paid ? '已付款' : '待付款,請於 ' + $today.plus(3, 'days').toFormat('M/d') + ' 前完成' }}
{{ $ifEmpty($json.nickname, '朋友') }} 您好
{{ $if($now.hour < 12, '早安', '午安') }}$ifEmpty(value, valueIfEmpty) 在值是 undefined、null、空字串、空陣列或空物件時改用預設值,處理表單沒填的欄位很方便。$if() 跟三元運算子效果一樣,哪個讀起來順就用哪個。條件一多、要分好幾條路走,就別硬塞在表達式裡,交給下一節的 If 或 Switch 節點。
用節點整理資料:Edit Fields、If、Switch、Merge、Split Out、Aggregate
表達式負責算出一個值,節點負責改變資料的形狀:要留哪些欄位、分成幾條路、拆成幾筆、合成幾筆。先看這張對照表,細節接著講。
| 想做的事 | 用哪個節點 |
|---|---|
| 改欄位名稱、算出新欄位、只留需要的欄位 | Edit Fields (Set) |
| 依條件分成兩條路 | If |
| 依條件分成三條以上的路 | Switch |
| 把兩條路的資料接回來,或用欄位比對合併 | Merge |
| 把一筆資料裡的陣列拆成多筆 | Split Out |
| 把多筆收成一筆,例如合併成一則通知 | Aggregate |
| 排序、只取前幾筆、去除重複、分組加總 | Sort、Limit、Remove Duplicates、Summarize |
Edit Fields (Set):把欄位整理乾淨
這個節點早期叫 Set,現在畫面上的名字是 Edit Fields (Set),在節點面板搜尋 set 或 edit fields 都找得到。它有兩種 Mode:Manual Mapping 是一欄一欄設定,JSON 是直接寫一段 JSON。新手用 Manual Mapping 就好,按 Add Field 新增欄位,填欄位名稱、選型別(String、Number、Boolean、Array、Object),再填值,值可以從 INPUT 直接拖進來。
最常讓人困惑的是 Include Other Input Fields 這個開關,預設是關的。關著的時候,輸出只有你在這個節點設定的欄位,原本其他欄位全部丟掉;打開之後,可以選擇保留全部(All)、只保留指定的欄位(Selected),或保留全部但排除某些(All Except)。發通知、寫進試算表或餵給 AI Agent 之前,先用它把欄位精簡成剛好需要的幾個,下游節點的設定會乾淨很多。
舉個實際的例子,把訂單資料整理成通知要用的三個欄位:
Mode:Manual Mapping
customer(String)
{{ $json.customer.trim() }}
total(Number)
{{ Number(String($json.amount).replace(/,/g, '')) }}
notify_text(String)
{{ $json.customer }} 您好,訂單 {{ $json.order_id }} 已於 {{ $now.setLocale('zh-TW').toFormat('M/d(ccc)') }} 出貨
Include Other Input Fields:關閉型別選 Number,值卻是 "2,400" 這種轉不過去的字串時,節點會報型別錯誤。最好是像上面一樣先在表達式裡清乾淨;真的要放寬,Options 裡有 Ignore Type Conversion Errors 可以打開。
If 與 Switch:依條件分流
If 節點有 true 和 false 兩個出口。設定條件時先選資料型別再選比較方式,例如 Number 的 is greater than、String 的 contains、Date & Time 的 is after,多個條件可以用 AND 或 OR 串起來。每一筆 item 會各自判斷,符合的往 true 走、不符合的往 false 走,所以同一次執行裡,兩條路可能都有資料。
型別在這裡特別容易出事。金額如果是字串 "1200",你選了 Number 的 is greater than,就會看到 Wrong type: '1200' is a string but was expecting a number。兩種解法:在左邊的值後面接 .toNumber(),或打開節點裡的 Convert types where required,讓 n8n 自動幫你轉型。
要分三條以上的路就用 Switch。Rules 模式是一條規則對應一個出口,例如台北、台中、高雄各一條。Options 裡的 Fallback Output 決定都不符合的資料去哪裡,預設 None 是直接丟掉,選 Extra Output 會多一個出口接住它們,建議打開,免得資料無聲無息消失;Send data to all matching outputs 打開時,符合多條規則的資料會同時送到每個符合的出口。另一種 Expression 模式是寫一個表達式回傳出口編號,例如 {{ $json.level === 'VIP' ? 0 : 1 }}。
Merge:把兩條路的資料接回來
Merge 會等所有接進來的輸入都到齊才動作。Append 模式是把各路的 items 依序接在一起;Combine 模式再分三種方式:Matching Fields 用欄位比對,像試算表的 VLOOKUP 或資料庫的 join,可以選 Keep Matches、Enrich Input 1 等輸出方式;Position 是第 1 筆配第 1 筆、第 2 筆配第 2 筆;All Possible Combinations 是每一筆都跟另一邊的每一筆配一次。另外還有能直接寫 SQL 的 SQL Query,以及只取其中一路的 Choose Branch。從 n8n 1.49.0 起,Merge 可以在 Number of Inputs 設定兩個以上的輸入。
最常見的用法是「訂單」和「客戶資料」各自從不同地方撈出來,用 Combine、Matching Fields 以客戶編號對起來,Output Type 選 Enrich Input 1,訂單一筆都不會少,每筆只是多了客戶欄位。
Split Out 與 Aggregate:拆開與收合
舊教學常提到的 Item Lists 節點,在 n8n 1.21.0 就拆成 Split Out、Aggregate、Sort、Limit、Remove Duplicates、Summarize 六個獨立節點,現在新增節點已經用不到它;官方的 3.0 升級說明也把舊的 Item Lists 列在移除清單裡,舊流程裡還有的話,趁早換掉。
Split Out 把一筆資料裡的陣列拆成多筆。在 Fields To Split Out 填陣列欄位的名稱,例如 orders,巢狀的可以寫 body.orders;Include 決定其他欄位要不要一起帶到每一筆,選項是 No Other Fields、All Other Fields 和 Selected Other Fields。
Aggregate 做相反的事,把多筆收成一筆。Individual Fields 是把指定的欄位各自收成陣列,例如把每一筆的 notify_text 收成一個 notify_text 陣列;All Item Data (Into a Single List) 是把每一筆的完整資料收成一個陣列,預設放在 data 欄位。收完只剩一筆,後面的通知節點只會發一次,內容用 join 串起來:
今天共有 {{ $json.notify_text.length }} 筆出貨
{{ $json.notify_text.join('\n') }}一條典型的流程長這樣:HTTP Request 抓訂單(1 筆,裡面是陣列)→ Split Out 拆成每張訂單一筆 → Edit Fields 整理欄位 → If 篩出今天出貨的 → Aggregate 收成一筆 → 發一則通知。拆開、處理、收回來,這個節奏會在你之後做的流程裡一再出現。
Code 節點:什麼時候才需要寫程式
原則很簡單:先用節點和表達式,真的卡住才開 Code。節點做的事在畫布上看得到,別人接手,或三個月後的你自己,一眼就懂;Code 節點像一個黑盒子,出問題得讀程式才知道。值得寫程式的情況大致有三種:同一個步驟裡要分組、排序再組成文字,用節點得串五、六個;要用正規表示式解析格式很亂的文字;或是要跨很多筆資料做有狀態的計算,例如累計、比較前後兩筆。
Code 節點有兩種 Mode。Run Once for All Items 是預設值,整批 items 只跑一次程式,用 $input.all() 拿到全部;Run Once for Each Item 是每一筆跑一次,用 $json 拿目前這筆。回傳值是陣列,每個元素是 { json: {...} },漏了 json 這一層,n8n 也會自動幫你包起來。
下面這段是 Run Once for All Items 模式,把一週的訂單依客戶加總、排名,最後輸出成一筆可以直接拿去發通知的資料:
// Mode:Run Once for All Items
const totals = {};
for (const item of $input.all()) {
const name = item.json.customer || '未填姓名';
// 有千分位的字串先去掉逗號再轉數字
const amount = Number(String(item.json.amount).replace(/,/g, '')) || 0;
totals[name] = (totals[name] || 0) + amount;
}
const ranking = Object.entries(totals)
.sort((a, b) => b[1] - a[1])
.map(([name, total], i) => `${i + 1}. ${name} ${total.toLocaleString('zh-TW')} 元`);
return [{
json: {
title: '本週客戶消費排行(' + DateTime.now().setZone('Asia/Taipei').toFormat('M/d') + ')',
message: ranking.join('\n'),
customerCount: ranking.length,
},
}];4 筆訂單(王小明 1200、陳美玲 850、王小明 300、林志豪 2,400)跑過這段程式,輸出會是一筆,message 欄位有三行:1. 林志豪 2,400 元、2. 王小明 1,500 元、3. 陳美玲 850 元。這種分組加排序再組字串的工作,用 Summarize、Sort、Aggregate 三個節點也做得到,但寫成十幾行程式反而好讀。
n8n 2.x 的 Code 節點有幾個跟以前不一樣的地方,從網路上抄舊範例時要留意:
- 程式一律在任務執行器(task runner)裡跑,而且預設不能讀環境變數(
N8N_BLOCK_ENV_ACCESS_IN_NODE預設改成 true)。API 金鑰請放進 Credentials,不要靠環境變數在程式裡讀。 format()、plus(7, 'days')這類 n8n 額外加的寫法,在 Code 節點不能用。plus(7, 'days')特別陰險,它不會報錯,結果只加了 7 毫秒,日期完全沒變;Code 節點裡請寫plus({ days: 7 })。$if()、$ifEmpty()這些輔助函式只能在表達式裡用,Code 節點裡就用一般的 if 判斷。- Code 節點產生新的 items 之後(像上面把四筆收成一筆),下游再用
$('Webhook').item往回找配對資料可能會失敗,改用.first(),或在回傳的每一筆加上pairedItem指回來源的索引。
常見的坑與錯誤訊息
Can't get data for expression
測試時最常看到的紅字,意思是表達式要拿的資料還不存在,通常是上游節點這次還沒執行。在編輯器裡一個節點一個節點測試時,只按了目前這個節點的 Execute step,前面的節點卻沒跑過,就會出現。解法是按 Execute previous nodes,或直接執行整個流程。上游如果是 Webhook、排程這類要等外部觸發的節點,可以先觸發一次,在 OUTPUT 面板把資料釘選(Pin)起來,之後測試就不用每次重送,Webhook 怎麼接可以看 Webhook 與第三方串接教學。
長得很像的還有 Node 'Check Stock' hasn't been executed:表達式引用的節點在另一條這次沒執行的分支上,例如 If 的 false 那條這次沒有資料。可以先用 {{ $if($('Check Stock').isExecuted, $('Check Stock').first().json.qty, 0) }} 判斷它有沒有跑,或調整連線,讓那個節點一定會先執行。
pairedItem 對不上
用 .item 往回找資料時,n8n 靠每一筆的 pairedItem 紀錄追溯來源。這條線斷掉或對不到,編輯器預覽會顯示 Can't determine which item to use,執行時則是下面幾種訊息之一:
Paired item data for item from node 'Code' is unavailable,或 Info for expression missing from previous node:路徑上有節點沒提供配對資訊,最常見的是 Code 節點自己產生了新的 items。在 Code 節點的回傳值加上pairedItem: 0這類索引,或下游改用.first()、.all()[index]。Multiple matching items for expression:中間經過 Aggregate、Summarize 或 Merge,多筆被合成一筆,n8n 不知道要對回哪一筆。改用.first()、.last(),或直接從合併後的欄位拿。'節點名稱' is on another branch:你引用的節點跟目前這筆資料走的是不同分支。改用.first()這類寫法,如果分支後面有 Merge 匯合,把 Merge 設成 Combine 模式。
陣列只拿到第一筆
明明有十筆,結果只處理了一筆,通常是下面其中一個原因。第一,節點 Settings 分頁裡開了 Execute Once,這個選項的意思就是只拿第一筆執行一次。第二,表達式寫了 .first() 或 $json.items[0],本來就只會拿第一個。第三,資料其實是「一筆裡面裝著一個陣列」,n8n 看到的是 1 item,這時要先用 Split Out 拆開。第四,只看了表達式預覽就以為全部都對,前面說過預覽只算一筆。
反過來,如果你要的就是把十筆內容放進同一則訊息,就用 Aggregate 收成一筆,或在表達式裡用 {{ $('取得訂單').all().map(i => i.json.customer).join('、') }} 一次拿全部。注意這種寫法放在一個會跑十次的節點裡,十次都會拿到同樣的全部內容,所以通常搭配 Execute Once,或放在已經收成一筆的節點後面。
日期差了 8 小時或 12 小時
差 8 小時,多半是外部 API 給的是 UTC 時間(字串結尾是 Z 或 +00:00),沒轉時區就直接格式化,解法是 .toDateTime().setZone('Asia/Taipei')。差 12 或 13 小時,則是 n8n 本身沒設時區,跑在預設的 America/New_York:自己架的話設定環境變數 GENERIC_TIMEZONE=Asia/Taipei 並重新啟動,或至少到流程的 Settings(編輯器右上角的選單裡)把 Timezone 改成 Asia/Taipei。Schedule Trigger 的觸發時間也吃這個設定,時區沒設好,排程就會在奇怪的時間跑。
還有兩個隱藏版。把 DateTime 直接跟字串相加,像 {{ '今天是 ' + $now }},得到的是一長串毫秒數字,不是日期,要放進文字裡一定先 .toFormat()。另一個是用原生 JavaScript 的 new Date(),它不會套用流程的時區設定,而且表達式預覽在你的瀏覽器算、正式執行在伺服器算,兩邊時區不同就會出現「預覽是對的、跑起來差幾小時」的怪事。官方建議一律用 Luxon 的 DateTime,照著做就不會踩到。
不想自己顧主機的話
上面這些坑,有不少跟主機設定有關:時區、環境變數、Code 節點的任務執行器、升級之後的行為變化。如果你想把時間花在設計流程,不想自己追版本、處理 HTTPS 憑證和備份,RoamerHost 的 n8n 全託管是獨立容器,執行次數與流程數不設上限,有專屬的 HTTPS 子網域與 Webhook 網址,時區已經預設為 Asia/Taipei,自動更新、自動備份,有中文支援,年繳 NT$5,988(平均每月 NT$499)。想先算算自己架要花多少,可以看自架 n8n 比較便宜嗎。
常見問題
Q:$json 跟 $('節點名稱').item.json 要怎麼選?
資料來自直接連進來的上一個節點,用 $json 就好,最短也最不容易出錯。要拿更前面某個節點的資料,例如經過 HTTP Request 之後還想用 Webhook 收到的原始內容,就用 $('Webhook').item.json,n8n 會幫你找到跟目前這筆配對的那一筆。如果中間經過 Aggregate、Merge 或 Code 節點,讓配對關係斷掉,就改用 .first() 或 .all()。
Q:表達式裡可以寫多行程式嗎?
可以,用立即執行的函式包起來,例如 {{ (() => { const n = $json.qty * 2; return n + 1; })() }}。不過表達式擠在一個參數欄位裡,超過三、四行就很難讀,也很難除錯。這時候改用 Code 節點,或拆成 Edit Fields 裡的好幾個欄位一步一步算,之後比較好維護。
Q:舊流程裡的 Item Lists 節點還能用嗎?
在 n8n 2.x 裡,舊流程的 Item Lists 節點還能執行,但它在 1.21.0 就被拆成 Split Out、Aggregate、Sort、Limit、Remove Duplicates、Summarize,官方的 3.0 升級說明也已經把它列為會移除的舊節點,到時候用到它的流程會直接失敗。建議現在就依照你用到的功能,換成對應的新節點。
Q:為什麼表達式預覽是對的,正式執行卻出錯?
常見原因有三個。預覽只算其中一筆,其他筆可能缺欄位或格式不同;測試時跑的是你釘選或手動送的資料,正式從 Webhook 或排程進來的資料結構可能不一樣;預覽在瀏覽器計算、正式執行在伺服器,用到不吃流程時區的寫法時結果會有落差。到 Executions 頁面打開失敗的那次執行,看每個節點實際收到的資料,比對欄位就知道差在哪。執行紀錄留太久會讓 n8n 變慢,保留多久比較好可以看執行紀錄膨脹與清理。