n8n 实战:把 AcmeFlow 的 READY 事件接到 CRM 通知

第 17 篇讨论了为什么主链与旁路可以选不同工具。现在做一个具体而有限的练习:AcmeFlow 客户申请在审批、当前资料版本签署、到账全部齐备后进入 READY,通过 Outbox 发出 APPLICATION_READY 事件;n8n 收到事件,向 CRM 写一条客户经理通知。这里的"通知"是旁路副作用,它失败会产生待重试投递和告警,但不会把申请退回 APPROVED,也不会让 n8n 自行把主状态改为 ACTIVE。ERP 确认创建仍由 AcmeFlow 主状态机负责。
本篇仍使用教学租户 xinghe-demo、业务键 CUSTOMER-001,申请 UUID 00000000-0000-4000-8000-000000000101,与第 03 篇主键语义一致。附带一个可供 n8n 导入的 workflow.json、一个 Python 标准库编写的本地模拟 CRM 服务和静态/HTTP 检查脚本。我们实际执行了 JSON 结构检查、模拟 CRM 的首次与重复及错误租户请求,并用 n8n 2.6.4 官方镜像的 CLI 成功导入一份工作流。CLI 导入不等于 Webhook/节点端到端执行;后者尚未实测。读者运行节点、配置认证与上线的验收步骤写在 README 中。

图 1:n8n 消费 READY 事件并通知 CRM;主状态继续由 AcmeFlow 决定。教学示意。SVG。
为什么只选 CRM 通知作为第一个场景
一个独立练习的好处是边界很清楚:事件输入有一个来源,CRM 请求有一个目标,成功与失败都能观察。若一上来把签署回执、到账、审批、ERP 创建全部拖进 n8n 画布,流程当然可能在演示中跑通,但状态所有权会从 AcmeFlow 悄悄转移到另一个系统。旧版资料签署无效、两个审核人并发、ERP 结果未知等原本由核心服务守护的规则,又要在画布中实现一遍。我们要学习的是用 n8n 做可靠的系统集成,不是用一张图绕过前十七篇建立的边界。
AcmeFlow 的业务事务可以在同一个数据库里提交主状态和 Outbox 行;中继稍后投递事件,失败后继续补投。n8n 作为下游只能获得通知所需字段,不能持有第 03 篇数据库的直接写权限。CRM 也不应该相信一个没有鉴权的公网 Webhook 就代表有效业务事实。本文 JSON 为方便本地教学设置了无认证 Webhook,保持 active=false,不能直接发布到公共网络。生产时必须配置 Webhook 鉴权或前置网关验证、限制来源、最小化载荷并保护 CRM 凭据。n8n 官方 Webhook 节点文档说明它区分测试 URL 与生产 URL,并支持多种认证方式;具体部署仍要由团队做威胁建模。
事件合同先于画节点
输入只需五个核心字段:event_id 指向这一条已发生的 READY 事件,type 固定为 APPLICATION_READY,tenant_id 界定租户,application_id 是申请 UUID,business_key 是可读业务键。event_id 不等于申请 ID:一个申请未来可能产生多个业务事件;也不等于请求幂等键:同一个事件被重新投递时应携带原来的事件 ID。第 06 篇的 Outbox/Inbox 已区分三类键,这里沿用其语义。CRM 模拟接口以 (tenant_id, event_id) 去重,同事件两次 POST 只形成一条通知记录。

图 2:租户、申请、业务键和事件 ID 各司其职,不能用可读业务键代替 UUID。SVG。
事件还应考虑版本与契约升级。真实集成建议增加 schema_version、发生时间、当前资料版本和可验证的来源信息;但"字段越多越好"并不成立。合同 PDF、支付明细和个人敏感数据不应直接塞进通知事件。若 CRM 需要客户名称,可让有权限的适配服务按申请 ID 查询一份最小视图,或在事件中明确加入经过评审的非敏感摘要。字段删改需要与消费者协商,事件模式可版本化;不能为了修一条通知,在没有评估的情况下更改所有历史事件含义。本文为保持实验最小,只包含模拟 CRM 必需字段,并明确不是生产级事件契约。
工作流四个节点做什么
AcmeFlow Event 是 POST Webhook,路径为 acmeflow-ready-crm,响应模式设置为由 Respond to Webhook 节点决定。它收到 JSON 后,CRM Notify 使用 HTTP Request 把必要字段送到 http://host.docker.internal:8765/crm/notify。这个地址只适用于读者按 README 在 Windows Docker 与本机模拟服务配合的情况,部署在别的环境时必须替换为该环境可访问的内部 CRM 地址。成功输出连到 CRM Acknowledged,配置为返回 200;HTTP Request 错误输出连到 CRM Failed,配置为返回 502。这样的结果分支意在让上游区分"CRM 已接受"和"这次投递失败"。n8n 官方的 Respond to Webhook说明响应节点如何控制请求回复;CLI 导入已经成功,实际错误分支的执行仍需在读者安装的环境中测试。

