第一个工作流:手动触发 → 编辑字段 → 代码

报价这件事我在表格里手算了两年:数量乘单价,加运费,再除回去算单个成本。两百个袋子、单价一块八毛五、运费六十,一套下来四百三十块,单个两块一毛五。数不复杂,问题是我漏过两次运费,客户那边报价单已经发出去了。

后来这套算法搬进了 n8n,三个节点,改一个数再跑一次,两秒钟出新价。这篇文章就是从空白画布到跑通的那三段,包括我第一次把 430 算成 37060 的那个坑。

前置只有两样:n8n 已经装好能打开(没装的看《n8n 怎么安装》),画布、节点面板、执行记录这三样东西眼熟(不熟的翻《界面导览》)。不用会写代码,代码那段整块抄就行。

三个节点各管一件事

新手最容易犯的错,是把三个节点当成「三个步骤」。它们不是步骤,是三种角色。

  • Manual Trigger(手动触发):造一条空数据往下发。它不查数据库、不收请求、不看时间,纯粹是「你点一次它跑一次」。第一个工作流用它,是为了把变量减到一个——数据从哪来这件事先不用操心。
  • Edit Fields(编辑字段):往 item 里塞字段,或者改掉已有的。条数不变,进来一条出去还是一条。旧教程里它叫 Set,改名有一阵了,2.43.7 的节点面板写的是 Edit Fields (Set),两个名字都搜得到。
  • Code(代码):唯一能改条数的一步。一条能拆成多条,多条能压成一条。所以它的返回值必须是数组——这是它跟其他节点最大的不同,也是报错最多的地方。

我这台实例是 2.43.7,界面切了简体中文。要提醒一句:中文界面只翻了说明文字和按钮,节点名、参数名还是英文。你搜「编辑字段」搜不到东西,得搜 edit fields。网上那些教程写「Set 节点」「Set 节点的 Keep Only Set Fields」,对着 2.x 的界面一样对不上,别怀疑自己。

节点之间传的到底是什么

不搞清楚这个,后面每条报错都只能靠猜。

n8n 里所有在节点之间流动的数据,是一个数组,数组里每个元素长这样:

[
  { "json": { "qty": 200 } },
  { "json": { "qty": 150 } }
]

外层方括号是「一批」,里面每个对象是一条数据,n8n 管它叫 item。真正的业务字段塞在 json 这个键里。所以「处理 200 条订单」在这套模型里不是循环 200 次,而是一次给节点 200 个 item,节点自己挨个处理一遍。

这个设计带来两个后果。好消息是节点天然会批量干活,你不用写循环。坏消息是你写代码的时候,手上拿到的是一个数组,不是单个对象——row.qty 这种写法不能直接用在数组上,得先取出第一条。

从空画布到三个节点

  1. 工作流列表右上角「新建工作流」,或者地址栏直接敲 /workflow/new。我习惯后者,快。
  2. 空画布上点右上角那个 +(也可以点画布中间那个加号),面板弹出「接下来会发生什么?」。
  3. 搜索框里打 manual trigger,选中 Manual Trigger。
  4. 鼠标移到节点右侧的小圆点上,出现一个 +,点它,搜 edit fields,选中 Edit Fields (Set)。用这个方式加,连线会自己连好,不用手动拖。
  5. 再重复一次,搜 code,选中 Code。
  6. 节点名默认是英文。双击标题可以改,我改成了中文:手动触发、填写报价单、计算金额。节点一多,英文名一眼扫过去全是 Trigger、Set、Code,改中文纯为省眼睛,功能上没有任何影响。
n8n 添加节点面板里搜索 edit fields,结果为 Edit Fields (Set) 节点
搜 edit fields 只出一条结果。它下面那行小字 Modify, add, or remove item fields 说清了它的职责:只改字段,不造数据。

编辑字段:给一条数据补四个字段

双击「填写报价单」打开面板,参数只有两处要动。

Mode 保持 Manual Mapping,就是表单填法。另一个选项 JSON Output 是直接写一段 JSON,做嵌套结构时更省事,入门阶段先别用。

Fields to Set 里加四行。每行左边是字段名,中间是类型下拉,右边是值:

字段名 类型 值
product 字符串 真空压缩袋 60x80cm
qty 数字 200
unit_price 数字 1.85
freight 数字 60

类型下拉默认是「字符串」,qty、unit_price、freight 这三个必须手动改成「数字」。界面不会拦你,选错了它也不报错,安安静静把 200 存成 "200"。这个坑有多疼,等下算出来给你看。

n8n 编辑字段节点参数:Mode 为 Manual Mapping,四个字段 product、qty、unit_price、freight
四个字段,三个是数字。字段名用下划线而不是驼峰,因为等下写代码要拼 row.unit_price,少一个字符就是 undefined。

面板最下面那个 Include Other Input Fields 开关,默认是开的,意思是「输入里我没动过的字段,原样输出」。这条流程的输入是空的,开关开着关着都一样。但换成带数据的流程,关掉它等于把上游字段全砍掉,只剩你手填的四个——到时候代码里取 product 取不到,别以为是代码写错了。

