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,欢迎交流~

发表回复

Your email address will not be published. Required fields are marked *.

*
*