图 3:节点设计示意。200 表示 CRM 本次调用成功返回;502 表示该次调用失败,不自动说明 CRM 一定未执行。SVG。
特别注意,502 也可能对应"不知道 CRM 是否执行"。HTTP 请求可能已到达 CRM,只是响应在返回途中丢失。上游不能收到 502 就换一个新事件 ID 再发;必须沿用同一 event_id,让 CRM 依据原键去重或查询。本地模拟 CRM 故意做了事件 ID 去重:第二次送同一事件,它返回 duplicate,通知总数保持一。真正 CRM 如果没有这种幂等接口,应增加企业侧通知适配器,在它的数据库里用业务唯一约束记录请求和效果;单靠 n8n 的执行历史不能提供跨系统"恰好一次"保证。
实际运行的本地检查
执行 python code/check.py,脚本先读 workflow.json,检查四个节点、成功/错误分支以及工作流默认未激活。随后在 127.0.0.1 的随机端口启动同一个模拟 CRM Handler,以真实 HTTP POST 发送三次请求:有效事件首次得到 200 created,相同事件再次得到 200 duplicate,错误租户得到 422。脚本断言服务内只有一个去重键,最后关闭本地服务。输出保存在 run-output.txt。另以 n8nio/n8n:2.6.4 执行 import:workflow --input=/tmp/workflow.json,退出码为零,实际输出 Successfully imported 1 workflow。这证明该版本 CLI 接受文件,但仍未验证表达式求值、Webhook 真实响应或 CRM 节点端到端执行。

图 4:模拟 CRM 的同事件去重语义;T2 响应丢失是教学假设,非本地 HTTP 检查注入过的网络故障。SVG。
要进行真实 n8n 验收,应先在隔离环境导入 JSON,核对四个节点无配置错误,保持工作流未发布;启动 python code/mock_crm.py;把 HTTP Request 地址改为 n8n 容器可访问的地址;使用 Webhook 测试 URL 送入样本事件,观察 CRM 模拟服务输出和 n8n 执行记录;再重复发送、停掉 CRM、发送错误租户、恢复 CRM。只有这些实际执行通过,才能称为 n8n 端到端测试。n8n 的 Executions 文档描述了执行记录与失败重试入口,读者应在自己部署的版本中检查保存策略与权限,而不是假定所有失败都会无限保留。
发布前的安全与运营检查
首先关闭公共裸露入口。Webhook 至少要有来源认证,较高要求场景还应在边缘网关校验签名、时间窗口与重放 ID,并限制网络来源。n8n 官方的 Security audit把未受保护 Webhook 列为可检查的风险。其次,把 CRM 地址与密钥移入环境或凭据管理,不把密钥写进可导出的工作流 JSON。第三,限制出站目标与访问能力,避免一个被篡改的事件把 HTTP Request 引到内部敏感服务。第四,明确执行记录中是否存储事件载荷、保留多久、谁能查看,遵循租户与个人信息最小化。
还要定义重试责任。AcmeFlow 的 Outbox 中继可以在没有收到明确成功时重新投递,但必须有限次数、退避、告警和人工处理出口;n8n 节点内部若也开启重试,两层叠加可能放大请求。建议实验阶段先只由上游负责投递重试,n8n 的 HTTP Request 保持单次调用,待 CRM 的限流与幂等合同明确后再协商是否把局部重试下沉。成功回执要关联 event_id 保存,不能只记录"某个时间点 n8n 返回 200"。CRM 返回 duplicate 也应按已处理事件的成功回执对待,而不是当成数据异常。若 CRM 返回 422,先诊断载荷或模式冲突,不进行无限自动重试。

