n8n 增量更新踩坑记录:为什么我的工作流总是重复发邮件?
一次看似简单的需求,却花了我几个小时才搞定。记录下来,希望能帮到同样踩坑的你。
我想做什么?
用 n8n 搭建一个 AI 新闻监控机器人:
- 每天自动抓取 OpenAI、Anthropic、Google AI 的博客更新
- 只有新文章时才发邮件,没有更新就不打扰我
听起来很简单对吧?但”只发新文章”这个需求,让我掉进了一个大坑。
我遇到了什么问题?
问题现象
我用 n8n 的 Static Data(静态数据存储)来记住”上次发送的时间”,逻辑是这样的:
第一次执行:发送所有文章,记住最新文章的时间
第二次执行:只发送比这个时间更新的文章
但实际情况是:每次执行都发送全部文章,就像它完全没有记忆一样。
我手动执行了两次,调试信息显示 lastSentTime: 从未发送过,感觉 Static Data 完全不工作。
排查过程
第一个怀疑:Docker 存储问题?
我的 n8n 跑在 Docker 里,第一反应是数据没有持久化。
于是我检查了:
- Docker volume 挂载 ✅ 正常
- 数据库文件存在 ✅ 正常
- 文件大小在增长 ✅ 正常
但问题依旧。
第二个怀疑:代码逻辑有 bug?
反复检查了 Code 节点的代码,逻辑没问题。
真相大白
最后,我直接查了数据库:
SELECT staticData FROM workflow_entity WHERE name='AI新闻监控';
结果让我惊讶:
{"global": {"lastSentTime": 1765497600000}}
数据明明保存了! 那为什么调试时显示”从未发送过”?
坑点一:Static Data 的”双重人格”
这是 n8n 的一个设计特性(不是 bug):
| 执行方式 | Static Data 行为 |
|---|---|
| 手动点击「Execute Workflow」测试 | ❌ 不读取也不保存 |
| 工作流激活后自动触发 | ✅ 正常读取和保存 |
换句话说:手动测试时,Static Data 是”假”的,只有真正激活运行时才是”真”的。
这个设计可能是为了让测试环境和生产环境隔离,但如果你不知道,就会像我一样困惑很久。
坑点二:IF 节点的类型陷阱
解决了 Static Data 的问题后,我激活了工作流。
结果:手动测试成功,自动执行全部失败!
错误信息:
Conversion error: the boolean 'true' can't be converted to a dateTime
问题原因
我的 Code 节点在没有新文章时会返回:
return [{ json: { _noNewItems: true } }];
然后 IF 节点判断这个字段是否存在。
但我在配置 IF 节点时,不小心选错了数据类型:
- ❌ 选了「Date & Time」类型(日历图标)
- ✅ 应该选「String」或「Boolean」类型
n8n 会尝试把 true 这个布尔值转换成日期,当然会失败。
为什么手动测试没问题?
因为手动测试时,Static Data 不工作,所以 Code 节点返回的是 812 篇文章(全部都是”新”的),每篇文章都没有 _noNewItems 字段,IF 节点就不会触发类型转换错误。
只有自动执行时,Static Data 生效,Code 节点返回 _noNewItems: true,才会暴露这个问题。
最终解决方案
Code 节点(排序 + 过滤)
const items = $input.all();
const staticData = $getWorkflowStaticData('global');
const lastSentTime = staticData.lastSentTime || 0;
// 按日期排序(最新的在前)
items.sort((a, b) => {
const dateA = new Date(a.json.pubDate || a.json.isoDate || 0);
const dateB = new Date(b.json.pubDate || b.json.isoDate || 0);
return dateB.getTime() - dateA.getTime();
});
// 过滤出新文章
const newItems = items.filter(item => {
const pubDate = new Date(item.json.pubDate || item.json.isoDate || 0);
return pubDate.getTime() > lastSentTime;
});
// 更新记忆
if (newItems.length > 0) {
const newestDate = new Date(newItems[0].json.pubDate || newItems[0].json.isoDate);
if (!isNaN(newestDate.getTime())) {
staticData.lastSentTime = newestDate.getTime();
}
}
// 没有新文章时返回特殊标记
if (newItems.length === 0) {
return [{ json: { _noNewItems: true } }];
}
return newItems;
IF 节点配置
类型:String(T 图标)
字段:{{ $json._noNewItems }}
操作:does not exist
逻辑:
- 有新文章 →
_noNewItems字段不存在 → 条件为 true → 继续发邮件 - 没有新文章 →
_noNewItems字段存在 → 条件为 false → 不发邮件
经验总结
1. Static Data 只在激活状态下才真正工作
手动测试看到的行为可能和自动执行完全不同。如果你的逻辑依赖 Static Data,一定要激活工作流后再测试。
2. IF 节点的类型选择很重要
那个小小的类型图标(T、#、📅 等)选错了,就会导致类型转换错误。而且这种错误可能只在特定条件下才会触发。
3. 查看 Executions 历史是排查问题的关键
n8n 的 Executions 面板会显示每次执行的状态和错误信息,比盲目猜测有效得多。
4. 手动测试成功 ≠ 自动执行成功
这两个环境的行为可能不同,特别是涉及到持久化存储时。
写在最后
一个”只发新文章”的小需求,让我深入了解了 n8n 的 Static Data 机制和 IF 节点的工作原理。
虽然过程曲折,但这些经验对以后搭建更复杂的自动化工作流很有价值。
希望这篇记录能帮你少走一些弯路!
如果你也在用 n8n,欢迎交流~