代码节点:拿一条,算一遍,还回一条

双击「计算金额」。Mode 保持 Run Once for All Items(整个数组跑一次),Language 保持 JavaScript。然后把下面这段贴进编辑区:

const row = items[0].json;
const amount = row.qty * row.unit_price + row.freight;
const unitCost = Number((amount / row.qty).toFixed(2));

return [{
  json: {
    product: row.product,
    qty: row.qty,
    amount,
    unitCost,
    summary: row.product + ' ' + row.qty + ' 个,含运费合计 ' + amount + ' 元,单个成本 ' + unitCost + ' 元',
  },
}];

逐行说,只说会咬人的地方。

items[0].json 是拿第一条。这里只有一条数据,所以取 [0] 就够。多条的场景要写 items.map(...),那是下一篇的事。items 是老写法,2.x 里还能用;新的写法是 $input.first().json,两种我在这台实例上跑出来结果完全一样。我留着 items,是因为在「整个数组跑一次」这个模式下,你手上本来就是整个数组,这个名字更诚实。

toFixed(2) 不是为了好看。430 除以 200 正好 2.15,看着整齐;换成运费 55,就是 2.175,浮点算出来可能是 2.1749999999999998。写进表格或者发给客户,这种尾巴很难看。外面那层 Number() 是把它从字符串转回数字——toFixed 返回的是字符串,不转的话 unitCost 在表格里会靠左对齐。

末尾那段 return 是重点。返回数组,每个元素包一层 json。n8n 靠这个 json 键区分「这是业务数据」和「这是二进制附件」。格式不对,节点直接红给你看。

n8n 代码节点参数:Mode 为 Run Once for All Items,Language 为 JavaScript
Mode 这个下拉有两个选项,选错一个,代码就整个废掉。区别写在后面「五个真会撞上的报错」第 2 条。

跑一次,看输出

点画布下方那个橙色的「执行」。一两秒,三个节点边框变绿,每个节点右上角一个小勾,连线中间标出「1 个项目」。

n8n 三节点工作流执行完成,连线上标注 1 个项目,节点带绿色勾
连线上的「1 个项目」就是这条线上过了几条数据。等你以后加了拆条节点,这里会变成 3、5、10,一眼能看出在哪一步把数据放大了。

双击「计算金额」,右边输出区切到 JSON 标签:

n8n 代码节点输出的 JSON:amount 430、unitCost 2.15 与拼好的 summary
430 是 200×1.85+60,2.15 是 430÷200。手算一遍对得上,说明三个节点接得没错。

顺便说一句容易误会的:这个「执行」按钮只在你点的时候跑。Manual Trigger 的工作流不会因为被发布就自己动起来,它没有「自己动」的机制。要定时,得换成 Schedule Trigger 那类触发器。

附:整份工作流 JSON

懒得点节点,可以整份导进来。复制下面这段,在画布空白处按 Ctrl+V,三个节点会直接落到画布上。走菜单也行:右上角 ⋯ → 导入 → 从文件导入,先把内容存成 .json。

这段我自己粘进一个空白工作流跑过一遍:三个节点、两条连线、位置都对,执行完 amount 也是 430。

