ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

面向 Novu 的通知工作流设计:渠道选型、严重度与摘要(Digest)的决策指南

面向 Novu 的通知工作流设计:渠道选型、严重度与摘要(Digest)的决策指南 面向 Novu 的通知工作流设计渠道选型、严重度与摘要Digest的决策指南【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu设计一个正确形状的通知工作流难点通常不在敲代码而在决策选哪些渠道、要不要把通知标记为HIGH、是否走摘要聚合、离线订阅者怎么兜底。本文以 Novu 仓库中的design-workflow技能文档为骨架完整梳理这套适用于Dashboard无代码与novu/framework代码优先双表面的统一设计规则——在两种表面上决策维度完全一致只是语法不同。读完你可以对照 9 个参考模板为订单确认、支付失败、找回密码、试用到期、评论通知等真实场景快速产出可落地的工作流设计。该技能文档位于 docs/.mintlify/skills/design-workflow/SKILL.md配套的 5 份参考文档分别覆盖渠道选型、严重度与关键性、摘要默认值、步骤条件与模板全文本文会随章节逐一引用。本技能适用的边界design-workflow技能只回答工作流应该长成什么样这一类问题例如设计一个订单确认工作流支付失败应该走哪些渠道把这个通知设为 critical这条通知该做 digest 吗给离线订阅者加一个兜底渠道场景 X 对应的正确模板是什么按文档约定它不适用于以下任务它们由其他技能承接触发一条已存在的工作流 → trigger-notification编写代码侧工作流包装器workflow(...)、step.*、controlSchema、Bridge Endpoint→ framework-integration在 Inbox UI 中渲染严重度样式 → inbox-integration工作流设计完成后无论你在哪种表面创作步骤正文subject、body、editorType、headers、conditions都应交给 dashboard-workflows 技能填充。两个相互独立的旋钮severity 与 criticalNovu 把这条通知多重要拆成了两个彼此独立的工作流级开关。文档特别强调绝大多数工作流两者都不要设置。开关取值默认值作用severityLOW/MEDIUM/HIGH不设置决定 Inbox 中的视觉优先级颜色、光晕、铃铛配色并参与摘要跳过规则的判断criticaltrue/falsefalse运行时覆盖绕过订阅者偏好、跳过 digest、取消一切延迟、并行触发所有可用渠道经验法则大多数工作流保持severity不设只有需要视觉分层时才设置HIGH表示今天就处理支付失败、明天试用到期critical: true表示无视偏好也要送达账号被封、安全告警、重置密码critical: true⇒ digest 自动被跳过渠道立即投递。两个开关不互相依赖一个工作流可以critical: true而severity留空。这在代码侧也有印证——workflow.resource.ts 中创建工作流时severity: options.severity ?? SeverityLevelEnum.NONE默认即不设置workflow.types.ts 中severity为可选的SeverityLevelEnum。severity 的语义纯视觉severity 值及其使用建议见 references/severity-and-critical.md值含义适用场景不设置无优先级默认。绝大多数工作流LOW纯告知、无紧迫性营销推送、低优先级生命周期通知MEDIUM值得浮出提示提及、轻度告警HIGH今天就处理支付失败、明天试用到期、需要 KYCseverity不改变偏好、digest 或投递行为只影响 Inbox 对通知的渲染颜色、光晕、铃铛色并作为 digest 跳过规则的一条输入。其视觉映射colorSeverityHigh、severityHigh__notificationBar等可参见 inbox-integration 中的 Severity styling 章节。critical 的语义运行时强投当critical: true时绕过订阅者偏好订阅者即便关掉了该渠道也照样送达跳过 digest每次触发立即投递不做聚合无延迟所有步骤以最快速度执行所有可用渠道并行触发Push 仍受subscriber.isOnline false条件约束。请把critical: true留给必须送达的事件账号封禁、安全告警新设备登录/可疑活动、忘记密码/OTP、法律合规通知。readOnly vs critical最容易踩的陷阱文档用一张对照表区分了两个经常被混淆的标志标志归属位置作用preferences.all.readOnly: true工作流的偏好默认值把该工作流从偏好设置 UI中隐藏订阅者无法为其切换渠道critical: true工作流级标志运行时绕过偏好、digest 与延迟无视退订照常投递实践中你通常需要的是critical: true强制投递。单独的readOnly: true只是把 UI 开关藏起来并不能覆盖订阅者已有的偏好覆盖。preference 的解析顺序见 manage-preferences。完整行为矩阵场景应用偏好运行 digest应用延迟Inbox 样式severity不设、critical: false是是是默认severity: HIGH、critical: false是否是高优先级severity: HIGH、critical: true否否否高优先级severity不设、critical: true否否否默认规律可归纳为severity: HIGH或critical: true任一成立时 digest 自动跳过critical: true一票否决偏好、digest 与延迟。典型组合建议用例severitycritical订单确认不设false帖子收到评论不设false支付失败HIGHfalse明天试用到期HIGHfalse账号被封需 KYCHIGHtrue忘记密码 / OTP不设true安全告警HIGHtrue营销 / 周报不设false渠道选型渠道决策的第一分叉是用户是否点名了渠道。完整决策树见 references/channel-selection.md。规则一用户点名的渠道是精确的如果用户说了渠道发货时发一条 push、只发邮件和短信那就只用这些渠道不加兜底、不加额外渠道即使某个被点名的渠道在组织里尚未配置也要照加——因为用户明确要求了。下单发货时创建一条 push 通知Trigger ↓ Push发票逾期时通过 email 和 SMS 通知用户Trigger ↓ Email ↓ SMS规则二未指定渠道时的默认选型用户未指定渠道时从组织已配置的渠道中按以下优先级挑选上限 3 个渠道In-App Email Chat Push SMS注意上限 3 个不等于取前 3 个——允许跳档应当选出最相关的一个子集。各渠道的用途与取舍渠道用在哪何时跳过In-App产品内内容的默认渠道。只要收件人在你的产品内就应包含收件人看不到它重置密码、OTP、注册前、账户尚未创建时Email回执、文档、异步沟通用户日后想再翻出来看的东西。In-App 之后的默认兜底纯粹的产品内会话式提醒改用 In-App 或 PushChatSlack/Teams/Discord该渠道已配置且severity MEDIUM时加入。最适合运维、部署、内部告警、B2B 流程营销或低严重度提醒以及面向 C 端用户的内容Push订阅者离线但需要即时感知时的兜底订阅者在线此时 In-App 已覆盖SMS最后的退路。留给真正的紧急情况、OTP、合规要求凡是 Email 或 Push 能覆盖的Push 永远要配一条步骤条件仅在subscriber.isOnline false时发送。把渠道选型和 severity 联动无用户偏好时的建议组合为Severity建议渠道组合无用户偏好不设In-App Email 离线时 PushLOWIn-App EmailMEDIUMIn-App Email Chat若已配置HIGHIn-App Chat Email Push若离线critical: true所有可用渠道并行Push 由离线条件门控Digest 默认配置新增 digest 步骤时文档给出的默认值如下同时适用于 Dashboard digest 与 Frameworkstep.digest字段默认值Typeregular回看窗口look-back window5 分钟摘要时长digest time1 小时Digest keysubscriberIdregular摘要的行为是首个触发事件到达后收集落在回看窗口内的后续事件再等够摘要时长统一发送聚合通知。套用上述默认值就是首个触发最多等待 1 小时把彼此 5 分钟内到达的匹配事件一把捞进同一封摘要。跳过 digest 的时机severity: HIGH或critical: true——高严重度和关键流程必须即时送达digest 步骤自动跳过。完整说明见 references/digest-defaults.md。Digest key 的组装规则digest key 决定什么算同一封摘要默认subscriberId让每个用户各自收自己的摘要。需要更细粒度分组时往 key 里追加维度模式适用场景subscriberId每个收件人一封摘要默认subscriberId threadId会话式流程——每个主题帖/话题一封摘要评论、回复、提及subscriberId projectId按项目拆分的动态流subscriberId organizationId多租户场景下按组织拆分的摘要不加threadId的后果很典型帖子 A 和帖子 B 上的评论会掉进同一封摘要用户看到5 条新评论而非帖子 A 有 2 条、帖子 B 有 3 条。会话式摘要示例评论你的帖子工作流的标准形状Trigger (event: payload.threadId: post_123) ↓ Digest: type regular, look-back 5min, digest time 1h Key: subscriberId threadId ↓ In-App Redirect: → thread每个线程各得一份摘要帖子 123 的评论不会与帖子 456 混在一起。何时不添加 digest单事件流程单笔订单的订单确认、单次请求的密码重置critical: true的工作流digest 反正会被绕过高严重度告警severity HIGH语义下要求即时用户点名渠道但并未要求批量聚合的流程。何时应当添加 digest高频会话事件评论、提及、表态、关注活动动态流你的项目有 5 条新动态短窗口内可能多次触发的生命周期提醒。定时摘要cron 型 digest需要固定计划如工作日早 9 点时改用 cron 表达式而非look-back digest timeDashboard把 digest 类型切到cron并填入 cron 字符串Framework给step.digest传cron: 0 9 * * 1-5以替代unit/amount。代码侧的支撑可以在仓库中找到。在 digest.schema.ts 中digest 的输出 schema 被定义为regular与timed二选一regular需要amountunit单位枚举seconds/minutes/hours/days/weeks/months并可携带lookBackWindow、digestKey与extendToScheduletimed则需要cron。digest 的结果 schema 暴露eventCount与events数组——这正是步骤条件中引用steps.digestId.events/.eventCount的数据来源。对应的 schema 校验测试位于 digest.schema.test.ts分别验证了{ amount: 1, unit: seconds }与{ cron: 0 0-23/1 * * * }两种形态可以通过validateData校验。digest 的常见坑会话流忘加threadId给critical工作流硬加 digest虽会被自动跳过但属于掩盖意图的代码异味一个工作流放两个 digest 步骤不被支持需用自定义步骤链式工作流或在第二个工作流里触发回看窗口设得过长导致送达感延迟保持 look-back ≤ digest time。用户在线状态与路由决策路由要依据订阅者是否在线来动态调整状态行为在线In-App 立即发送跳过 PushEmail/Chat 依据 severity 做延迟离线用 Push 或 Chat 抓取注意力订阅者离线这一条件的判定在 Dashboard 与 Framework 两种表面上完全一致实现见 references/step-conditions.md。默认延迟建议B2B应用 → 推迟到下一个工作时间B2C应用 → 约 30 分钟。步骤条件Step Conditions同一语义、两套语法条件决定某个步骤是否执行。两种创作表面对比创作表面语法Dashboard无代码在step.condition上写 JSON-Logic如{ : [...] }Frameworknovu/framework给步骤传skip: () boolean \| Promiseboolean回调两套语义互为镜像移植时记得反转布尔值Dashboard条件求值为true⇒ 步骤执行。 Frameworkskip返回true⇒ 步骤被跳过。条件里可用的变量文档强调只使用当前作用域内的变量并建议优先复用已有变量仅在确有必要时引入新的payload.*变量否则模板与条件会难以维护。变量分四个命名空间命名空间来源内容workflow.*系统工作流元数据workflowId、name、description、tags、severitysubscriber.*系统收件人信息firstName、lastName、email、phone、avatar、locale、timezone、subscriberId、isOnline、lastOnlineAt、datapayload.*用户定义触发时传入的事件数据如actionUrl、productName、orderNumber。定义了payloadSchema时会被校验steps.*系统步骤结果In-App 的seen/readdigest 的events/eventCountHTTP 响应属性仅限responseBodySchema中声明的context.*用户定义触发时传入的多租户元数据如tenant、region、appsubscriber.*可用的完整属性列表firstName、lastName、email、phone、avatar、locale、timezone、subscriberId、isOnline、lastOnlineAt以及可深层寻址的自定义数据subscriber.data.key。步骤输出可用路径路径何时可用说明steps.stepId.seenIn-App 步骤之后布尔用户看到通知后为truesteps.stepId.readIn-App 步骤之后布尔用户标记已读后为truesteps.stepId.eventsdigest 步骤之后被聚合的触发事件数组steps.stepId.eventCountdigest 步骤之后events的长度模板中更顺手steps.stepId.propHTTP 步骤之后只有responseBodySchema声明的属性可寻址Dashboard 侧标准 JSON-Logic 片段订阅者离线{ : [{ var: subscriber.isOnline }, false] }In-App 未被阅读触发邮件兜底{ : [{ var: steps.stepId.read }, false] }In-App 未被看到{ : [{ var: steps.stepId.seen }, false] }工作流 tags 命中任一{ in: [tag1,tag2, { var: workflow.tags }] }HTTP 响应属性等于某值{ : [{ var: steps.http-step-id.status }, active] }Framework 侧等价实现Framework 侧语义相反——skip返回true即跳过。下面的例子分别对应仅离线时发送、In-App 未读才发邮件兜底与按 HTTP 响应分支const inApp await step.inApp(inbox, async () ({ /* ... */ })); await step.push(offline-push, async () ({ title: ..., body: ... }), { skip: ({ subscriber }) subscriber.isOnline true, });const inApp await step.inApp(inbox, async () ({ /* ... */ })); await step.delay(wait, async () ({ unit: hours, amount: 4 })); await step.email(fallback, async () ({ subject: ..., body: ... }), { skip: () inApp.read true, });const plan await step.http(fetch-plan, async () ({ method: GET, url: https://api.example.com/users/${payload.userId}/plan, responseBodySchema: { type: object, properties: { status: { type: string } }, required: [status], } as const, })); await step.email(notify, async () ({ /* ... */ }), { skip: () plan.status ! active, });双语速查表意图DashboardJSON-LogicFrameworkskip仅订阅者离线时运行{ : [{ var: subscriber.isOnline }, false] }skip: () subscriber.isOnline true仅 In-App 未读时运行{ : [{ var: steps.inbox.read }, false] }skip: () inAppResult.read true仅 In-App 未看时运行{ : [{ var: steps.inbox.seen }, false] }skip: () inAppResult.seen true仅 tagged billing 的工作流运行{ in: [billing, { var: workflow.tags }] }skip: () !tags.includes(billing)仅 HTTPstatus active时运行{ : [{ var: steps.fetch.status }, active] }skip: () fetchResult.status ! active步骤条件的常见坑布尔反了——Dashboard 在条件为true时执行Framework 在skip为true时跳过二者互为相反引用未声明的 HTTP 属性——只有responseBodySchema中的属性能通过steps.http.prop寻址把subscriber.isOnline true当字符串用——isOnline是布尔JSON-Logic 里的false是字符串字面量Framework 里要用 JS 布尔false在 delay 步骤上套条件——delay 也支持 skip但一旦被跳过流程会立即继续别把 skip 当缩短等待用。九大工作流参考模板每个模板由一张元数据表与 ASCII 流程图构成。元数据字段含义SeverityLOW/MEDIUM/HIGH/不设、Criticaltrue⇒ 绕过偏好、跳 digest、即时送达、ActionableInformational纯告知 /Requires Action需用户操作、InteractionUSER TRANSACTION、CONVERSATIONAL、SYSTEM TRANSACTION、LIFECYCLE。标注(if channel is configured)的渠道仅在组织已配置对应集成时加入。全部 9 个模板的完整版见 references/workflow-templates.md。主文档中的速查总表#用例SeverityCritical备注1订单确认不设false走 digestIn-App Email Push仅离线2帖子收到评论不设false按subscriberId threadId聚合3支付失败HIGHfalseIn-App Chat Email Push离线4账号封禁HIGHtrue全部渠道、无视偏好、无 digest5忘记密码不设true仅 Email SMS无 In-App6明天试用到期HIGHfalseIn-App Chat Email Push离线7用户点名渠道n/an/a只使用用户指定的渠道8Webhook / 外部 API 调用variesvaries在渠道步骤之后追加step.http9先取数再通知variesvariesstep.http在前并声明responseBodySchema模板 1订单确认Order ConfirmationSeverity不设CriticalfalseActionableInformationalInteractionUSER TRANSACTIONTrigger ↓ Digest: type regular, look-back 5min, digest time 1h Key: subscriberId ↓ In-App ↓ Email ↓ Push (if channel is configured) Step condition: Send only if subscriber is offline模板 2评论你的帖子Comment on Your PostSeverity不设CriticalfalseActionableInformationalInteractionCONVERSATIONALTrigger (event: payload.threadId: post_123) ↓ Digest: type regular, look-back 5min, digest time 1h Key: subscriberId threadId ↓ In-App Redirect: → thread ↓ Push (if channel is configured) Step condition: Send only if subscriber is offline ↓ Delay (4 hours) Step condition: Only if In-App not seen ↓ Email Content: summary of the comments Step condition: Only if In-App not seen模板 3支付失败Payment FailedSeverityHIGHCriticalfalseActionableRequires ActionInteractionUSER TRANSACTIONTrigger ↓ In-App ↓ Chat (if channel is configured) ↓ Email ↓ Push (if channel is configured) Step condition: Send only if subscriber is offline模板 4账号封禁Account SuspendedSeverityHIGHCriticaltrueActionableRequires ActionInteractionSYSTEM TRANSACTION关键流程特征绕过订阅者偏好、无延迟即时投递、所有可用渠道并行。Trigger (event: payload.account.suspended, payload.reason: kyc_required) ↓ In-App ↓ Email ↓ SMS (if channel is configured) ↓ Chat (if channel is configured) ↓ Push (if channel is configured) Step condition: Send only if subscriber is offline模板 5忘记密码Forgot PasswordSeverity不设CriticaltrueActionableRequires ActionInteractionSYSTEM TRANSACTION不包含 In-App 步骤——触发该流程时用户根本未登录。Trigger ↓ Email ↓ SMS (if channel is configured)模板 6明天试用到期Trial Expiring TomorrowSeverityHIGHCriticalfalseActionableRequires ActionInteractionLIFECYCLETrigger ↓ In-App ↓ Chat (if channel is configured) ↓ Email ↓ Push (if channel is configured) Step condition: Send only if subscriber is offline模板 7用户点名渠道Explicit Channel Request用户说下单发货时创建一条 push 通知Trigger ↓ Push规则用户点名时只用这些渠道不加兜底、不加额外即使该渠道在组织内尚未配置也照加——用户明确要求了它。模板 8Webhook / 外部 API 调用SeverityHIGHCriticalfalseActionableRequires ActionInteractionUSER TRANSACTION用户示例支付失败时通知用户并调用我们的 webhookTrigger ↓ In-App ↓ Email ↓ HTTP Request method: POST url: {{payload.webhookUrl}} headers: [{ key: Content-Type, value: application/json }] body: [ { key: event, value: payment_failed }, { key: subscriberId, value: {{subscriber.subscriberId}} } ] continueOnFailure: true任何需要在发通知之外调用外部 API/webhook 的工作流都适用 HTTP Request 步骤。模板 9先取数再通知Fetch Data Then Notify用户示例从我们的 API 拉取用户套餐然后发送一封个性化邮件Trigger ↓ HTTP Request (stepId: fetch-plan) method: GET url: https://api.example.com/users/{{payload.userId}}/plan responseBodySchema: { type: object, properties: { planName: { type: string }, renewalDate: { type: string } }, required: [planName, renewalDate] } ↓ Email subject: Your {{ steps.fetch-plan.planName }} plan body: Your plan renews on {{ steps.fetch-plan.renewalDate }}.规则当下游步骤要引用 HTTP 响应数据时HTTP 步骤必须声明responseBodySchema只有 schema 中声明的属性可以通过{{ steps.http-step-id.property }}寻址。主文档第 8 条陷阱与此一致——没有responseBodySchema下游模板就无法用{{ steps.id.prop }}读取响应字段。常见陷阱汇总默认别设 severity——除非真的需要视觉分层保持不设critical: true≠readOnly: true——readOnly只是把工作流从偏好 UI 里隐藏critical是在运行时绕过偏好与 digest见 references/severity-and-critical.md用户点名渠道时不要自作主张加兜底——点名的请求是精确的未指定渠道时渠道数量封顶 3 个——渠道越多越打扰不代表触达更广不要把 digest 和critical: true混用——关键流程必须即时送达digest 步骤会被自动跳过会话流里 digest key 至关重要——缺了threadId帖子 A 与帖子 B 的评论会聚到同一封摘要只在离线时发 Push——给在线用户发 Push 等于重复 In-App 告警HTTP 步骤需要responseBodySchema——否则下游步骤无法通过{{ steps.id.prop }}读取响应属性。深入阅读与周边技能本文对应的技能文档与参考资料均可在仓库中继续深挖主文档design-workflow/SKILL.md渠道选型——完整决策树与逐渠道指引Severity Critical——行为矩阵、偏好与 digest 交互、readOnlyvscriticalDigest 默认值——窗口、key 组合与会话式 digest 模式步骤条件——JSON-Logic 片段与 Frameworkskip等价写法工作流模板——9 个参考流程的 severity/critical/interaction 元数据表按创作表面与后续动作还需要配合以下技能dashboard-workflows——在 Dashboard 或 Novu MCP 上填充步骤正文subject、body、editorType、headers、conditionsframework-integration——用代码实现上述设计workflow()、step.*、controlSchema、Bridgemanage-preferences——理解critical如何与订阅者级偏好交互inbox-integration——severity 如何在 Inbox 中视觉呈现trigger-notification——工作流设计完成后如何触发调用。值得留意的是这些设计规则并非悬空约定在 Framework 源码中可以找到对应落地例如 workflow.resource.ts 将未显式传入的severity默认回落到SeverityLevelEnum.NONE对应主文档大多数工作流不设 severity的默认语义而 digest.schema.ts 把 digest 严格建模为regularamountunit可选lookBackWindow/digestKey与timedcron两种 schema 并以校验测试锁定行为。理解这两处源码能帮你把本设计文档中的每一项建议落到可执行的代码层。【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进