n8n Workflow Engineer
何时触发
- 编辑或构建 n8n 工作流 JSON
- 调试 n8n 问题:"节点未执行"、"配对项不可用"、"静态数据未持久化"
- 通过 n8n REST API 进行批量操作(重命名、重试、删除)
- Telegram、Gmail、HTTP Request、Code、Switch、Merge 节点模式
- n8n 中的定时触发器/时区/cron 行为
- n8n v2.x UI 行为(发布/取消发布、归档)
硬性规则
- 审计后再编辑。 读取完整的工作流 JSON → 展示发现 → 确认 → 一次性应用。绝不盲目修补。
- 通过定时触发器测试。 手动 "Execute" 会重置
$workflow.staticData。去重验证需要一次真实的 cron 运行。 - 验证真实结果。 检查执行日志和最终状态(消息已到达、数据行已存在)。节点返回
ok: true是一个好迹象,但不是证据。UI 同步延迟会误导观察者。 - 匹配 Code 节点模式与返回签名。 错误的模式 + 错误的返回 = 静默的部分处理。
快速参考——最常见的 Bug
| 症状 | 根因 | 修复 |
|---|---|---|
对 localhost:4000 的 HTTP Request 返回连接拒绝/超时 | n8n 在 Docker 桥接模式下运行 | 使用主机网关:通常默认桥接为 172.17.0.1(用 docker network inspect bridge 检查),或在 extra_hosts 中映射 host.docker.internal:host-gateway |
| Telegram 消息从不到达,无错误 | IPv6 DNS 解析超时 | 在 docker-compose 中设置 NODE_OPTIONS=--dns-result-order=ipv4first |
new Date() 返回 UTC 尽管 cron 使用 EST | 容器缺少 TZ 环境变量 | 将 TZ 和 GENERIC_TIMEZONE 都设置为你的时区 |
parse_mode: Markdown 静默失败或乱码 | AI 输出中不匹配的 * 或 _ | 使用 parse_mode: HTML + 发送前清理 |
| 对 LiteLLM HTTP Request 节点返回 401 | 缺少认证头 | 添加 Authorization: Bearer {{ $env.LITELLM_API_KEY }} |
调用 Claude 时 ECONNABORTED: timeout of 15000ms | 默认 15s 超时 | 设置 options.timeout: 90000 |
PUT 请求 /api/v1/workflows/:id 返回 400 | 请求体中包含额外字段 | 仅保留:name、nodes、connections、settings、staticData |
| 重命名节点静默破坏工作流 | 连接引用仍指向旧名称 | 在 nodes[].name 和 connections{} 的键中同时替换所有旧名称 |
| 聚合 Code 节点后配对项不可用 | N→1→M 丢失了项的血统 | 设置 executeOnce: true,显式遍历 $('UpstreamNode').all() |
| 手动运行时静态数据去重 "无效" | 手动执行会重置静态数据 | 仅通过定时 cron 测试 |
Code 节点——模式/返回规则
每个 Code 节点都有一个 mode 参数。不匹配 = 静默 Bug。
| 模式 | 访问方式 | 返回值 |
|---|---|---|
runOnceForAllItems(默认) | const items = $input.all(); | return items.map(...) —— 必须是数组 |
runOnceForEachItem | $json 是当前项 | return { json: ... } —— 单个对象,不是数组 |
不匹配的症状:
runOnceForAllItems+ 访问$json→ 仅第一项被静默处理runOnceForEachItem+ 返回数组 →"A 'json' property isn't an object"验证错误
去重模式(跨运行状态)
const store = $getWorkflowStaticData('global');
store.jobIds = store.jobIds || [];
const newItems = items.filter(item => !store.jobIds.includes(item.json.id));
// 持久化新 ID,限制最后 500 个以避免无限制增长
store.jobIds = [...store.jobIds, ...newItems.map(i => i.json.id)].slice(-500);
return newItems;
关键: staticData 仅跨越 定时 运行持久化。手动执行会重置它。通过观察两次连续的定时运行来验证去重,而不是通过点击两次 Execute。
每次运行缓存清理
// 在第一个 Code 节点顶部——清理临时缓存但保留跨运行状态
const store = $getWorkflowStaticData('global');
for (const k of Object.keys(store)) {
if (k.startsWith('cjob_') || k.startsWith('cprep_') || k === 'pendingJobs') {
delete store[k];
}
}
// 永远不要删除 jobIds —— 跨运行去重依赖它
管道可观测性模式
在每个主要 Code 节点设置计数器。在格式化步骤读取。追加到消息中。
// 阶段 1——构建 Tickers
const store = $getWorkflowStaticData('global');
store.pipeline = { ticker_count: tickers.length, fixed_count: fixed.length };
// 阶段 2——计算 Signals
store.pipeline = store.pipeline || {};
store.pipeline.signal_count = results.length;
// 最终——格式化消息
const pl = store.pipeline || {};
const pf = (n) => (n === undefined || n === null) ? '?' : String(n);
const pipelineBlock =
'\n\n📊 Pipeline\n' +
'Build Tickers: ' + pf(pl.ticker_count) + '\n' +
'Signals: ' + pf(pl.signal_count);
配对项问题 + 解决方案
症状: 在聚合节点(N 项 → 1 个提示 → 1 个 LLM 响应 → 拆分为 M 项)之后,下游的 $('UpstreamNode').item.json 抛出 Paired item data for item from node 'X' is unavailable。
解决方案(按偏好顺序):
-
在下游 Code 节点设置
executeOnce: true,显式遍历:const upstream = $('UpstreamNode').all(); const outputs = []; for (let i = 0; i < parsed.length; i++) { outputs.push({ json: { ...parsed[i], original: upstream[i].json } }); } return outputs; -
通过静态数据传递 ID;在下游显式查找。
-
pairedItem.sourceOverwrite—— 脆弱,最后的手段。
Telegram 模式
HTML 模式——仅允许 7 个标签
<b>、<i>、<u>、<s>、<a>、<code>、<pre> —— 其他任何标签都会引发 400 Bad Request: Unsupported start tag。
针对 AI 生成内容的三层防御
- 提示词: 明确告诉 AI 允许哪些标签
- 清理: 在发送前的代码中剥离不支持的标签
- 转义: 在嵌入前对用户/AI 内容进行 HTML 转义(
&→&、<→<、>→>)
隐藏 n8n 归属声明
"additionalFields": {
"parse_mode": "HTML",
"appendAttribution": false,
"disable_web_page_preview": true
}
使用 .env 中的令牌 vs 凭据
优先使用环境变量 + HTTP Request 模式,而不是内置的 Telegram 节点 + 凭据:
- 跨迁移可移植
- 更易于轮换(
nano .env+ 重启容器) - 无数据库加密密钥锁定风险
- 模式:
POST https://api.telegram.org/bot{{ $env.BOT_TOKEN }}/sendMessage - 权衡:令牌出现在请求 URL 中,因此可能出现在执行数据、代理日志和错误消息中。为此工作流关闭执行数据保存,或者如果其他人可以读取你的日志,则使用内置的 Telegram 凭据。
Gmail OAuth 模式
| 问题 | 解决方案 |
|---|---|
invalid_grant ... refresh token revoked | "Testing" 模式下的 OAuth 大约 7 天过期。通过 UI 重新授权,或将应用发布为 Production。 |
空的 payload.headers(发件人/主题为空) | 默认 simple: true 会剥离标头。设置 options.simple: false。 |
| 缺少安全警报 / 收据 | 不要排除 -category:promotions -category:social -category:updates —— Gmail 将 90% 的相关邮件发送到那里。仅使用 is:unread newer_than:Nd。 |
| 需要跨多个账户标记为已读 | 需要每个账户的凭据。一个 Gmail 凭据无法操作另一个账户的消息 ID。 |
REST API 模式(批量操作)
认证:X-N8N-API-KEY: <key> 标头。
| 端点 | 备注 |
|---|---|
GET /api/v1/workflows | 返回所有(分页)。客户端过滤。 |
GET /api/v1/workflows/:id | 完整的工作流 JSON。 |
PUT /api/v1/workflows/:id | 严格:仅 name、nodes、connections、settings、staticData。settings 本身有一个允许列表(见下文)。在 GET 然后 PUT 的往返中剥离其他所有内容。 |
DELETE /api/v1/workflows/:id | 实际删除(UI 仅提供 Archive)。用于批量清理。 |
POST /api/v1/workflows/:id/activate | 相当于 v2.x UI 中的 Publish。 |
settings 字段允许列表(PUT 请求体)
executionOrder、timezone、saveExecutionProgress、saveDataErrorExecution、saveDataSuccessExecution、saveManualExecutions、callerPolicy、callerIds、errorWorkflow、executionTimeout
节点 type 在创建后不可变
更改 node.type 的 PUT 请求返回 200,但类型保持不变。要进行转换(例如 telegram → httpRequest):替换整个节点对象(新的 UUID 用于 id,同名 name)。连接按名称路由,因此保留名称的替换是安全的。
n8n v2.x UI 特性
| 旧版 (v1.x) | 新版 (v2.x) |
|---|---|
| 激活开关 | 发布(绿色按钮,右上角)/ 取消发布 |
| 删除工作流 | 仅归档(实际删除使用 REST API) |
HTTP Request 节点——LLM 调用的必备配置
{
"method": "POST",
"url": "http://172.17.0.1:4000/v1/chat/completions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{"name": "Content-Type", "value": "application/json"},
{"name": "Authorization", "value": "=Bearer {{ $env.LITELLM_API_KEY }}"}
]
},
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify({model:'<your-model-alias>',messages:[{role:'user',content:$json.prompt}],temperature:0.3}) }}",
"options": {
"timeout": 90000,
"retry": { "tries": 2, "waitBetweenTries": 5000 }
}
}
n8n 容器没有 curl
对于容器内 HTTP 测试,通过 docker exec 使用 Node 内置的 https 模块:
docker exec n8n node -e "const https=require('https'); /* request */"
SSH 预检(在 VPS 上执行任何 heredoc 之前)
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 <user>@<host>
# 验证提示符:[<user>@<host> ...]$
# 只有 THEN 粘贴 heredoc
将 bash heredoc 粘贴到 SSH 连接后的 PowerShell 中 = 数百个解析错误。
评测套件模式(纯逻辑)
通过 n8n 评测功能进行模拟输入测试可以捕获逻辑回归,而无需消耗 API 额度。在对过滤逻辑、关键词列表或验证规则进行任何更改之前 AND 之后重新运行。
反模式
| 反模式 | 应该怎么做 |
|---|---|
| "再运行一次,看看是否有效" | 首先从头到尾读取执行日志 |
| 多次往返一次修补一个节点 | 审计完整文件 → 批量补丁脚本 → 一次性验证 |
对 AI 输出使用 parse_mode: Markdown | parse_mode: HTML + 清理器 |
| 在工作流 JSON 中硬编码 API 密钥 / 聊天 ID | 引用 {{ $env.KEY }},存储在 <secret-file> 中(chmod 600) |
| 重命名节点而不更新连接 | 在 nodes[].name 和 connections{} 的键中同时替换节点名称 |
| 手动执行以测试去重 | 等待定时触发器 |
| 对 LLM 调用使用默认的 httpRequest 超时 | 设置至少 90000ms |
混合使用 runOnceForAllItems 和 $json | 匹配模式与访问方式 |