{
  "name": "第一个工作流",
  "nodes": [
    {"parameters":{},"id":"m1","name":"手动触发","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[-368,0]},
    {"parameters":{"assignments":{"assignments":[
      {"id":"a1","name":"product","value":"真空压缩袋 60x80cm","type":"string"},
      {"id":"a2","name":"qty","value":200,"type":"number"},
      {"id":"a3","name":"unit_price","value":1.85,"type":"number"},
      {"id":"a4","name":"freight","value":60,"type":"number"}
    ]},"includeOtherFields":true,"options":{}},"id":"m2","name":"填写报价单","type":"n8n-nodes-base.set","typeVersion":3.5,"position":[-144,0]},
    {"parameters":{"jsCode":"const row = items[0].json;nconst amount = row.qty * row.unit_price + row.freight;nconst unitCost = Number((amount / row.qty).toFixed(2));nnreturn [{n  json: {n    product: row.product,n    qty: row.qty,n    amount,n    unitCost,n    summary: row.product + ' ' + row.qty + ' 个,含运费合计 ' + amount + ' 元,单个成本 ' + unitCost + ' 元',n  },n}];","mode":"runOnceForAllItems"},"id":"m3","name":"计算金额","type":"n8n-nodes-base.code","typeVersion":2,"position":[80,0]}
  ],
  "connections": {
    "手动触发": {"main": [[{"node": "填写报价单", "type": "main", "index": 0}]]},
    "填写报价单": {"main": [[{"node": "计算金额", "type": "main", "index": 0}]]}
  },
  "settings": {"executionOrder": "v1"}
}

怎么算真的跑通了

四条,都能自己看,缺一条都不算完。

  1. 三个节点绿框带勾,两条连线上都标「1 个项目」。数字不是 1,说明中间有节点改了条数,回头查。
  2. 代码节点输出 JSON 里 amount 是 430,unitCost 是 2.15。
  3. 执行记录里能找到这一次。手动点的也留记录,别以为只有定时任务才进列表。
  4. 把 freight 从 60 改成 100,再跑,amount 应该变成 470。改一个输入、看输出跟着变,这一条比前面三条都硬——只看一次 430,有可能是巧合。

第四条我建议每次都做。跑通一次和跑对,中间隔着一层。

五个真会撞上的报错

1. Code doesn't return items properly

返回了字符串。我实测就写了一句 return "共 430 元";,节点立刻变红,右下角弹这条。

n8n 代码节点返回字符串时报错:Code doesn't return items properly
红框、白叹号、一句英文报错。前面两个节点还是绿的,说明错就错在最后一个。

有意思的是,return { amount: 430 } 这种不包数组、不包 json 的写法能跑。Code 节点会自动给你补上 json 键和方括号,文档里写明了这个行为。所以这条报错不是「格式不够标准」,是「你返回的东西根本不是数据」——字符串、数字、null 都补不了。

2. items is not defined [line 1]

Mode 选成了 Run Once for Each Item,代码里还在用 items。逐条模式下 n8n 一次只喂一条进来,当前这条叫 $json,取数量要写 $json.qty。两个模式的区别就一句话:整个数组跑一次,还是每条跑一次。写代码前先看一眼这个下拉。

3. 一个工作流中只允许一个"Manual Trigger"节点

往已经有触发器的工作流里再塞一个触发器。我这次犯得更蠢:把一段带触发器的 JSON 粘进一个已有工作流的画布,弹出「无法插入节点」,下面跟着这句。想对照着试,先开一个新工作流再粘。

4. 输出一片 undefined,不报错

Edit Fields 里字段叫 unit_price,代码里写成 row.unitPrice。大小写和下划线差一个字符,n8n 不管,JS 也不管,结果就是 undefined 一路传下去,summary 里出现「含运费合计 undefined 元」。这种错最难查,因为它不红、不响,只是数字不对。

5. amount 变成 "37060"

字段类型选了字符串。六十和 "60" 在 JS 里看着一样,但 + 遇到字符串就改成拼接:370 加 "60" 得到 "37060",不是 430。

n8n 编辑字段里 freight 类型选成字符串后,代码输出 amount 变成 37060
左边输入里 freight 前面的图标从 # 变成了 T,右边 amount 就成了带引号的 "37060",unitCost 跟着变成 185.3。整条流程不报错,还理直气壮写着「合计 37060 元」。

第五条是这篇最值钱的一条。我第一次跑出这个数,盯着 37060 想了半分钟才反应过来是字符串拼接——因为代码看上去毫无问题,问题在上一个节点的类型下拉里。

Code 节点的三条边界

Code 节点跑在 n8n 自己的进程里,但有沙箱:读不了本地文件,发不了 HTTP 请求。要动文件用 Read/Write File From Disk 节点,要发请求用 HTTP Request 节点。这不是残缺,是把「一段随手贴进来的 JS 能干出多大的事」压到了最小。

但它挡不住的三件事,得自己管。

别人给的工作流 JSON 不要随手粘。粘进去就等于在你自己实例上执行别人写的 JS。我这台实例粘 JSON 是直接落节点的,不弹确认、不问来源。真要试,先丢进一个空的测试实例,看完代码再决定跑不跑。

密钥别硬编码在代码里。工作流可以整份导出、发给同事、贴进群里。代码里写死的 api key 会跟着 JSON 一起走。该放凭据节点,让 n8n 加密存。

自托管能装第三方 npm 包。默认关着,要手动开环境变量。开了等于给 Code 节点开了任意代码执行的口子。一个人用的实例随意,多人共用的实例我建议一直关着。

小结

三个节点,一条链:触发器造数据,编辑字段补数据,代码算数据。真正要记的不是这三个节点怎么点,是那个「数组套 json」的形状。后面 IF、Merge、循环、错误分支,全都是这个形状的变体。

代码能不用就不用。这个报价用 Edit Fields 加表达式也能算,{{ $json.qty * $json.unit_price + $json.freight }} 一行完事,还少一个节点。我最终选了 Code,是因为后面要同时算三种口径的报价,节点里堆表达式比写代码更难维护。你只算一个数,就别上代码。

类型那个坑,建议你现在就故意把 freight 改成字符串跑一次,亲眼看着 430 变成 37060。看十遍文字,不如自己错一次。

接着往下

上一篇是《n8n、Zapier、Make 怎么选》。装机看这篇,界面不熟翻这篇。整门课的目录在这里。

参考资料

  • Manual Trigger:只在你点执行时运行,不提供自动触发;一个工作流只允许一个
  • Edit Fields (Set):Manual Mapping 与 JSON Output 两种模式,Include in Output 的作用
  • Code:两种 Mode 的区别、语言选项、不能访问文件系统和发起 HTTP 请求
  • Understand n8n's data structure:数组套 json 的来历,以及 Code 节点自动补 json 键的说明
  • Use built-in shortcuts:表达式里的内置变量与方法,哪些在 Code 节点里不适用

最后更新:2026 年 10 月 28 日,用的是 n8n 2.43.7。

THE END