ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Formbricks Workflows 数据模型与领域包:从节点图、触发器到运行时执行的完整解析

Formbricks Workflows 数据模型与领域包:从节点图、触发器到运行时执行的完整解析 Formbricks Workflows 数据模型与领域包从节点图、触发器到运行时执行的完整解析【免费下载链接】formbricksOpen Source Qualtrics Alternative项目地址: https://gitcode.com/GitHub_Trending/fo/formbricksFormbricks Workflows 是 Formbricks 中用于在“用户提交问卷响应”等事件发生后自动执行业务动作如自动发送邮件的工作流能力。formbricks/workflows是这套能力背后框架无关的领域包它用一组 Zod schema 定义了工作流的持久化结构、可执行结构、触发器、动作、条件以及运行日志的全部契约。本文以 packages/workflows/README.md 及其数据模型文档 packages/workflows/src/types/README.md 为主线结合仓库源码完整讲解该包的设计边界、每个数据对象的字段语义与 JSON 示例、图形校验规则以及如何安全地扩展新的动作与触发器。包定位与设计边界为什么 Workflows 需要独立成包Formbricks Workflows 的核心诉求是同一套工作流逻辑可以在多种运行时runner、builder、API 层之间复用。因此formbricks/workflows被设计为一个领域层包只负责定义和校验“工作流长什么样”不关心它被存储、渲染或执行的具体环境。从 packages/workflows/package.json 可以看到它的依赖面非常窄——运行时依赖只有zod其余均为 lint / 测试 / 构建工具链并对外暴露两个入口formbricks/workflows核心类型与校验由 packages/workflows/src/index.ts 统一导出analytics、contracts、execution、recipients、types五个子模块formbricks/workflows/server面向 runner 的服务端辅助入口packages/workflows/src/server/index.ts。包本身在 packages/workflows/README.md 中明确了三条硬性边界不得导入apps/web或任何 Next.js 专属 API——保持框架中立运行时适配器保持薄而显式——执行相关的胶水代码必须显式、克制依赖收窄应用/运行时关注点通过接口传入——应用侧可以通过接口把存储、发送邮件等能力注入进来而不是让领域包直接依赖具体实现。这种设计带来一个直接好处工作流对象可以在前端 builder用于编辑、后端 API用于持久化与 runner用于执行之间安全地传递而对象形状只定义一处。核心抽象一触发、多节点、有向边的持久化图formbricks/workflows的整套数据模型可以用四句话概括见 packages/workflows/src/types/README.md一个工作流有且只有一个触发器trigger触发器之后的工作由子节点child node表示边edge把节点连接成一张图运行负载payload与运行日志run log记录触发器被触发时实际发生了什么。围绕这四句话类型目录packages/workflows/src/types/被组织为actions/动作、triggers/触发器、conditions/条件、document.ts图与文档、runs.ts运行数据、common.ts共享对象与content.ts内容辅助。其中有两个贯穿全文的关键概念config 用户配置触发器与动作上的config字段只存放用户在工作流编辑器中填写的配置。任何运行时数据比如本次触发时的响应内容都不能放进config而应进入触发器负载、运行数据或运行日志。ui 纯构建器元数据只用于帮助工作流 builder 渲染画布绝不能影响执行。节点基类ZWorkflowNodeBase所有节点trigger、action、if_else都扩展自 packages/workflows/src/types/common.ts 中的ZWorkflowNodeBase字段类型语义idstring定义内稳定的节点 id边edge通过它引用节点typetrigger|action|if_else节点种类labelstring可选人类可读的节点标签长度限制 1–120uiZWorkflowNodeUi可选仅供构建器使用的元数据画布位置ZWorkflowNodePosition与构建器元数据ZWorkflowNodeUiZWorkflowNodePosition表示节点在 workflow builder 画布中的坐标只有x与y两个数字字段{ x: 320, y: 0 }ZWorkflowNodeUi是可选元数据position可选、collapsed可选节点是否折叠渲染并通过catchall(z.unknown())允许额外键——构建器可以自由添加颜色、视图偏好等展示信息而不必改动执行契约{ collapsed: false, color: blue, position: { x: 0, y: 0 } }动态数据引用ZWorkflowDataRefZWorkflowDataRef表示“运行时可用数据的一个引用”它用点路径dot path指向运行上下文中的某个值而不是把值拷贝进工作流定义字段类型语义pathstring运行上下文中的点路径fallbackstring可选路径无法解析时使用的兜底字符串{ fallback: supportexample.com, path: response.email }工作流图对象边、持久化定义与可执行定义边ZWorkflowEdge边是图中两个节点之间的连接packages/workflows/src/types/document.ts字段类型语义idstring定义内稳定的边 idsourcestring边起点的节点 idtargetstring边终点的节点 idsourceHandlestring可选源侧句柄if_else节点的出边必须使用then或elsetargetHandlestring可选目标侧句柄主要为 builder UI 与未来节点类型预留{ id: trigger-send-email, source: trigger, target: send-email, targetHandle: input }定义基类ZWorkflowDefinitionBaseZWorkflowDefinitionBase是所有持久化工作流文档的公共形状ZWorkflowDefinition与ZWorkflowExecutableDefinition都基于它做不同强度的校验字段类型语义schemaVersionnumber文档 schema 版本默认1常量WORKFLOW_SCHEMA_VERSION 1triggertrigger 节点 | null工作流唯一触发器草稿尚无触发器时为 null源码中.nullable()注释明确说明nodes子节点数组触发器之后运行的节点只能是 action 或 condition 节点不能是 triggeredges边数组触发器与子节点之间的连接entryNodeIdstring | null工作流入口必须引用触发器节点 id无触发器时为 null一个完整的持久化工作流文档示例response.completed触发器 send_email动作{ schemaVersion: 1, entryNodeId: trigger, trigger: { id: trigger, type: trigger, triggerType: response.completed, config: { surveyId: cm9zr4mps000008l8btfy1vtz, endingCardIds: [] } }, nodes: [ { id: send-email, type: action, actionType: send_email, config: { to: {{response.email}}, from: noreplyexample.com, replyTo: [supportexample.com], subject: Thanks for your response, body: We received your response., attachResponseData: true, includeHiddenFields: false, includeVariables: false } } ], edges: [ { id: trigger-send-email, source: trigger, target: send-email } ] }持久化定义ZWorkflowDefinition宽容的保存期校验ZWorkflowDefinition通过superRefine(validateWorkflowGraph)对图做引用级校验packages/workflows/src/types/document.ts 第 51–160 行。它拒绝以下情况重复的节点 id报Duplicate workflow node id: ...entryNodeId与触发器 id 不一致无触发器时 entryNodeId 必须为 null边引用了不存在的节点source 或 target 不在节点集合中if_else节点的出边未使用then/elsesourceHandle反之非if_else节点不允许使用这两个句柄触发器出边超过一条。注意只含触发器、没有任何节点和边的草稿是合法可保存的——宽松校验保证“保存工作进度”不被打断而“必须有一条出边”是执行期的要求。同时它对每个if_else节点强制要求恰好一条then出边与恰好一条else出边。可执行定义ZWorkflowExecutableDefinition严苛的运行期校验ZWorkflowExecutableDefinition是当前 runner 真正能执行的定义快照。它收紧了两处类型trigger与entryNodeId不再允许 null草稿期空值在启用时被剔除。在此之上validateExecutableGraph同文件第 175–267 行叠加了更严格的规则触发器必须恰好一条出边An executable workflow must have exactly one outgoing trigger edge每个子节点都必须从触发器可达从 trigger 出发做 BFS/DFS不可达节点报unreachable node ids图必须无环基于三色标记的 DFS 环检测Executable workflow graph must be acyclicif_else节点在当前版本中被拒绝执行if_else nodes are not executable in this version of workflows——条件分支已纳入数据模型并可通过持久化校验但 runner 尚未支持send_email动作必须内容非空源码通过getBlankSendEmailContentFields(node.config)packages/workflows/src/types/content.ts找出空白的邮件字段并报错send_email node ... is missing ...。原因正如源码注释所述持久化 schema 是宽容的以便保存半成品但“可执行”的send_email必须有真正可发送的内容。这个“保存宽容、执行严格”的双层设计是理解整套 workflow 契约的钥匙。触发器对象以response.completed为例枚举与判别字段触发器 id 集中在 packages/workflows/src/types/triggers/enum.ts当前只有RESPONSE_COMPLETED response.completed一种。动作 id 同理见 packages/workflows/src/types/actions/enum.ts当前只有SEND_EMAIL send_email。一个容易被忽略但很重要的设计原则README 中专门强调判别字段discriminator挂在节点上actionType/triggerType而不是藏在config内部。这样config可以纯粹地只描述用户配置且可以利用 Zod 的discriminatedUnion做精确的类型收窄。触发器配置ZResponseCompletedTriggerConfig“问卷响应完成”触发器的配置packages/workflows/src/types/triggers/response-completed.ts字段类型语义surveyIdcuid2响应完成会触发工作流的问卷 idendingCardIdscuid2 数组默认[]应触发工作流的结束卡 id空数组 所有结束{ surveyId: cm9zr4mps000008l8btfy1vtz, endingCardIds: [cm9zr4q7i000108l84gozfggr] }触发器节点ZWorkflowResponseCompletedTriggerNodetype恒为triggertriggerType恒为response.completed其余字段id、label、ui继承节点基类{ id: trigger, type: trigger, triggerType: response.completed, label: Survey response completed, config: { surveyId: cm9zr4mps000008l8btfy1vtz, endingCardIds: [] }, ui: { position: { x: 0, y: 0 } } }运行时负载ZWorkflowTriggerPayload负载是运行时事件上下文而非用户配置由 runner 在触发器触发时产生注意源码中使用了.catchall(z.unknown())允许未来附加运行时元数据字段类型语义typeresponse.completed负载类型workspaceIdcuid2运行所属的工作空间/租户上下文surveyIdcuid2收到响应的问卷responseIdcuid2触发工作流的已完成响应endingCardIdcuid2可选响应到达的结束卡datarecord可选提供给动作与条件使用的触发器数据如响应字段{ type: response.completed, workspaceId: cm9zr4wsp000508l8y6nh9r2v, surveyId: cm9zr4mps000008l8btfy1vtz, responseId: cm9zr4rsp000708l8bqccpfrx, endingCardId: cm9zr4q7i000108l84gozfggr, data: { response: { email: janeexample.com, score: 9 } } }动作对象send_email的配置与运行语义动作配置ZWorkflowSendEmailActionConfigsend_email是目前唯一的动作packages/workflows/src/types/actions/send-email.ts字段类型语义tostring收件人表达式字面邮箱地址或持有受访者邮箱的问卷问题/隐藏字段的 element idfromemail发件人邮箱语义见下文说明replyToemail 数组收件人回复时使用的地址subjectstring邮件主题最大长度 998MAX_SUBJECT_LENGTH对齐 RFC 5322 行宽上限bodystring邮件正文最大长度 100,000MAX_BODY_LENGTHGmail 对超过约 102KB 的邮件会截断渲染attachResponseDataboolean是否在邮件中包含响应数据includeVariablesboolean可选附加响应数据时是否包含调查变量includeHiddenFieldsboolean可选附加响应数据时是否包含隐藏字段{ to: {{response.email}}, from: noreplyexample.com, replyTo: [supportexample.com], subject: Thanks for your response, body: We received your response., attachResponseData: true, includeHiddenFields: false, includeVariables: false }源码注释揭示的运行细节重要安全语义该文件头部有一大段高质量注释揭示了若干与表面字段不一致的真实运行语义值得展开to的两类取值行为不同若为字面邮箱地址则必须属于能访问该工作流 workspace 的用户——启用/测试时校验失败会被拒绝runner 也拒绝向其发送防止把响应数据转发到任意外部邮箱对应 ENG-2029或被撤销访问权的人ENG-2186若为 element id则解析为受访者自己的邮箱地址不做 allowlist 校验。body支持 recall token正文是 HTML支持#recall:[elementId]/fallback:x#形式的回填 token运行时基于响应展开后再经白名单清理并包裹进品牌化的 Follow-ups 邮件模板subject则原样使用不应用 recall。from是“残留”字段实际发件人始终来自部署的MAIL_FROM环境变量与 Follow-ups 保持一致from仅用于派生稳定的 Message-ID 域名。长度上限是刻意的分叉subject与body在此有长度上限而 Follow-ups 没有——因为持久化 schema 宽容、任何消费方空白判定、recall 展开、清理都会遍历 body若不设上限唯一的封顶就是 16MB 的代理 body 限制。这一整套语义的目标是与既有调查 Follow-ups 能力保持 1:1 字段对等源码注释明确写到 “1:1 field parity with survey Follow-ups”从而让两种能力共享同一套运行时渲染逻辑。动作注册表与判别联合动作的注册集中在一处packages/workflows/src/types/actions/index.tsexport const WORKFLOW_ACTION_CONFIG_SCHEMAS { [WORKFLOW_ACTIONS.SEND_EMAIL]: ZWorkflowSendEmailActionConfig, } as const; export const ZWorkflowActionNode z.discriminatedUnion(actionType, [ZWorkflowSendEmailActionNode]);触发器侧对应packages/workflows/src/types/triggers/index.tsZWorkflowTriggerNode z.discriminatedUnion(triggerType, [ZWorkflowResponseCompletedTriggerNode])配置则通过TWorkflowTriggerConfigSchemas接口类型映射到ZResponseCompletedTriggerConfig。条件对象if_else分支已建模、暂不可执行条件逻辑由三部分组成packages/workflows/src/types/conditions/。单条条件ZWorkflowCondition字段类型语义idstring稳定的条件 idleftZWorkflowDataRef被检查的值引用operatorstring比较运算符equals、notEquals、lessThan、lessEqual、greaterThan、greaterEqual、contains、notContains、exists、notExistsrightany条件性右侧比较值存在性运算符exists/notExists必须省略right其余运算符必须提供{ id: score-high, left: { path: response.score }, operator: greaterThan, right: 8 }条件组ZWorkflowConditionGroup字段类型语义idstring稳定组 idconnectorand|orand 所有子项通过or 至少一个子项通过conditions非空数组ZWorkflowCondition或嵌套的ZWorkflowConditionGroup支持嵌套逻辑{ id: qualified-response, connector: and, conditions: [ { id: email-exists, left: { path: response.email }, operator: exists }, { id: score-high, left: { path: response.score }, operator: greaterThan, right: 8 } ] }分支节点ZWorkflowIfElseNodetype恒为if_elseconfig.condition是条件组。边规则恰好一条出边用sourceHandle: then恰好一条出边用sourceHandle: else持久化校验强制且文档图校验在validateWorkflowGraph中实现。该节点在通用工作流定义中合法但当前 runner 不可执行可执行定义会直接拒绝。{ id: condition, type: if_else, label: Qualified response, ui: { collapsed: true }, config: { condition: { id: qualified-response, connector: and, conditions: [ { id: score-high, left: { path: response.score }, operator: greaterThan, right: 8 } ] } } }运行与版本对象一次触发如何被记录工作流不只是“定义”还包括“运行痕迹”。这些对象定义在 packages/workflows/src/types/runs.ts 并由 packages/workflows/src/types/index.ts 统一导出。ZWorkflowTriggerRunPayload触发器负载的运行时快照继承ZWorkflowTriggerPayload全部字段并增加triggeredAtISO 时间字符串记录触发器被捕获的时刻。ZWorkflowRunLogInput/ZWorkflowRunLogOutput单个步骤被捕获的任意输入/输出对象形状取决于步骤类型与提供方{ subject: Thanks, to: janeexample.com }{ messageId: message-1, provider: smtp }ZWorkflowStepResult单步执行结果内存中或序列化后字段类型语义stepIdstring被执行步骤的节点 idstepTypestring步骤类型如send_emailstatuspending|running|succeeded|failed|skipped步骤状态input/outputobject可选步骤输入 / 输出errorstring可选错误消息startedAt/finishedAtISO string可选开始 / 结束时间{ stepId: send-email, stepType: send_email, status: failed, input: { subject: Thanks, to: janeexample.com }, output: { provider: smtp }, error: SMTP provider rejected the message, startedAt: 2026-06-09T12:01:01.000Z, finishedAt: 2026-06-09T12:01:03.000Z }ZWorkflowRunData运行级数据快照把触发器负载与步骤结果存放在一起允许额外键如示例中的attempt为未来重试元数据预留字段类型语义triggerZWorkflowTriggerRunPayload可选触发器快照stepsZWorkflowStepResult数组默认[]步骤结果ZWorkflowVersion不可变的已发布工作流定义供运行引用字段类型语义idstring版本 idworkflowIdstring所属工作流 idworkspaceIdstring工作空间/租户 idversionnumber单调递增的正整数版本号definitionZWorkflowExecutableDefinition可执行定义快照publishedAtISO string发布时间publishedBystring | null可选发布该版本的用户 idZWorkflowRunLog一条持久化的运行日志行是ZWorkflowStepResult的落库形态增加id、runId、sequence用于排序的序号字段且error、startedAt、finishedAt可为 null。三组状态枚举状态贯穿工作流生命周期、整次运行与单个步骤三个层级见 packages/workflows/src/types/common.ts 与 packages/workflows/src/types/README.md工作流生命周期ZWorkflowStatusdraft可编辑、未激活、enabled激活、disabled停用但保留、archived软删除默认读取排除整次运行ZWorkflowRunStatusqueued排队中、running执行中、completed成功完成、failed出错结束、canceled完成前被终止单步日志ZWorkflowRunLogStatuspending未开始、running执行中、succeeded成功、failed出错、skipped有意跳过。扩展指南如何新增动作与触发器packages/workflows/README.md 给出了明确的扩展开箱步骤结合源码可以看到每一步对应的落点。新增一个动作action在src/types/actions/enum.ts中把动作 id 加入WORKFLOW_ACTIONS常量枚举同时驱动ZWorkflowActionType的取值在src/types/actions/下新增 config schema 与 node schema参考send-email.ts的模式ZWorkflowXxxActionConfigZWorkflowXxxActionNodenode 通过ZWorkflowNodeBase.extend固定type: action与对应的actionType字面量把 config schema 注册进WORKFLOW_ACTION_CONFIG_SCHEMASpackages/workflows/src/types/actions/index.ts把 node schema 加入ZWorkflowActionNode的discriminatedUnion(actionType, [...])列表新增或更新 fixtures 与测试用新动作校验一份完整的工作流文档。新增一个触发器trigger在src/types/triggers/enum.ts中把触发器 id 加入WORKFLOW_TRIGGERS在src/types/triggers/下新增 config schema、需要的 payload schema 与 node schema参考response-completed.ts把 config schema 注册进TWorkflowTriggerConfigSchemas接口映射packages/workflows/src/types/triggers/index.ts把 node schema 加入ZWorkflowTriggerNode的discriminatedUnion(triggerType, [...])新增或更新 fixtures 与测试。贯穿始终的纪律config schema 只聚焦用户提供的配置任何运行时数据必须放进触发器负载、运行数据、运行日志或服务层输入绝不能混入config。同时保持ui字段只承载构建器元数据、不影响执行。测试与示例契约的落地验证该包配有完整的测试与 fixture是理解契约语义的绝佳佐证图校验测试packages/workflows/src/types/index.test.ts覆盖持久化定义与可执行定义各自拒绝/接受哪些文档内容校验测试packages/workflows/src/types/content.test.ts验证send_email空白字段判定与内容边界服务层测试packages/workflows/src/services/workflows.service.test.ts覆盖领域服务行为handler 测试packages/workflows/src/handlers/workflows.handlers.test.ts 与执行计划测试packages/workflows/src/execution/plan.test.ts验证 HTTP 处理抽象与执行计划生成。fixtures 目录 packages/workflows/src/types/fixtures/ 提供了六份可直接参考的完整 JSON 样例workflow-definition.full.json持久化定义、workflow-executable-definition.full.json可执行定义、workflow-trigger-payload.full.json触发器负载、workflow-run-data.full.json运行数据、workflow-run-log.full.json运行日志与workflow-version.full.json版本快照。新增动作/触发器时README 明确要求“新增或更新 fixtures 和测试来校验包含新节点的完整工作流文档”。此外analytics/、contracts/、execution/、handlers/、recipients/与services/子模块分别承载定义摘要、契约漂移检测spec-drift.test.ts、执行规划、HTTP 处理抽象与收件人解析等能力共同构成一个“定义、校验、规划、执行、记录”闭环的领域包。小结formbricks/workflows通过“框架无关的领域包 Zod 契约 双层校验”这套组合把工作流的定义、校验与运行支撑完整地封装起来ZWorkflowDefinition宽容地守护构建期草稿ZWorkflowExecutableDefinition严格地守护运行期快照response.completed触发器与send_email动作构成当前可用的最小闭环if_else条件分支则已进入数据模型、等待 runner 支持。对于想要深度集成或扩展 Formbricks Workflows 的开发者理解config/ui/payload 的三分法、节点判别字段的位置以及“持久化宽容、执行严格”的校验哲学是正确使用和扩展这套能力的关键。【免费下载链接】formbricksOpen Source Qualtrics Alternative项目地址: https://gitcode.com/GitHub_Trending/fo/formbricks创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表