clickSomo

漏斗与触发器

漏斗是一张图。一次运行是一行。一次等待是一段时间。

这就是全部模型,这个产品拥有的每一个难点性质都源自这三句话。正因为如此,编辑运行中的漏斗是安全的;正因为如此,重试不会重复发送;正因为如此,周二退订的人在周四不会收到任何东西。

运行时实际发生的事

发布漏斗会写入一个不可变版本。有人进入后,就会出现一行记录:一次运行,属于这个联系人,固定在那个版本上,保存着当前节点、唤醒时间和它自己的上下文。

工作进程会租借唤醒时间已到的运行,用 SKIP LOCKED 认领它们,这样两个工作进程不会拿到同一条。它执行节点,直到遇到等待或终止节点,然后写入下一个唤醒时间并释放。

节点是关于运行、联系人和节点本身的纯函数——而且它的副作用与运行自身的推进在同一个事务中提交。正是这一个特性让重试变得安全。工作进程如果在“发送”和“推进”之间的半秒内被杀掉,留下的要么是两者都有,要么是两者都没有,绝不会只有其一。

而且由于时间由应用提供而非数据库,现在写入的行现在就到期。一个默认取数据库时钟的调度列在写入的那一刻还不算到期,会错过创建它的那一次滴答——这个 bug 看起来毫无异常,直到某条消息晚了一个小时。

工作进程的一次滴答执行
  • 1租借wake_at <= now(),FOR UPDATE SKIP LOCKED。没有租借机制的队列默认会重复发送。
  • 2执行按顺序执行节点,直到遇到等待或终止节点。每个节点写入一条步骤:它决定了什么,发出了什么。
  • 3提交副作用与推进,同一个事务。这就是全部的诀窍。
  • 4休眠写入下一个 wake_at。下游的行还不存在,也不需要存在。

每个联系人每个版本一次运行

十四种触发器

什么可以启动一次运行

其中一些是你做的事。大多数是你的客户做的事,或者没做的事——而“没有动作”和动作本身一样,都是很好的触发器。

他们到来

form_submit、list_join、contact_created、tag_added 和 segment_enter。细分是一个实时谓词,所以“进入细分”涵盖了你能表达的所有“已变成”——下了三单、九十天没动静、消费超过某个门槛。

他们做了什么

来自网站代码片段的 page_visit、message_opened、link_clicked、booking_made 和 payment_made。消息被打开本身就是联系人上的一个事件,所以一个漏斗可以等待对它上一条消息的反应。

时间,或者你

带偏移量的 date_field——生日、续费、“满一年”、提前七天或之后三天。周期性的用 schedule。你因为自己决定要发而发的公告用 manual。

你自己的应用

api_event 是你用自己的令牌从自己的代码里发布的自定义事件。你的产品知道的任何事都能成为触发器:已激活、邀请了同事、达到上限、取消、导出。正是这一项把一个群发工具变成你产品的一部分。

重新进入,一次决定

漏斗要么允许重新进入,要么不允许,由数据库强制执行:除非你另有规定,每个联系人每个漏斗只有一次运行。同一个触发器在同一秒内触发两次,不会意外产生两次运行。

进入条件

触发器触发后,进入规则决定这个联系人是否有资格——门口的一道细分谓词。比在第一步放一个条件节点更省事,而且让流程图专注于“会发生什么”,而不是“谁被允许进来”。

十四种东西可以启动它。十六种东西可以接着发生。

十六种动作和七种流程节点

漏斗能做什么,以及它如何决策

发送点什么

send_email、send_sms、send_push、send_letter——四个渠道,一张流程图——再加上 notify_team,用于需要通知的人是你自己的员工时。

修改联系人

add_tag、remove_tag、add_to_list、remove_from_list、set_field。边走边做标记,正是让下一个漏斗的细分变得简单的原因。

触达外部

http_request 可以请求任何你想要的东西,create_payment 走你自己连接的账户,book_slot 预订预约资源。付款人始终属于你;没有人会变成我们的客户。

在漏斗之间移动

enter_funnel 和 exit_funnel。建一套欢迎序列,从四个地方调用它,而不是维护四份到三月就各奔东西的副本。

