
OneUptime 工作流变量完全指南全局变量、局部变量与组件输出的引用、安全与实践【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime导读OneUptime 的 Workflow工作流本质上是搬运数据数据从触发器流向第一个组件从上一个组件流向下一个组件从共享值流向任何需要它的位置而变量Variables正是承载这些数据流转的媒介。本文基于 OneUptime 仓库中的工作流变量官方文档结合Common/Models/DatabaseModels/WorkflowVariable.ts、App/FeatureSet/Workflow/Services/RunWorkflow.ts、Common/Server/Utils/VM/VMAPI.ts等源码实现系统讲解全局变量、Workflow 局部变量与组件输出三种引用方式的使用方法、{{#each}}数组遍历语法、通过 OneUptime API 从工作流内部轮换密钥的实战模式以及最容易踩坑的解析规则。读完本文你将能安全地在工作流中引用与管理变量并理解未解析引用、秘密值脱敏等底层行为背后的原理。变量体系概览两种作用域 组件输出OneUptime 工作流中存在两类变量作用域外加一类由组件在运行期间产生的输出引用形式作用域生命周期{{global.variables.NAME}}全局变量项目级别所有工作流共享持久存在直到被删除{{local.variables.NAME}}Workflow 局部变量仅属于某一个工作流仅存在于一次运行期间每次新执行都从零开始{{local.components.COMPONENT_ID.returnValues.FIELD_ID}}组件输出指向前序触发器/组件产生的返回值仅存在于一次运行期间从源码层面看这三种引用在运行时被统一收集进一张存储映射表StorageMap。在 RunWorkflow.ts 中可以看到它的完整结构定义export interface StorageMap { local: { variables: Dictionarystring; components: { [x: string]: { returnValues: JSONObject; }; }; }; global: { variables: Dictionarystring; }; }也就是说global.variables与local.variables都是「变量名 → 字符串内容」的字典local.components则按组件 ID 存放每个组件执行后产出的returnValues对象。所有{{...}}模板表达式最终都会对这个存储映射表做查找与替换。全局变量一次存储处处复用全局变量适合存放项目级共享值——API 密钥、URL、渠道名称等不希望被复制到十个工作流里的内容。它们的管理入口位于界面左侧的Arbeitsabläufe工作流→ Globale Variablen全局变量。字段说明每个全局变量包含四个字段Name名称用于引用变量的标识。至少两个字符不能包含空格只允许字母、数字、连字符-和下划线_。官方文档建议使用UPPER_SNAKE_CASE全大写蛇形命名的命名习惯因为它在工作流组件参数中非常醒目能一眼认出是变量引用。Beschreibung描述可选自由文本用于提醒自己该变量的用途。Geheimnis秘密开关。开启后该变量的值会在执行日志与步骤跟踪step traces中被移除替换为[REDACTED]。Inhalt内容变量的实际值。这是一个长文本字段因此支持多行值。在数据模型 WorkflowVariable.ts 中这些字段与数据库列一一对应name为必填短文本且带有UniqueColumnBy([workflowId, projectId])唯一约束同一作用域内名称唯一description为可选长文本content为必填超长文本VeryLongTextisSecret为布尔开关默认false。如何使用全局变量在任何工作流的任意组件中用以下模板表达式引用{{global.variables.NAME}}例如将 PagerDuty 密钥保存为全局变量PAGERDUTY_KEY后任意组件都能通过{{global.variables.PAGERDUTY_KEY}}引用它。编辑器保存的是引用本身而非展开后的明文工作流日志系统则会移除解析后的秘密值。全局与局部的本质区别workflowId 是否为空从数据模型看全局变量与局部变量共用了同一张数据库表WorkflowVariable区别只在于workflowId字段workflowId为null时是全局变量非空时属于对应工作流。模型中workflow字段的注释明确写道Workflow this variable belong to. If this is null then this variable will be a global variableWorkflowVariable.ts。运行引擎在 RunWorkflow.ts 的 getVariables 方法 中分别查询两类变量局部变量按workflowId精确匹配全局变量用workflowId: QueryHelper.isNull()查询即 SQL 中的IS NULL再按projectId过滤最终合并进 StorageMapconst localVariables: ArrayWorkflowVariable await WorkflowVariableService.findBy({ query: { workflowId: workflowId }, select: { name: true, content: true, isSecret: true }, // ... }); const globalVariables: ArrayWorkflowVariable await WorkflowVariableService.findBy({ query: { workflowId: QueryHelper.isNull(), projectId: projectId }, select: { name: true, content: true, isSecret: true }, // ... });创建与删除而非编辑界面上的变量表格没有编辑按钮变量只能创建和删除。想要在界面上修改某个值需要先删除该变量再重新创建。文档同时指出也可以通过 API 更新变量见下文从工作流中更新变量一节。此外官方文档明确说明全局变量与 Workflow 变量是 Growth 付费计划Growth Plan的功能。这一限制同样体现在数据模型的计费访问控制装饰器中——TableBillingAccessControl将 WorkflowVariable 的增删改查全部绑定到了PlanType.GrowthWorkflowVariable.ts。Workflow 局部变量仅属于单个工作流局部变量只对某一个工作流可见管理入口位于该工作流左侧菜单的Workflow-VariablenWorkflow 变量中。引用方式{{local.variables.NAME}}关键特性是生命周期局部变量只存在于一次正在进行的运行中。每次新的执行都会从零开始构建局部变量映射运行结束即消失。这与全局变量的持久性形成鲜明对比。从存储映射表的构建过程可以印证这一点RunWorkflow.getVariables每次运行都会重新从数据库加载局部变量并写入newStorageMap.local.variables因此即使上一个运行周期中发生了变量更新新一次执行也会读取到数据库中的最新值。组件输出引用前序组件的返回值每个触发器Trigger和每个组件在一次执行中都会产生输出Outputs。引用组件输出时官方强烈建议使用编辑器中的**组件值选择器Komponentenwert-Auswahl**来生成引用而不是手工输入——选择器会精确插入 Runner 期望的组件 ID、返回值 ID避免手误。语法结构{{local.components.COMPONENT_ID.returnValues.FIELD_ID}}拆解这三个关键部分COMPONENT_ID组件的Identifier标识符——印在组件块上的短 ID不是组件上显示的名称。新添加的组件会得到类似api-get-1的默认 ID。你可以在组件的ID区块中重命名它。重命名会破坏所有已经指向它的引用——这与重命名变量的后果完全一致。returnValues固定关键字表示读取返回值集合。FIELD_ID所选返回值的 ID例如response-status、response-body、returnValue。在存储映射表中storageMap.local.components[COMPONENT_ID] { returnValues: ... }由运行引擎在每个组件执行完成后立即写入RunWorkflow.tsstorageMap.local.components[stackItem.node.id] { returnValues: result.returnValues, };因此后序组件引用前序组件时只需要在模板表达式中给出组件 ID 与返回字段 ID 即可。常见组件的返回字段示例API 组件假设组件 ID 为lookup-user运行后其状态码为{{local.components.lookup-user.returnValues.response-status}}响应体为{{local.components.lookup-user.returnValues.response-body}}。Run Custom JavaScript 组件假设组件 ID 为transform其返回值是{{local.components.transform.returnValues.returnValue}}。数据集类型触发器On Create Incident及其同类触发器只返回一个值model你可以深入该对象读取字段。例如 ID 为incident-on-create-1的触发器事件标题为{{local.components.incident-on-create-1.returnValues.model.title}}。组件输出的作用域同样限定在当次执行内每次新运行都从零开始这与局部变量一致。变量在哪些地方可用文本字段与 JSON 字段的行为差异几乎所有文本字段都接受变量API 组件的 URLSlack、Teams、Discord、Telegram、E-mail 等通知组件的消息正文邮件组件的主题与正文Header 与 Body 字段在字符串值内部If / Else组件的两侧比较值位于 Conditions 分类下。JSON 字段字符串内可用裸引用可整体替换在 JSON 字段中变量可以用于字符串值内部但不能作为 JSON 键。当一个引用单独占满整个值时它会被裸naked插入——也就是说解析结果会直接把整个对象嵌入 JSON 字段而不是套上引号。这样做的意义在于你可以把一个完整对象放进 JSON 字段。如果需要在运行期动态构建复杂结构官方建议的做法是在Run Custom JavaScript组件中构造该结构然后把它的输出传给下一个组件。Run Custom JavaScript 不会自动获得变量Run Custom JavaScript 组件的沙箱里不会自动注入任何变量——沙箱中不会预先传入任何东西。正确的做法是把{{global.variables.NAME}}或任何组件引用写进该组件的ArgumentsJSON 字段这些值会在脚本运行之前被解析替换并以args的形式传入脚本。从实现上看这一行为源于运行引擎的参数解析流程getComponentArguments在组件真正执行前对所有参数值调用VMAPI.replaceValueInPlace(storageMap, ...)完成模板替换RunWorkflow.ts。换句话说变量解析发生在脚本沙箱之外——沙箱永远只看到解析后的args而非模板表达式本身。数组遍历{{#each}} 循环在文本字段中可以使用 Handlebars 风格的块表达式遍历数组{{#each path}}…{{/each}}在循环块内部{{property}}读取当前元素的属性{{index}}表示从 0 开始的当前位置索引{{this}}对于简单值数组代表元素本身。一个容易被忽略的细节是{{#each}}块内部的名称会被去除首尾空格trim所以循环体内多余的空白是无害的——这与块外的其他所有位置恰恰相反块外空格会导致解析失败详见容易踩的坑。底层实现先展开循环再做变量替换在 VMAPI.ts 的 replaceValueInPlace 方法 中替换流程分为两步首先调用VMUtil.expandEachLoops(storageMap, valueToReplaceInPlaceCopy, shouldEscapeForJSON)先展开所有{{#each}}...{{/each}}循环块然后才用正则/{{(.*?)}}/g匹配剩余变量并逐一从 storageMap 中查找替换。expandEachLoops支持循环体内部的{{variableName}}相对当前数组元素解析{{index}}解析为当前迭代的 0 基索引嵌套的{{#each}}块用于多层数组遍历循环体内仍可访问父级 storageMap 的属性。实现中每次迭代都会把当前元素属性合并进一个作用域化的 storageMap{ ...storageMap, [elementProperty]: ... }再递归展开嵌套循环VMAPI.ts 附近。另外未匹配的{{#each}}标签会被移除以避免死循环。实战示例一从 Webhook 构造一个 Incident假设收到一个 Webhook其请求体形如{ service: checkout, status: failed }希望据此创建一条 OneUptime 事件Incident添加Webhook触发器ID 设为ci-webhook。添加If / Else组件左侧选择 Webhook 的 Request Body 输出取其status属性操作符选右侧填failed。从Ja是分支引出Create One Incident组件填写Titel标题CI build failed: {{local.components.ci-webhook.returnValues.request-body.service}}Beschreibung描述See {{local.components.ci-webhook.returnValues.request-body.url}} for the logs.当构建checkout流水线失败时Webhook 携带的service与url字段就会被解析进事件标题与描述中实现构建失败自动生成事件。实战示例二在 API 调用中使用秘密变量需要一个调用 PagerDuty 的工作流将PAGERDUTY_KEY保存为秘密Geheimnis全局变量。在API组件上把请求头Authorization设置为Token token{{global.variables.PAGERDUTY_KEY}}。这样密钥既不会出现在工作流中也不会出现在日志里。其安全保障来自运行引擎的**日志脱敏Redaction**机制运行结束后cleanLogs会调用getSecretWorkflowVariableValues收集所有秘密变量的内容然后对每条日志文本执行redactSecretsFromString把所有出现的秘密值替换为[REDACTED]RunWorkflow.ts、SecretRedaction.ts。脱敏不仅作用于纯文本日志还作用于结构化步骤跟踪step tracesredactSecretValues会递归地对对象/数组的键和值同时脱敏因为工作流变量可能被替换进 JSON 属性名例如 HTTP 请求头名称只清理值不清理键仍会泄露秘密RunWorkflow.ts。脱敏规则按秘密长度降序排列并合并为单个全局正则确保较短的秘密不会先被替换而暴露出较长秘密的尾部空值被排除因为空字符串存在于任何字符串中SecretRedaction.ts。实战示例三链式调用两个 API第一个调用返回的 ID 是第二个调用需要的参数API组件lookup-order使用组件值选择器把 Manual 触发器 JSON 中的 email 字段插入GET /orders?email...。API组件cancel-order请求POST /orders/{{local.components.lookup-order.returnValues.response-body.id}}/cancel。需要注意如果lookup-order执行失败则不会走Erfolg成功输出而是走Fehler错误输出。务必把错误输出连接到 E-mail 或 Slack 组件这样失败就不会被静默吞掉。这也反映了 API 组件的错误分支语义从 RunWorkflow.ts 的注释可以看到组件可以调用options.onError来报告失败并触发错误端口此时步骤跟踪中会记录错误状态与错误信息。从工作流内部更新变量API 模式一个常见需求是定时轮换凭据从第三方获取新 Token再写回变量让下一次执行使用新值。这可以通过一个调用 OneUptime API 的API组件完成。请求格式PUT /api/workflow-variable/variable-id请求头需要携带ApiKey。关键坑点在于要修改的字段必须包裹在一个data对象里{ data: { content: {{local.components.get-token.returnValues.response-body.access_token}} } }如果提交不带data外壳的扁平 body请求会被以400拒绝。只发送你真正想改的字段即可name和description可以完全不出现在 payload 中。权限要求该 API 需要Edit Workflow Variables编辑工作流变量权限。值得注意的是不需要读取权限——因为更新操作不会把记录读回来API 组件只做写入。两个务必注意的约束不要重命名正在被引用的变量。name是{{local.variables.NAME}}模板的一部分。一旦改名所有已存在的引用都会解析失败而未解析的引用会作为字面文本被原样传递详见下文容易踩的坑。变量可以被写入但永远无法通过 API 读回内容。content字段在数据模型中的列访问控制是只写的——ColumnAccessControl里content的read权限列表为空[]只有create和update有权限WorkflowVariable.ts。无论变量是否为秘密这一点都成立。这恰好让变量成为存放轮换 Token 的安全位置任何调用方都无法读取历史值。将其标记为秘密则进一步把值从执行日志和步骤跟踪中剔除。安全设计的两道防线从服务层实现看WorkflowVariableService.onBeforeUpdate 还额外提供了两道数据模型无法表达的防护秘密开关是棘轮ratchet变量可以被标记为秘密但不能取消秘密标记。原因是isSecret只决定日志是否脱敏而content本身通过 API 不可读——如果允许取消秘密标记那么可写但不可读的调用方就能先清除秘密标记、再触发一次运行、然后从日志中读出值。因此取消标记会被BadDataException拒绝并提示Delete the variable and create it againWorkflowVariableService.ts。重命名时的唯一性检查服务层会在更新前检查新名称是否与同作用域同一工作流或同一项目全局内的其他变量冲突并区分全局变量名冲突与工作流内变量名冲突两种报错信息WorkflowVariableService.ts。容易踩的坑Stolperfallen优先使用选择器字段务必使用编辑器中的选择器组件值选择器、变量选择器生成引用。它们会精确插入 Runner 期望的组件 ID、返回值 ID 和变量 ID让引用不依赖界面上的显示名称。变量名大小写敏感{{global.variables.MyKey}}与{{global.variables.mykey}}是两个不同的变量。大小写不一致的引用永远解析不到任何值。未解析的引用不会报错也不会被清空引用一个不存在的东西不是错误也不会得到空字符串——花括号会原样保留被当成字面文本直接传递。例如{{local.components.api-get-1.returnValues.body}}中如果步骤 ID 打错了这段文本会逐字出现在你的 Slack 消息、URL 或请求体中而这次运行仍然会报告Executed已执行。这一行为可以从运行引擎源码中得到精确印证VMAPI.replaceValueInPlace在 storageMap 中查找不到变量时会continue跳过替换VMAPI.ts把{{...}}原样留在字符串里与此同时logUnresolvedReferences会在执行日志中追加一行警告明确指出是哪个参数里的哪个引用没有被解析RunWorkflow.ts。因此运行日志中的警告行是排查这类问题的主要线索。Builder 无法校验变量是否存在Builder 能标记它无法映射的组件引用未知的步骤 ID、未知的返回值、错误的引用根并且是在你保存之前就标记。但它无法判断一个变量是否存在——因此变量被重命名后问题只会在运行日志中暴露以警告行的形式。花括号内的空格不会被去除{{ local.variables.NAME }}括号内带空格与{{local.variables.NAME}}是不同的查找永远不会解析成功。唯一的例外是{{#each}}块内部——那里的名称会被 trim。变量在定时调度中的特殊行为调度期即解析除运行期外变量还会在调度注册阶段被解析。在 QueueWorkflow.ts 中当工作流使用 cron 表达式调度时{{local.variables.schedule}}或{{global.variables.hours}}这类引用会在创建 BullMQ 可重复任务之前就被解析并校验——因为调度器无法等待运行期才发生的变量替换。如果引用的变量缺失调度表达式仍含未解析引用或解析结果不是合法的 cron 表达式注册会被拒绝并返回明确的错误信息且日志写入同样经过秘密值脱敏getSecretWorkflowVariableValues。这意味着用于调度表达式的变量必须事先存在且内容合法。小结OneUptime 工作流变量体系围绕一张WorkflowVariable表、一份 StorageMap 和一个统一的模板替换引擎展开全局变量workflowId为空跨工作流共享、局部变量与组件输出local.*仅存活于单次运行所有{{...}}引用在组件执行前由VMAPI.replaceValueInPlace统一解析秘密变量在日志与步骤跟踪落盘前被递归脱敏为[REDACTED]。掌握选择器生成引用、警惕未解析引用的静默字面量行为、遵循秘密值不可取消标记、内容只写不可读的安全约束你就能构建出既灵活又安全的自动化工作流。延伸阅读Workflow 组件完整参考 —— 每个组件会产出哪些返回字段的完整清单Workflow 执行与日志 —— 执行后查看每个变量在每个步骤中的实际解析值Workflow 配置与安全 —— 什么样的值可以安全地放进全局变量【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考