NEW

免费试用已开放

立即开始

自动化 · 回调 · Webhook

企业微信自动化工作流怎么搭

更新于 2026-08-168 分钟

「企微工作流」这个词底下压着两个完全不同的东西。一个是「收到 X 就做 Y」的单步回调处理,写代码比配流程快;另一个是带等待、带分支、能跑好几天的流程实例,不上编排会在第三个需求上崩掉。这篇先给出区分两者的信号,再拆开事件驱动编排真正需要的四个组件 —— 触发器、条件、动作、流程状态,重点在最后一个:它被漏掉得最多,也决定这套东西能不能长起来。下面的例子按 wecomapi 的事件与接口形态来写。

先判断你到底需不需要工作流

大部分挂着「企业微信自动化工作流」名头的需求,本质是一条 if 语句:收到关键词就回复、有人入群就发欢迎语、订单发货就推通知。这类需求写在代码里三十行搞定,套一个编排引擎反而多出一份流程定义要维护、多一个调试入口要学。

需要引擎的信号只有一个:流程里出现了「等待」。等客户回复、等三天后跟进、等人工审批、等外部系统回调 —— 只要有一步的下一步不是立刻发生,你就必须在某个地方记住「这个客户走到哪一步了」。这个「记住」就是流程实例,也是编排引擎和一堆回调处理函数之间真正的分水岭。

还有个更好用的自测:业务方问「这个客户现在走到哪一步」,你能不能不翻日志就答上来。答不上来,说明状态只存在于代码的执行路径里,再加一个分支就会失控。

  • 出现「N 天后如果还没 X 就 Y」这类需求:要定时器加实例状态,自己写最容易漏掉进程重启后的恢复。
  • 同一个客户可能同时命中两条流程:要做实例级的互斥或优先级,散在各个回调函数里做不了。
  • 业务方开始每周提改话术、改分支:要把规则从代码里挪出去,否则每次改动都得走一遍发版。
  • 流程要跨小时甚至跨天:状态必须显式落库,否则一次滚动发布就把内存里正在等待的实例清空了,而这件事总是在第一次发版当天才被想起来。

反过来,上面几条一条都不占就别上引擎。见过太多团队先搭了一整套编排,跑了半年只跑着五条单步流程,每条都比直接写代码复杂三倍,没人愿意接手。

触发器:把事件映射到实例,不是映射到函数

触发器要解决的不是「怎么收到事件」,那是回调层的事。它解决的是「这条事件该拉起哪个流程的哪个实例」。两样东西必须先定死:实例键和步骤去重键。

实例键决定事件的归属,通常是客户标识或会话标识,也可能是订单号,取决于流程围绕谁展开。用 wecomapi 的事件回调收到消息之后,第一件事就是把它解析成实例键,解析不出来的直接丢弃,别让它进编排层。这一步做错的症状很典型:实例数量随消息数增长,几天涨到几十万条,翻开一看全是没有归属的孤儿实例。

去重键是另一回事。两层对「重复」的定义不同:回调层重复的是事件,编排层重复的是状态转移 —— 一次合法的消费重试携带的事件标识早就被记录过,却仍然能让实例从「已发欢迎语」再往前推一格。所以编排层要按「实例 + 步骤」再去重一次。

比去重更容易翻车的是并发:同一个实例的两条事件同时到达。客户连发两句话,两个消费者各自读到「当前在第二步」,各自把它推进到第三步,结果动作做了两遍而状态只前进了一格。要么按实例键分区消费,要么在推进状态时带版本号做乐观锁 —— 前者简单,后者更稳,但必须选一个。什么都不做,等于默认这种并发不会发生。

最后一类触发器最容易被漏:流程自己埋下的定时器到期。它必须和外部事件走同一个入口、同一套实例键解析,不能因为「是自己产生的」就直接调处理函数。一旦走旁路,进程重启后的定时器恢复逻辑就得单独写一遍,而那段代码在正常测试里根本跑不到。