图 5:本地结构、CRM HTTP 与 n8n 2.6.4 CLI 导入已运行;Webhook 节点执行仍需实测。本图不是运行截图。SVG。
与第 03 篇基座怎样对接
第 03 篇的 applications 和 workflow_instances 构成主数据:application_id、instance_id 是 UUID,business_key 在同一租户内唯一。第 06 篇增加事实、Outbox/Inbox 时,应用从 APPROVED 进入 READY 的事务应同时写入 APPLICATION_READY 事件。第 14 篇讨论 Webhook/事件入站的鉴权与顺序后,这里是出站 :Outbox 中继调用 n8n Webhook,而非让 n8n 扫描 PostgreSQL 并自己判断"是否 READY"。若后续让 n8n 在旁路调用 CRM,它的成功只能更新通知投递记录,不能写 workflow_instances.state。第 03 篇的原始状态 CHECK 与早期人工审核切片仍需迁移后才能包含 READY;本篇 JSON 没有修改该数据库。
出站接口合同要写清什么时候认为消息"已投递"。如果中继在 n8n 返回 200 后标记 Outbox 完成,那么 200 应意味着 CRM 已明确接受此次调用。本文工作流设计的成功分支正是如此。但 HTTP 返回成功并不能证明客户经理实际读到通知,最终交付是否需要回读 CRM 通知记录,要由业务验收定义。若 n8n 接收到事件后先立即返回 200、后台再异步写 CRM,Outbox 完成含义就变成"n8n 已接收",需要 n8n 自己拥有可恢复的持久队列和失败运维责任。两个语义都可以设计,但不能混在同一个"成功"字段里。