发送时生成

ai_generate 按联系人填充合并变量,计入你的配额,并按创意和联系人属性哈希缓存——否则你每次发送都要为每个收件人付费。

等待、分支、拆分、汇合

wait 接受一个时长、一个日期时间、一个星期几加时间,或一个带偏移量的日期字段。condition 是一个带“是”和“否”两个出口的分群谓词。split 是带权重的 A/B/n 拆分。join 把各分支重新汇合。

goal —— 真正重要的那一个

目标会为到达它的任何人结束整个流程。他们预订了、购买了、回复了:流程的其余部分会专门为他们取消。没有人会在回来的第二天收到“我们想你了”,而正是这个节点防止了这种情况。

throttle 和 quiet_hours

限流会为突发流量控制节奏,以免一个速率上限很低的邮箱被轰炸到被暂停。静默时段会把消息保留到合适的时间再发——这在消息层应用,而不是复制到图的每个分支里。

运行前先检查

验证器对你和 AI 拒绝的是同样的东西

图在发布前会被检查,而且这种检查不是建议性的。无论是人画的漏斗还是模型生成的漏斗,走的都是同一条代码路径——这也是生成的漏斗之所以值得信任的唯一原因。

恰好一个入口

两个入口节点是一种没人会注意到的歧义,直到列表中错误的那一半收到了错误的流程。

每个节点都可达

一个孤立的分支要么是个错误,要么是你以为会发出、实际却没有发出的消息。这两种情况都值得被告知。

循环中必须包含等待

一个没有等待的循环就是一个中间夹着发送操作的死循环。这条规则就是漏斗和事故之间的区别。

每次发送都有其素材

没有素材和主题的 send_email 节点无法发布。主题正确而正文为空的消息是一种真实存在的失败模式;它在这里被拒绝。

每个变量都是已知的

合并变量在发布时进行验证。{{frist_name}} 是你在编辑时就能看到的拼写错误,而不是“Hi ,”出现在四万个收件箱里。

然后模拟它

让一个真实联系人试运行整个图,然后读回所走的路径以及每条将要发送的消息及其日期。什么都不会真正发出去。

那些棘手的情况

会击垮天真实现的七件事

每一项背后都有一个带确定性时钟的测试,因为每一项都会击垮一个看起来完全正确、直到遇到现实才出问题的实现。

引擎的测试基准证伪
  • 1自我循环的漏斗合法、常见、而且没问题——前提是循环中包含一个等待,验证器会坚持这一点。
  • 2有人在流程中途退订退订抑制在发送时检查,而不是在入队时检查。在入队时检查会把在等待期间离开的人也发一遍。
  • 3漏斗在一万人进行中被编辑他们继续走完他们进入时的那个版本,直到结束。新进入者走新版本。
  • 4同一个触发器在一秒内触发两次只会有一次运行,因为唯一性是一个索引,而不是先检查再插入。
  • 5联系人在运行中途被删除运行结束。一个删除之后还有流程仍在向某人发送的删除,就不是真正的删除。
  • 6等待跨越夏令时切换“三天后的早上九点”意味着在时钟调整前后都是早上九点。
  • 7工作进程在发送和推进之间被杀死效果和推进是同一个事务。要么两者都发生了,要么都没有发生。

确定性时钟每个测试都能击破一种看似合理的实现

营销活动是另一半

漏斗是被动响应的。营销活动则是一次性的广播:一个受众、一个排期、一个限流和一次发送。它是另一种形态的东西,因此被刻意保持独立,而不是假装成一个只有一个节点的漏斗。

两条规则让它安全。受众只物化一次并拒绝运行第二次——重新解析会悄悄加入发送开始后新加入的所有人,而去重索引会通过阻止它自己制造的重复来掩盖这一点。计数器则从投递事件重新计算,绝不原地累加,因此重试的 webhook 无法虚增报表。

该用哪个:如果它发生是因为这个人做了什么,那就是漏斗。如果它发生是因为你某个周二做了个决定,那就是营销活动。相当多的营销自动化之所以出问题,就是因为硬把后者塞进前者的形状里。