ObjectOS
构建自动化

审批流程

把记录路由给人签核 —— 同时不让自动化悄悄绕过行级安全。

**审批就是带审批节点的流程:流程暂停,直到有人批准或拒绝,然后沿匹配的分支继续。**没有需要另学的独立审批引擎 —— 触发器、分支、错误处理都与任何其他流程完全一致。本页新增的内容是审批节点本身,以及两个与路由同样重要的访问决策。

谁做什么

三个角色,刻意分离:

角色能做需要
构建者编写和编辑审批流程manage_metadata(通常是 Studio 用户)
提交人提交进入流程的记录正常的记录访问 —— 不需要任何自动化配置权限
审批人处理审批请求(批准 / 拒绝)由其权限集授予的能力

最终用户提交记录、处理请求;他们从不编辑自动化。把自动化配置界面挡在消费者应用之外。

以谁的身份运行 —— 安全决策

流程通过 runAs 声明运行身份,对审批而言,正是这个决策防止流程悄悄绕过行级安全:

runAs数据操作以谁运行何时使用
'user'(默认)提交人,遵守其 RLS流程只触碰提交人本来就能看到的记录
'system'提权 —— 绕过 RLS流程必须读写提交人看不到的记录(写入总账、通知一个拥有提交人不可见行的审批人)

显式声明提权,让它可见而非意外。默认的 'user' 意味着审批流程无法悄悄给提交人跨租户或跨所有者的触达 —— 提权是自愿开启且可审计的。

**提示:**由定时触发的升级流程没有触发用户 —— 它必须 runAs: 'system' 才能行动。这是提权的正当场景;"到处 system 好让它能跑"才是反模式。

审批节点

节点声明谁来审批以及多个决策如何聚合:

{
  id: 'manager_approval',
  type: 'approval',
  label: 'Manager Approval',
  config: {
    approvers: [{ type: 'field', value: 'owner_manager_id' }],
    behavior: 'unanimous',
    approvalStatusField: 'approval_status',
    lockRecord: true,
  },
}
配置作用
approvers谁必须决策 —— 具名用户、岗位,或从记录字段解析出的用户(如提交人的经理)
behavior多个审批人如何聚合:'first_response'(默认)、'unanimous''quorum''per_group'
minApprovals所需批准数 —— quorum 为总数(N 中取 M),per_group 为每组所需数(默认 1)
approvalStatusField可选的记录字段,插件把请求状态镜像进去
lockRecord请求待定期间锁定记录、禁止编辑

警告 —— position 与成员层级。{ type: 'position', value: 'finance_manager' } 路由给某个岗位的持有者(sys_user_position)。组织成员层级(sys_member.roleowner/admin/member)自 16.0 起写作 type: 'org_membership_level';旧写法 role保留一个版本的弃用别名 —— 仍能加载并按相同方式解析,但会给出警告,下个大版本移除。把岗位名写成成员层级类型谁也匹配不上,请求就会卡住。os lint 对两种情况都会标记(approval-approver-not-membership-tierapproval-approver-type-deprecated);如果值是岗位名,正确写法是 type: 'position'

法定人数与会签(16.0)

first_responseunanimous 之外,16.0 新增两种聚合模式。**Quorum(法定人数)**在达到 N 中取 M 个批准时定案:

config: {
  approvers: [
    { type: 'user', value: 'director_a' },
    { type: 'user', value: 'director_b' },
    { type: 'user', value: 'director_c' },
  ],
  behavior: 'quorum',
  minApprovals: 2, // 3 人中任意 2 人批准
}

**Per-group(会签)**要求每个带标签的组都签核 —— 用 group 给审批人打标签,只有当每个组都达到 minApprovals 个批准(默认每组 1 个)时节点才会前进:

config: {
  approvers: [
    { type: 'position', value: 'legal_counsel',   group: 'legal' },
    { type: 'position', value: 'finance_manager', group: 'finance' },
  ],
  behavior: 'per_group', // 法务一个签核,且财务一个签核
}

对所有模式都成立的语义:

  • 审批人集合按开启时快照统计,并带休假(OOO)替补 —— 谁必须响应在请求开启时就已确定,休假的审批人会被替补而不是卡住请求。
  • 单个拒绝仍然是一票否决:在任何模式下它都把节点定案为 rejected
  • 阈值在运行时**收拢(clamp)**到可解析的审批人数量,因此配置错误的 minApprovals 永远不会让请求死锁。
  • 没有 group 标签的审批人各自成组,所以普通的审批人列表在 per_group 下依然行为合理。