图 6:导入、替换、验收和发布是分开的门槛;生产阶段须补认证、凭据和告警。SVG。
FDE Thinking:节点画完以后还剩什么
在客户演示时,把一个 Webhook 连到一个 CRM 节点,只要五分钟就能显示绿色对勾。真正交付需要回答:事件重复时 CRM 会不会生成两条客户任务?n8n 被停掉一天以后如何补投?出错记录保留多久?多个租户共用实例时,谁能看谁的执行数据?业务人员修改节点后,谁检查它没有获得改写核心申请的权限?这些问题比"节点个数"更能判断集成能否进入生产。
本篇只承诺一个窄而清楚的集成:AcmeFlow 对 READY 事件负责,n8n 对一次 CRM HTTP 调用及其执行记录负责,CRM 对通知唯一性负责,最终 ACTIVE 仍以 ERP 创建证据为准。边界清楚,下一次客户需求变更才有位置可放。例如增加短信提醒,可以新增一个独立消费者或在通知服务中扩展通道;增加"短信确认后才能开通",则是主链规则变更,不能只改通知工作流。FDE 的工程价值,就是不断区分这两种看似相近、责任却完全不同的请求。
一次完整投递应该如何记账
把一次投递看成四份记录,可以避免"n8n 显示成功,客户却说没有通知"的争议。AcmeFlow Outbox 记录事件生成时间和 event_id;中继记录每次投递开始、目标、HTTP 结果、尝试次数和下一次计划时间;n8n 执行记录保存 Webhook 收到事件到 HTTP Request 结束的节点轨迹;CRM 记录以事件 ID 去重后的通知 ID、接收人和创建时间。四份记录的 ID 要能互相查到。没有这层关联,值班者只能在各系统按时间猜同一请求,尤其在高峰期会遇到多条相似客户申请。
成功也应分层。Webhook 接收请求是网络层成功,n8n 执行到 HTTP Request 是流程层完成,CRM 返回通知 ID 才是目标系统接受,客户经理阅读通知又是另一层业务效果。本文练习把"CRM HTTP 返回成功"作为 AcmeFlow 可标记投递完成的条件,尚不承诺已阅读。如果合同要求"客户经理十五分钟内处理",还需要从 CRM 回收阅读或处理状态,并另设一个任务或 SLA。不能直接把 Outbox 发送成功时间当成客户经理已处理时间,也不能把 CRM 通知失败解释成服务未开通。
当上游收到 502 时,首先按错误类别处理。连接失败发生在发出请求之前,通常可在预算内重试;连接超时或响应丢失则不确定 CRM 是否写入,要以原 event_id 查询或重发到幂等端点;CRM 返回 422,通常是载荷合同不符,应该停止自动重试、留存脱敏错误详情、修正模式;CRM 返回 429,要尊重限流提示,避免 AcmeFlow 中继和 n8n 节点同时指数重试。本文工作流没有实现自动分类器,也没有开启节点内部重试,故这些处置规则需要接入真实环境时落到 Outbox 中继的策略与告警里。不要因为图中有错误分支就误以为所有故障已经自动恢复。
如何验证导入后的每一个节点
导入之前先打开 JSON 检查 URL 和表达式。AcmeFlow Event 的路径固定,若同一 n8n 实例里已有相同路径,可能冲突;应换成环境专用路径并同步上游配置。CRM Notify 里的 host.docker.internal 是本地教学地址;在 n8n Cloud 中它不会指向你的笔记本,在 Linux 容器中也要按网络配置确认可达。导入后先查看节点是否有版本警告、红色必填项或失效凭据。本文导出的节点 typeVersion 来自 n8n 当前常见 JSON 结构,目标版本若提示迁移,应以目标版本的 UI 与官方文档为准。不要在未审查的情况下直接把 active 打开。
第一次真实测试从 Webhook 的测试 URL 开始。n8n 的测试地址通常在编辑器等待测试事件时使用,而生产地址依赖工作流发布/激活;两者不要混写进 AcmeFlow 的正式 Outbox 配置。先启动模拟 CRM,发送一条完整 JSON,确认 Webhook 收到对象时有 body 包装,再检查 HTTP Request 实际传给 CRM 的 JSON:event_id、type、tenant_id、application_id、business_key 不应变成字符串化的整个对象,也不应丢字段。查看成功分支是否返回 200。再发送完全相同的事件,CRM 应返回 duplicate,通知数不变。若 n8n 执行记录只显示绿色而 CRM 计数增加两次,表示去重合同没有成立。
第二组测试切断 CRM。此时确认 HTTP Request 走错误输出、Respond to Webhook 返回预期失败码,AcmeFlow 中继保持事件待投。然后恢复 CRM,用原 event_id 再送。第三组测试发送错误租户、空事件 ID 和不支持的事件类型。当前样例把详细校验放在模拟 CRM,实际生产更推荐在 n8n 前置网关或 AcmeFlow 中继出口按合同拒绝明显错误的事件,避免无意义调用。第四组测试针对并发:用相同事件 ID 快速发送多次,确认 CRM 的唯一键或事务确实抗并发;本地 mock_crm.py 用进程内锁演示单进程效果,不具备多进程、重启或真实数据库的持久保证。生产 CRM 必须用持久化唯一约束或等价幂等接口。
每组测试都保存输入事件、n8n 执行 ID、CRM 结果和上游 Outbox 状态。不要把真实密钥或完整客户数据粘进教程截图。若演示使用虚构租户与 UUID,要保持它们在整个系列里一致,否则读者难以分辨是事件关联错误还是教程改了案例。验收完成后再把 CRM 地址切到正式环境、配置凭据和入口鉴权,以受限租户做小流量灰度。正式发布之后继续观察首次投递成功率、重复事件比例、失败积压和最老待投事件年龄;不是因为测试通过就认为今后不会出现超时。
CRM 去重要放在持久边界
本地模拟服务内的 seen 集合只存在于进程内;服务重启后集合消失,第二次投递可能再次创建通知。它的价值是让读者通过真实 HTTP 看见"同事件重复"这种输入,不是生产实现建议。正式 CRM 若支持调用方提供幂等键,应把 event_id 或 tenant_id + event_id 映射到那个键,并约定同键同载荷返回原结果、同键不同载荷拒绝。若 CRM 不支持,企业可以在通知适配服务自己的数据库里建立 (tenant_id,event_id) 唯一约束,并与本地通知意图一同提交;随后向 CRM 发出请求时仍需处理响应丢失,最好能通过外部请求标记查询。适配器不可凭本地唯一键就宣称远端绝不重复。
business_key 不适合拿来做通知去重。客户同一申请可能先进入 READY,后续版本变化又产生补充通知;如果只按业务键去重,新通知会被吞掉。相反,同一个 event_id 的重投即便跨越几个小时,也代表相同业务事实,应被识别为同一次通知意图。事件 ID 的生命周期至少应覆盖上游可能重投的最长窗口;如果 CRM 只保留一天去重记录,而 Outbox 可在一周后人工补投,仍会重复。去重保留期、事件重放政策和通知记录生命周期必须在两端对齐,不能只看单次 API 调用是否返回成功。
n8n 实例运营与权限
通知工作流投产后,要明确谁能编辑、谁能发布、谁能查看执行数据、谁能使用 CRM 凭据。业务人员可能需要修改文案,却不应拥有改写 HTTP 目标地址或关闭鉴权的权限;若产品计划无法满足细粒度授权,就把变更放进受审查的发布流程。官方 工作流共享文档解释了共享工作流与凭据访问之间的关系,具体可用功能会随计划和版本变化。团队至少应保留一份经审查的导出 JSON、版本号、变更原因和回滚版本;不要把生产画布当作唯一源文件。
监控应有独立层级。AcmeFlow 监控 Outbox 最老待发年龄,n8n 监控工作流失败执行,CRM 监控通知创建与去重冲突。若只有 n8n 失败数,AcmeFlow 中继尚未投出事件的故障就看不到;若只有 Outbox 发送成功数,CRM 写入后内部派发失败也看不到。值班手册要标明先查哪个系统、如何通过 event_id 关联、何时允许手动重试、何时联系 CRM 负责人。手动重试要沿用原事件 ID,并在审计里记录操作者与原因。
安全上还要考虑 Webhook 被伪造的可能。一个公网无认证入口若接受外部随意提交 APPLICATION_READY,即使它不能改 AcmeFlow 状态,也可能向 CRM 注入垃圾通知或探测内部网络。生产入口应要求密钥、签名或可信网关认证,并检查租户与事件类型;HTTP Request 出站地址固定,不能由外部载荷决定。n8n 的 Security audit 可帮助发现未保护的 Webhook,但不是安全验收的全部。应该结合访问控制、凭据轮换、出站网络规则、事件保留和异常告警制定操作规程。本文 JSON 保持未激活且无认证,正是为了让读者先在隔离本地环境确认机制,再主动完成这些配置。
给业务方看的验收场景
技术团队常用"节点全绿"作为演示终点,业务方真正关心的是客户经理是否只收到该收到的通知。可以准备四份样本:一份符合条件的 READY 事件、一份完全重复的事件、一份其他租户事件、一份资料版本已不再有效的旧事件。当前模拟 CRM 只验证前三类的部分行为;第四类必须由 AcmeFlow 在发布事件前以及通知适配器在合同约定下处理。验收时,第一份应生成一条带申请 ID 的通知,重复的第二份不增加数量,第三份被拒绝并产生可查的错误记录,旧事件如果仍可能出现,则按版本策略拒绝或标注失效。业务人员还应检查通知标题、接收人、跳转链接和脱敏内容,而非只看 HTTP 状态码。
若 CRM 通知要链接到 AcmeFlow 申请详情,链接必须按已认证用户的权限在目标系统重新鉴权。不能因为事件里有 application_id,就拼一个无认证的访问地址给所有收件人。不同客户经理可能负责不同租户;CRM 收件人与 AcmeFlow 查看权限未必完全相同。最小设计是只送一个可读的业务摘要和内部申请 ID,点击后由 AcmeFlow 自己判断能否查看。若客户要求在通知正文展示合同金额、联系人电话等字段,应单独做字段级授权与保留期评估,而不能沿用教学事件的自由扩展方式。
对"通知发送成功"的业务验收,也要约定 CRM 的接口语义。有些 CRM 在请求返回时仅排入内部队列,后续才生成可见通知;有些立即返回通知 ID。前者的 200 只能证明 CRM 已接收,后续还需 CRM 自己的投递结果事件或查询。FDE 应把接口文档、模拟假设和真实验收结果放在同一张表里,明确本篇 mock_crm.py 的 200 是"进程内集合已记录",不能从这个实验推断真实 CRM 的用户可见性。
中继和 n8n 之间要有清晰的失败协议
假设 Outbox 中继在凌晨投出 E1,n8n HTTP Request 调 CRM 得到 200,随后 n8n 正准备把 200 返回给中继时连接中断。中继只能记录"结果未知",不能把 E1 标记成功;按预算再次送 E1,CRM 应返回"duplicate",n8n 再把成功响应传回,中继才完成投递。这种重复不是系统异常,而是分布式系统合理的恢复路径。若 CRM 不提供去重,至少要有可查询的通知记录,否则中继无法区分"未执行"和"已执行但回复丢失",自动重试就不安全。
另一个窗口是 n8n 已收到 Webhook,但进程在调用 CRM 前停止。若 Webhook 采用"立即响应",中继可能错误地认为完成;本文采用响应节点在 CRM 调用后返回,正是为了让上游尽量获得最终调用结果。不过这仍不是跨系统原子事务:n8n 进程可能在 CRM 成功后、响应上游前停止,重复依然不可避免。设计的目标不是消灭所有重复,而是让重复有稳定标识、可安全处理、可观测和可人工对账。这里与第 06 篇 Outbox/Inbox、第 07 篇未知结果的原则完全一致。
错误码本身也不应包办业务状态。502 可表示节点调用失败,但不能表示 CRM 绝无通知;422 可表示当前模拟接口拒绝了错误租户,但真实 CRM 可能用不同状态码和错误体。接入前应与 CRM 负责人建立映射:哪些状态可重试,哪些需改数据,哪些是权限或配额问题,哪些必须人工对账。把映射固化在中继配置或适配层,并对每一种类别设重试上限、总历时和告警接收人。未经验证不要盲目打开 n8n 的"遇错重试"选项,同时在上游保留另一个重试循环。
变更、回滚与在途事件
工作流发布不是修改一个 JSON 文件这么简单。即便这次只把 CRM URL 从测试地址换成正式地址,也要确认待投 Outbox 事件会被送到哪个版本。若旧版本通知模板包含过期链接,而新版本已修复,重试旧事件时应该用当前模板还是事件发生时的模板?两者都可能合理,但必须在业务合同中定义。对于已存事件,字段缺失也会影响新工作流的表达式求值;因此修改节点前应拿几条历史事件做回放,并在隔离环境确认兼容。发生故障时回滚工作流版本,同时要保留曾运行版本与执行记录,才能解释为什么同一事件在不同时间产生不同输出。
建议采用一份简单发布单:工作流 JSON 校验和、目标 n8n 版本、节点变更摘要、测试事件 ID、模拟 CRM 与真实测试环境的结果、入口鉴权配置、回滚文件、值班联系人。发布后首先用受限租户事件测试,不马上把所有积压事件放行;观察成功率、失败类型和去重比例,再逐步扩大投递速率。若失败率升高,暂停 Outbox 中继比不断修改线上节点更容易控制影响范围。暂停期间要监控最老待投事件年龄,防止修复工作流时忘了业务 SLA。
最后给这个练习一个清晰的"完成"定义:隔离环境可导入工作流;同一 READY 事件首次投递生成一条通知,重复投递不新增;错误租户与不支持事件被拒绝;CRM 停机时上游保留待投状态,恢复后沿用原事件 ID;工作流执行记录能与 AcmeFlow Outbox、CRM 通知记录互相定位;入口和出站凭据通过目标环境的安全评审。当前附件实测了 JSON 结构、本地模拟 CRM HTTP 与 n8n 2.6.4 CLI 导入;WebHook 触发后的节点运行和其余条目仍是读者或项目团队的验收任务。把未完成项写出来,是为了不把一个可演示样例误交成生产承诺。
在教学环境里,导入命令成功只证明 n8n 能解析并保存这份工作流定义。它不替代 UI 检查:HTTP Request 的字段表达式是否正确、错误输出是否按预期连接、Respond 节点能否在目标版本返回设定的状态码,都需发送真实 Webhook 请求后观察。本文刻意将这一层证据与本地 CRM 测试分开记录,防止"文件被接受"被误读成"通知链路可投入生产"。如果目标环境导入时自动升级节点版本,也应重新运行全部测试样本,并把升级后的导出 JSON 留作发布记录。