条件层:规则放代码、放表达式,还是放 DSL

条件层有三档实现,成本递增,绝大多数团队应该停在前两档。

  1. 1硬编码在代码里。改一次发一次版。规则少于十条、只有开发自己会改的阶段,这一档最快,别急着离开。
  2. 2配置化表达式。规则存库,用一个受限的表达式求值:字段比较、集合包含、时间窗。业务方能改了,代价是必须配校验和灰度 —— 一个写错的条件能在半夜命中全量客户。
  3. 3完整 DSL 或可视化编排。真正的成本不在实现引擎,而在你得同时提供调试、试运行、版本对比和操作审计;少一样业务方就会回来找你。

判断线是「谁在改」。改动方还是开发,停在第一档;改动方变成运营且频率到了每周,才值得上第二档;只有当改动方是多个互不相关的业务组,第三档才回得了本。按预期规模选档,几乎总是选高了。

还有一条硬规矩:条件层不许读外部系统。在一个条件里塞一次 CRM 查询看着方便,代价是流程的求值时间变得不可控,而且失败语义会含糊掉 —— 到底是条件为假,还是查询超时了?这两件事在流程里必须能分开。要用外部数据,就让它先作为一个动作把结果写进流程上下文,条件只读上下文。

动作层:返回成功不等于流程能往下走

动作是流程里唯一产生外部影响的部分,它必须可重试、带幂等键、结果可查。前两条站内讲过,这里只说第三条 —— 它是编排层特有的问题。

一次调用返回成功,只说明请求被接受了,不代表流程期望的结果已经发生。如果下一步依赖「这件事真的完成了」,就不能拿调用返回当推进条件。这不是接口的问题,是异步系统的普遍性质:请求确认和结果确认之间隔着一段真空,而流程恰好最怕这段真空。

实践上把动作显式分成两类写法。一类是「发出即完成」,调用返回就推进下一步,适合结果不影响后续分支的动作。另一类是「发出后等确认」,调用之后实例进入等待态,由 wecomapi 推过来的对应事件、或一次主动读回把它唤醒,同时挂一个超时兜底。哪个动作属于哪一类要写在流程定义里,不能靠写代码的人记得。

动作还要再分一次「可重试」和「不可重试」。可重试的(超时、下游 5xx、被限流)交给退避重试,实例留在原步骤等着;不可重试的(参数不合法、权限不足、目标已不存在)应该立刻推进到失败终态并留下原因。两类混在一起的表现很好认:一批实例卡在同一步重试到天亮,把配额烧光,而它们第一次就该被判死。

超时兜底不能省。等待态没有超时,就是一个永远不会结束的实例;实例不结束,就没有任何指标会告诉你这条流程早就断了 —— 它只是安静地卡在那里,看起来还很正常。

流程状态与版本:这套东西真正难的地方

流程状态是四个组件里最容易被漏掉的一个,因为前三个都能在一次请求里跑完,只有它需要落库、需要恢复、需要迁移。没有它,你搭的不是工作流,是一组共享配置的回调函数。

落库要存五样:当前步骤、上下文数据、进入当前步骤的时间、下一个定时器的到期时间、流程定义的版本号。前四样大家都会存,最后一样几乎所有第一版都会漏。

漏掉版本号的后果在你第一次改流程时出现。把「发欢迎语 → 等 3 天 → 跟进」改成「发欢迎语 → 等 1 天 → 打标签 → 跟进」,此时库里正躺着两千个处在「等 3 天」的实例。它们醒来时按新定义找下一步,可能找到一个不存在的步骤,也可能跳过打标签直接跟进。两种都不抛异常,都是数据悄悄错掉。

  • 实例绑定版本:老实例跑老定义,新实例跑新定义。最干净,代价是要长期保留旧版本定义。
  • 只允许向后兼容的改动:加步骤可以,删步骤和改语义不行。约束强,实现最省。
  • 显式迁移:每次改流程都给出老实例的迁移映射,跑一次批处理。灵活,但要求每次改动都写迁移脚本,实际很难长期坚持。

