做了两年多n8n工作流,我见过太多人卡在“会用节点”但“不会写表达式”这个阶段。节点拖拖拽拽谁都会,但一旦遇上数据清洗、条件分支、动态参数这类场景,立刻露怯——要么在Code节点里写一堆for循环,要么把表达式糊成一长串让人看都不想看。其实n8n内置方法和变量体系早就把这块路铺好了,只是很少有人系统讲清楚它们之间的关系。这篇教程就专门拆这两个东西:n8n里到底有哪些内置方法、哪些变量,它们各自在什么场景下发力,以及怎么组合起来设计一套真正“如虎添翼”的工作流。适合刚接触n8n表达式的新手,也适合写了几个工作流但总觉得代码块太多、想精简的老手。
1. 从一次“翻车”说起:为什么变量与内置方法是n8n的命门
先讲个我自己踩过的坑。早先帮一个客户搭客户线索自动同步流程,从Webhook接收表单数据,经过数据清洗,最后写入表格。图倒是很漂亮,但我在“数据格式化”节点里写表达式时,直接写了{{$json.body.email}}——结果一执行,对方发过来的字段名是Email,大小写对不上,整条数据流直接断掉。后来改成{{ $json.body.Email || $json.body.email }}才解决。
这件事给我的教训很直接:n8n节点的拼装只是骨架,变量与内置方法才是让数据真正“流动”起来的血液。如果你不理解每个节点输出什么、表达式能拿到什么、内置方法怎么把一个数据变成另一个结构,那工作流做再复杂也只是空中楼阁。
1.1 变量体系全览:全局、环境、工作流与节点级
n8n里的变量不是一个笼统的概念,它其实分四个层级,搞清楚这四个层级,你就能知道“在这个位置到底该用什么”。
第一层是环境变量,也就是$env。这些变量通常定义在部署层面,比如用Docker跑n8n时,通过N8N_ENCRYPTION_KEY、WEBHOOK_URL这类环境变量注入。在表达式里可以通过$env.MY_VARIABLE读取。我一般把API密钥、数据库连接串、第三方服务的端点地址放在这里,而不是硬编码进工作流,因为一旦要换环境,改一处就行。
第二层是全局变量,对应n8n的Settings里的Variables功能。这一层最适合放“跨工作流共享的配置”,比如统一的官方电话、企业统一的社会信用代码、固定的折扣比例。用{{ $vars.discount_rate }}就能在任何工作流里引到,比每个流程里单独写死要优雅得多。
第三层是工作流级静态数据,也就是$getWorkflowStaticData()。这玩意儿很多人不熟悉,它允许你在同一个工作流的多次执行之间保存少量数据,比如记录上次同步的游标、累计计数、去重用的ID集合。它不像数据库那样强大,但解决“我这次执行需要知道上次执行到哪儿”这种问题简直不要太合适。
第四层才是大家天天打交道的节点级数据:$json、$node、$input、$items这些。它们描述的是当前这条数据流上,这个节点能从上游拿到什么、命令能访问哪个节点的输出。这一层是n8n内置方法和变量的主战场,后面的实操大部分都在这里进行。
1.2 内置方法到底是什么:一张图拆解n8n数据流转
我经常在交流群里看到有人问“内置方法是什么”,其实用一句话说:内置方法就是n8n表达式引擎里预置好的一批函数和对象,让你不用写完整JavaScript,也能完成取值、转换、判断、格式化这些常见动作。
你可以把n8n的数据流想象成一条传送带。传送带上的每一件“包裹”就是一条Item,包裹里装着json数据、二进制数据、还有索引信息。n8n的表达式就是你在传送带旁边伸出一只手,通过$json取出当前包裹里的某个字段,用$node去翻之前经过的某个节点留下的记录,再用JSON.parse这类内置方法把包裹里一坨字符串解开,重新打包。整个过程中,你不需要懂JS也能做,但如果你稍微理解一点JavaScript,再叠加n8n预置的这些方法,能发挥的余地会成倍增加。
理解这条“传送带”很关键,因为n8n里超过70%的工作流bug都不是节点配置错了,而是使用者根本没搞明白当前节点上下文里到底能拿到哪些数据。比如你用“Edit Fields”节点做了字段映射,下一个节点里$json的内容就和上游完全不一样;你用了Loop节点,每个循环里$json其实是数组里的一项而不是整个数组。这些经验,变量和内置方法体系里都有对应的规则,只是没人给你串讲,所以总觉得磕磕绊绊。
2. 内置方法核心函数拆解:每个表达式背后的逻辑
说实话,n8n的官方文档把内置方法的语法写得很全,但读起来像天书。这里我不罗列全部,只挑日常工作中使用频率最高的那一批,告诉你它们到底是干嘛的、为什么这么设计、怎么用才不踩坑。
2.1 常用内置方法的定位与用法
先讲最基础的“四大金刚”:$json、$node、$input、$items。
$json是当前节点输入数据的JSON部分,绝大多数表达式的主力。记住一个细节:在遇到分支节点之后,$json的字段结构取决于你连接的是哪个出口,而不是上游节点。很多新人从IF节点的false出口连出来,结果还在attempt用true出口的字段,debug半天才反应过来。
$node["节点名"]用来访问指定节点的输出。这个非常强大,你可以在当前节点里引用流程中任意一个节点的执行结果。比如在“聚合”节点后面,你想看看最初Webhook节点收到了什么原始数据,直接用$node["Webhook"].json.body就能拿到。但要注意,如果这个节点在本次执行中没有被运行(比如被分支跳过了),拿到的就是undefined,不会报错但会一路传播空值。
$input代表当前节点的直接上游输出,适合在写“这个节点到底吃了什么”时快速查看。它等价于在大多数场景下的$json,区别在于$input更明确地指向“进入当前节点的数据”,而$json有时候会被n8n在某些节点内部重新赋值。我习惯在Code节点里用$input.first().json取第一条,在表达式里直接用$json,各取所长。
$items则是当前上下文可访问的全部Item数组,配合索引使用。比如$items(0).json.name就是当前批次第一条数据的name字段。这在带有多条数据的循环或批量处理场景下特别有用,你可以从不同索引的数据里拼出新的结构。
除了这几个对象,n8n还内置了一批方法,比如$now返回当前时间、$executionId返回当前执行ID、$workflow返回工作流信息、$runIndex返回当前是第几次运行(重试时很有用)。它们看起来不起眼,但组合起来就厉害了。比如我经常用$executionId拼进错误报警消息里,方便快速追溯到具体某次运行日志。
2.2 表达式函数速查:字符串、数组、日期必备
n8n表达式里可以直接调用一整套JavaScript内置函数,这也是很多人觉得“n8n难道不是零代码吗”的疑问来源。其实它更像“低代码”:简单场景拖节点,复杂逻辑写表达式,内置方法就是给你提供了一条平滑的过渡地带。
字符串处理三件套:toUpperCase、toLowerCase、replace。比如把用户输入的邮箱统一转小写再判断,直接{{ $json.email.toLowerCase() }}。replace是正则的好帮手,{{ $json.phone.replace(/[^0-9]/g, '') }}能把电话里的空格、横杠全部剥掉,只留下数字,这个我在电话号码清洗场景里用了无数次。
数组处理就更多了:map、filter、reduce、find。在n8n的表达式编辑器里,你可以对一个数组字段做{{ $json.orders.map(o => o.price).reduce((a,b) => a+b, 0) }},把子订单的价格全部累加出来。很多人在这一步就转向Code节点了,其实用表达式完全能做,而且可读性不差。
日期处理上,n8n在内置方法里封装了一个DateTime对象,比如{{ DateTime.fromISO($json.start_time).plus({days: 1}).toISO() }},比原生JavaScript的Date对象好用得多。在写“计算30天后的截止日期”“判断当前是否在工作时间内”这些逻辑时,用DateTime能省一半时间。
还有一个容易忽略的宝藏:JSON.parse和JSON.stringify。当你从Webhook收到一大段JSON字符串时,没有parse之前它就是一个文本,你没办法优雅地取里面的字段;当你想把一个对象传给HTTP Request的body时,不stringify就会得到[object Object]这样的天坑。这两个函数几乎出现在我每一个工作流里,属于内置方法中的“生存技能”。
3. 从“会写”到“会设计”:变量与内置方法驱动的高阶工作流
掌握了基础语法,接下来进入真正好玩的部分:怎么在工作流里灵活组合变量和内置方法,设计出健壮、可维护、别人看了都竖大拇指的流程。这里我从三个最常见也最实用的场景展开。
3.1 场景一:Webhook + 变量做动态条件分支
假设你要做一个线索分发流程:外部系统通过Webhook推送新线索,字段包括company_size(公司规模)、industry(行业)、lead_score(评分)。你需要根据不同条件把线索分给不同销售组。
第一步,在Webhook节点后加一个“Switch”节点。Switch节点支持多条规则,但很多人在这里只用固定等于,遇到“大于某个数”就不知道怎么办。其实n8n的Switch完全可以写表达式作为规则值。例如:
- 规则1:
{{ $json.lead_score >= 80 && $json.company_size > 500 }}→ 输出“大客户组” - 规则2:
{{ $json.industry == 'SaaS' }}→ 输出“SaaS组” - 规则3:默认 → “普通组”
第二步,在分支里需要使用同一个全局变量来标记线索来源。比如在Settings里定义了一个变量lead_source => "流量通道",你可以在后续的消息节点中通过{{ $vars.lead_source }}引用,这样以后改来源名称只需要改设置,不用每个分支都去改文案。
这个场景的坑在于表达式里的大小写和空格。company_size是下划线,lead_score也是下划线,如果外部系统的字段名是companySize,你直接用$json.company_size铁定拿不到。我一般在Webhook后面会跟一个“Edit Fields”节点,把所有字段名统一成自己这边好用的命名规范,再做判断。这一步相当于给下游建了一条干净的数据管道,后续所有节点都省心。
3.2 场景二:循环聚合与数据清洗
批量处理数据时,n8n有两种套路:一种是用Split Out把数组拆成多行,再用Loop节点逐条处理;另一种是把整个数组直接丢给Code节点一次性搞定。用内置方法可以把这两种套路揉在一起,做得很漂亮。
比如你要从一个接口拉回一批订单,每条订单里包含items数组,你需要把所有的items取出来、计算每件商品的税费,最后汇总成一个新的数组。
我的做法是:先用Split Out节点把订单拆开,拿到每条订单的items子数组后,再用一个“Code”节点配合n8n的$input.all()方法遍历。Code节点里可以这么写:
const results = []; for (const item of $input.all()) { const orderItems = item.json.items || []; for (const product of orderItems) { results.push({ order_id: item.json.order_id, product_name: product.name, tax: product.price * 0.06, total: product.price * 1.06 }); } } return results;注意这里我用了item.json.items而不是item.json["items"],效果一样,但后者在处理字段名带特殊字符时更稳。这种写法比嵌套好几个Loop节点要简洁太多,也方便调试。
如果你不想用Code节点,纯表达式也能做,但可读性会差一些。比如在“Aggregate”节点里,你可以对$json.items做{{ $json.items.length }}取数量,用{{ $json.items.map(i => i.price).reduce((a,b)=>a+b,0) }}汇总金额。这些都是内置方法直接支持的。
数据清洗的另一块是空值和类型。现实世界的数据永远没有理想的干净,订单金额可能是字符串"100",也可能是数字100,还可能是null。我在Code节点的开头永远会加上一行防御性代码:
const price = Number(item.json.price) || 0;这样无论来的是字符串、整数、浮点数还是没值,都能得到一个安全的数字。这个习惯帮我省了太多排查时间。
3.3 场景三:HTTP请求中的动态参数与Token刷新
这是最容易暴露变量使用水平的场景。你要调用一个第三方API,发送POST请求,body里要带上当前时间戳、上游传下来的用户ID,还要在Authorization头里带上一个动态更新的access_token。
很多人的第一版是:在HTTP Request节点的Headers里直接填死Token。但Token是会过期的,一过期整个自动化就死给你看。合理做法是使用n8n的Credentials功能,把API的密钥信息统一管理,然后在HTTP节点里通过{{ $credentials.apiKey }}引用。
body里的动态参数,用表达式做比用节点拼接更高效。比如:
{ "user_id": "{{ $json.user_id }}", "timestamp": "{{ DateTime.now().toMillis() }}", "sign": "{{ $json.user_id + '-' + $env.SIGN_KEY }}" }这里DateTime.now().toMillis()是n8n内置方法里面的一个类方法,直接取当前Unix时间戳,省得自己再调原生Date函数。$env.SIGN_KEY则是从环境变量里读取的签名密钥,不会出现在工作流内容里,安全性好得多。
如果要实现“Token快过期时自动刷新”,我一般是用一个独立的子工作流,或者在主流程前面加一个“Execute Workflow”节点去单独刷新Token,把结果存储到工作流静态数据$getWorkflowStaticData()里。后面每个HTTP请求前,先用{{ $getWorkflowStaticData().token }}读出来,如果为空或过期再进行刷新。这比在每个请求里重复调用登录接口要省太多时间,也避免API限流把你整个流程卡死。
4. 常见问题与避坑实录
接触n8n的这两年多,我在变量和内置方法上踩过的坑、帮别人排查过的问题,没有一百个也有八十个。这里挑几个最高频的整理成速查表,并附带排查思路,希望能帮你少走弯路。
4.1 变量作用域与命名冲突
现象:同一个变量名,在A节点能取到值,在B节点取到undefined,或者拿到了一个很奇怪的值。
原因:n8n的节点级数据是作用在Item上的,不是全局可随意访问的。更隐蔽的问题是变量命名冲突:你从Webhook拿到的字段叫status,自己在Edit Fields里也给一个字段取名叫status,然后又用$vars定义了一个全局变量status。三个来源、三个值,表达式的优先级会让你根本分不清拿的是哪个。
解决思路:我给自己定了一个很简单粗暴的命名约定:
$json字段尽量全小写下划线,且不与任何变量同名。$vars里的全局变量统一加前缀,比如app_或site_。- 环境变量
$env的命名用全大写加下划线。
这样一眼就能在表达式里区分出来:
{{ $json.order_id }} // 当前item的订单号 {{ $vars.app_name }} // 全局应用名 {{ $env.SIGN_KEY }} // 环境变量签名密钥这套约定听起来简单,实战中极其管用。尤其是工作流规模变大以后,避免踩到命名冲突就是在给自己省时间。
4.2 空值与类型转换的坑
现象:{{ $json.name }}明明有值,但显示出来是null;或者你想把一个数字拼到字符串里,结果出现了undefined。
原因:n8n表达式对undefined和null的处理有时候会让人迷惑。当一个字段不存在时,表达式直接返回undefined,而不是空字符串。在模板字符串里就会变成你好,undefined。另外,很多外部API返回的数字是字符串,比如"10",你拿来做大于0判断时会发现JavaScript的隐式类型转换并不总是按你预期走。
排查步骤:我一般按三步走:
- 先用“Show Data”节点把当前节点的完整输出打出来,确认字段名和值到底长什么样。
- 在表达式里主动做类型转换,数字就
Number(...),字符串就String(...),数组就Array.isArray(...)先判断一下。 - 对可能出现空值的地方统一兜底,比如
{{ $json.age || 0 }}或者{{ $json.email || '未填写' }}。
这个习惯在数据来源混杂的流程里特别有价值。你永远不知道上游系统什么时候会抽风不发某个字段,提前兜底好过上线之后半夜收报警。
4.3 排查技巧与调试姿势
n8n的调试手段其实很够用,只是很多人不会用。我这里分享三个我每天都会用到的调试姿势。
第一个是“Show Data”节点大法。很多初学者觉得这个节点多此一举,其实它是排查问题的最好起点。每连完一个节点,加一个Show Data看一眼输出结构,确认字段名大小写、嵌套层级、数据类型都对了,再进行下一步。我自己的习惯是:复杂流程里每隔两三个节点就放一个Show Data,调试完再一次性删掉。
第二个是表达式编辑器里的实时预览。n8n的表达式编辑框右侧会实时显示当前执行环境下这个表达式的值,不需要跑完整条流程。写表达式时养成习惯:在提交之前先在预览栏看一眼结果是否符合预期。这一步能帮你当场发现类型错误,而不是等到执行到那个节点才爆红。
第三个是Code节点里临时加日志。有时候表达式觉得没问题,实际跑起来却不对,我就会在Code节点里写一个console.log(JSON.stringify($input.all())),然后看n8n的执行日志。这比盲猜快得多,尤其是上游数据来自第三方API时,先确认原始数据长什么样,再谈后面的字段映射才有意义。
我这里再补一个很多人会踩的坑:节点名称带空格时,$node["Node Name"]这种引用方式中括号内部必须用双引号,不能省。我见过太多人在表达式里写$node[Salesforce].json,然后怎么都取不到值,就是因为没有给节点名加上引号。
5. 进阶心得:把工作流当代码来维护
写了这么多场景和问题,最后还想聊一个比较抽象但很重要的心得:把n8n工作流当成真正的代码项目来维护,而不是“拖出来的图”。这听起来有点反直觉,因为n8n的卖点就是可视化,但在变量和内置方法用多了以后,你会发现工作流的复杂度主要来自数据流逻辑,而不是节点连线。
我在团队里推行的做法有三条。第一条是给关键节点写备注,尤其是那些用了复杂表达式或者内置方法的节点。备注里写清楚这个表达式在算什么、数据来源是什么、返回值类型是什么,三个月后你回来看工作流依然能快速恢复上下文。
第二条是做版本管理。n8n企业版本身支持版本历史,但如果是自托管或者社区版,我有一个土办法:对非常重要的生产工作流,每次大改动前手动导出一份JSON存档。哪怕只是改了一行表达式,也存档一次。成本极低,但能在上线后出问题时一键回滚,这比什么都重要。
第三条是统一的错误处理。千万别让错误一路抛到底。用“IF”节点或者n8n的“Error Trigger”在关键步骤把异常截住,配合变量$executionId拼出包含完整上下文的错误消息,发送到企业微信、钉钉或者Slack。这样出了问题,你第一时间看到的是有效信息,而不是一堆干巴巴的执行失败日志。
说回变量和内置方法,我个人在实际操作中的最大体会是:它们不是零代码的敌人,反而是让n8n变得更强的那块跳板。你不需要成为JavaScript高手,只要掌握十几个常用的内置方法、分清楚四个变量层级、养成数据验证的习惯,就已经能覆盖九成以上的自动化场景。剩下的一成,等真正遇到再学也不迟——但你现在掌握的这套体系,会让你遇到时完全不慌。