一个完整的审批流程

把大额提案路由给负责人的经理,再按决策分支:

export const opportunityApproval = defineFlow({
  name: 'opportunity_approval',
  label: 'Opportunity Approval',
  type: 'record_change',
  status: 'active',
  runAs: 'user', // 用提交人的 RLS,除非某一步确实需要更多权限
  nodes: [
    {
      id: 'start',
      type: 'start',
      config: {
        triggerType: 'record-after-update',
        objectName: 'opportunity',
        condition: "record.amount >= 50000 && record.stage == 'proposal'",
      },
    },
    {
      id: 'manager_approval',
      type: 'approval',
      label: 'Manager Approval',
      config: {
        approvers: [{ type: 'field', value: 'owner_manager_id' }],
        behavior: 'unanimous',
        approvalStatusField: 'approval_status',
        lockRecord: true,
      },
    },
    { id: 'mark_approved', type: 'update_record', label: 'Mark Approved' },
    { id: 'mark_rejected', type: 'update_record', label: 'Mark Rejected' },
    { id: 'end', type: 'end' },
  ],
  edges: [
    { id: 'e1', source: 'start', target: 'manager_approval' },
    { id: 'approved', source: 'manager_approval', target: 'mark_approved', label: 'approve' },
    { id: 'rejected', source: 'manager_approval', target: 'mark_rejected', label: 'reject' },
    { id: 'e4', source: 'mark_approved', target: 'end' },
    { id: 'e5', source: 'mark_rejected', target: 'end' },
  ],
});

注意带标签的边:approvereject 命名了决策之后的分支。永远要建模拒绝路径 —— 只有批准分支的流程会让被拒绝的记录搁浅。

**多级审批:**串联多个 approval 节点。**并行审批:**见流程中的聚合节点模式。

审批人体验到什么

@objectstack/plugin-approvals 包持有持久化的审批状态。请求待定期间:

  1. 插件持久化请求(sys_approval_request)以及针对它的每个决策(sys_approval_action)—— 你的审批历史是可查询的数据。
  2. 设置 lockRecord: true 时,记录被锁定、禁止编辑,直到决策落地。
  3. 如果设置了 approvalStatusField,记录自身的字段会镜像请求状态,视图和报表就能按它筛选。
  4. 审批人批准或拒绝;插件沿匹配的边恢复被暂停的流程。自 16.0 起,决策可以携带文件附件(持久化在 sys_approval_action.attachments,贯通决策/评论路由与客户端 SDK)—— 签署的 PDF 和证据材料与决策存放在一起。
  5. 请求暴露服务端计算的 decision_progress —— unanimous/quorum 为已获批准数 vs. 所需数,per_group 为按组明细 —— 收件箱和你自己的 UI 直接渲染进度,无需重新实现计票。

**决策即声明式操作(16.0)。**完整的决策集合 —— 批准、拒绝、转办、退回修改(/revise)、补充材料、催办、撤回、重新提交 —— 以 type: 'api' 操作的形式声明在 sys_approval_request 上,带类型化参数、仅待定时可见的门控,以及提交人 vs 审批人谓词(如 record.submitter_id == ctx.user.id)。Console 审批收件箱渲染的是这些声明的操作,而不是手写按钮,因此新的决策能力以元数据形式发布 —— 无需客户端发版。

批准本身也是被门控的操作。把"可以批准"建模为一个能力(如 approve_invoice),由审批人的权限集授予,并把批准操作的 requiredPermissions 建立在它之上 —— 这样门控就在 UI 和服务端两侧同时强制,而不只是从屏幕上藏起来。

最佳实践

应该不应该
定义清晰的进入条件设置过多的审批环节
设置合理的超时时间把审批做得过于复杂
在合适的场景允许撤回忘记拒绝路径
通知所有相关方把审批人写死
追踪审批历史只在 UI 层门控"批准"
默认 runAs: 'user',一步一步地提权到处设置 runAs: 'system' "好让它能跑"

下一步

页面原因
流程本页所基于的流程参考 —— 触发器、步骤、错误处理
工作流约束审批所处的生命周期
操作把提交和批准做成界面上的按钮
CEL 表达式进入条件背后的语言
Email通知审批人与相关方
自动化总览选择器:流程 vs 工作流 vs 审批

On this page