选第一种。它是唯一一种在你周五晚上改流程时不会出事的做法,多出来的那点存储成本可以忽略不计。

示意:一份流程定义里真正承重的四个字段yaml
# 示意定义,只表达组件关系,不对应任何真实配置格式
version: 3                     # 实例创建时记下这个号,老实例继续跑老版本

trigger:
  on: friend.added             # 事件类型以线上文档为准
  instanceKey: customerId      # 决定这条事件属于谁的流程

steps:
  - id: welcome
    action: sendText           # 走 https://manager.wecomapi.com/message/sendText
    await: none                # 发出即完成,返回即推进

  - id: wait
    delay: 3d
    timeout: 7d                # 等待态必须有超时,否则实例永远不结束

  - id: followUp
    when: "ctx.replied == false"  # 条件只读上下文,不查外部系统
    action: sendText

这份定义里真正承重的是 version、instanceKey、await 和 timeout 四个字段,它们分别对应四个组件最容易出事的那一面。步骤怎么排反而最不重要 —— 那部分业务方自己会告诉你,而且下个月还会再改一次。

第一版做到什么程度,以及作废条件

  1. 1只支持一条流程。把实例键、状态落库、定时器恢复、版本号这四件事做对,比支持十条流程有价值得多。
  2. 2定时器用数据库轮询就够。一张带到期时间的表加一个每分钟扫一次的 worker,能撑到很大的量,重启后自动恢复,比内存定时器可靠一个量级。
  3. 3给每个实例留一条可读的执行轨迹:什么时候进的哪一步、条件求值结果是什么、动作返回了什么。业务方问「为什么这个客户没收到」时,你需要的是这条轨迹,不是去日志里检索。

还有一样第一版就该有、却几乎人人都漏的东西:作废条件。客户已经成交、已经退群、已经转给别的同事了,流程还在按原计划发催单。每条流程都要显式声明「什么情况下这个实例直接作废」,并在每次推进前检查一遍。作废信号有两个来源:退群、客户归属转接这类企业微信侧的状态变化,用 wecomapi 的事件回调订阅到之后写进实例上下文;成交平台侧看不见,只能由你自己的订单或 CRM 系统在状态变更时回写一次上下文。两个来源都落进同一份上下文,作废检查只读上下文就够。这比事后靠人去后台一个个停掉便宜得多,也是业务方投诉最集中的一类问题。

这几条做完,编排层大概两千行,能覆盖后面两年里绝大多数需求。真正会逼你换成熟引擎的是并行分支和子流程 —— 在那之前,自研都划算。

本文讲的是编排层的组件划分与取舍。事件类型、字段与端点以 wecomapi 线上接口文档为准,示意的流程定义只用来表达组件关系,不对应任何真实的配置格式。

常见问题

企微工作流一定要用现成的编排引擎吗?
不一定,判断标准是流程里有没有「等待」。全是单步的「收到就做」,自己写一张状态表加定时扫描比引入引擎快得多;一旦出现跨天等待、分支和人工节点,再自研就是在重写一个引擎,那时用现成的更划算。
回调层已经做了幂等,工作流层还要再做吗?
要,两层挡的不是同一种重复。回调层挡的是同一个事件被重复投递,编排层挡的是同一个状态转移被执行两次 —— 一次合法的消费重试就能绕过回调层的去重,把实例多推一步。用 wecomapi 的事件回调进来时按事件标识去重,进编排层之后再按「实例 + 步骤」去重一次。
流程改了,正在跑的实例怎么办?
给流程定义编版本号,实例创建时记下自己跑的是哪一版,老实例一直跑到结束都用老定义。这是唯一一种改流程时不用担心存量实例的做法,代价只是长期保留旧版本定义,比每次写迁移脚本便宜得多。

准备好动手了?

精确字段、鉴权与端点以线上文档为准;可在控制台创建密钥后联调。

相关文章