ObjectOS
构建自动化

流程与自动化

声明式业务逻辑 —— 无论是描述给 AI 还是用 TypeScript 编写,运行时执行的都是同一份产物。

Flow 是你不写服务端就能表达业务逻辑的方式。每个 Flow 都是声明式元数据,由运行时执行 —— 与对象、视图一样。这意味着 Flow 会同时出现在 os diff、审计日志、Console 的流程构建器以及 AI Builder 中。

多数客户通过对 AI 提问来创建 Flow:

"当一个高优先级工单在 'new' 状态停留 30 分钟时,在 Slack 上通知经理。"

AI 会生成下面的 Flow。本页描述其结构,以便你阅读和编辑。

在 stack 中启用该能力:

export default defineStack({
  // ...
  requires: ['automation'],
});

流程类型

类型触发方式用于
Autolaunched记录变更(insert/update/delete)"用户注册时发欢迎邮件"
ScheduledCron 表达式或间隔"每晚 2 点标记过期任务"
Time-relative (16.0)日期字段与今天的距离,经由每日扫描"合同到期前 60 天提醒我"
Manual用户在 Console 点按钮,或 API 调用"审批发票"等 Action

Autolaunched:对记录变更做出反应

// src/flows/welcome_email.ts
import { defineFlow } from '@objectstack/spec';

export const welcomeEmail = defineFlow({
  name: 'welcome_email',
  type: 'autolaunched',
  trigger: {
    object: 'sys_user',
    when: 'after_insert',
  },
  steps: [
    {
      type: 'action',
      action: 'send_email',
      inputs: {
        to:      '{!trigger.record.email}',
        subject: 'Welcome to {!org.name}',
        body:    'Hi {!trigger.record.name}, welcome aboard.',
      },
    },
  ],
});

变量插值:{!trigger.record.<field>}{!org.<field>}{!user.<field>}{!step.<step-name>.output}condition: 块中使用 CEL 表达式。

触发时机:

when何时触发
before_insert写事务内、INSERT 之前
after_insert提交之后
before_update写事务内、UPDATE 之前
after_update提交之后
before_delete写事务内、DELETE 之前
after_delete提交之后

before_* 流程可以修改正在写入的记录(计算字段、归一化数据)。after_* 流程异步运行,可以调用慢的外部服务。

Scheduled:按时钟运行

export const nightlyCleanup = defineFlow({
  name: 'nightly_cleanup',
  type: 'scheduled',
  schedule: { cron: '0 2 * * *', timezone: 'America/New_York' },
  steps: [
    {
      type: 'query',
      query: { object: 'task', filter: 'status:open AND due_lt:now()' },
      output: 'stale',
    },
    {
      type: 'foreach',
      items: '{!step.stale}',
      do: [
        { type: 'update', record: '{!item.id}', fields: { status: 'overdue' } },
      ],
    },
  ],
});

@objectstack/service-job 能力承载 —— 见 Runtime Capabilities

Time-relative:相对日期字段触发(16.0)

"合同到期前 60 天提醒我"以前只能写成一个用 record.end_date == daysFromNow(60) 做门控的记录变更流程 —— 这个谓词只有在记录恰好被修改时才会求值,无人照看时几乎永远不会触发。别这么写。改为声明时间相对触发器:流程的开始节点携带一个 config.timeRelative 描述符,运行时按计划扫描该对象,并为每条匹配记录各启动一次流程:

export const renewalReminder = defineFlow({
  name: 'contract_renewal_reminder',
  type: 'schedule',
  status: 'active',
  nodes: [
    {
      id: 'start',
      type: 'start',
      config: {
        timeRelative: {
          object: 'contracts',
          dateField: 'end_date',
          offsetDays: [60, 30, 7],       // T-minus thresholds
          filter: { status: 'active' },  // AND-ed with the date window
        },
        // Optional sweep cadence — defaults to daily at 08:00 UTC.
        schedule: { type: 'cron', expression: '0 8 * * *' },
      },
    },
    { id: 'notify_owner', type: 'notify', label: 'Notify Owner' },
    { id: 'end', type: 'end' },
  ],
  edges: [
    { id: 'e1', source: 'start', target: 'notify_owner' },
    { id: 'e2', source: 'notify_owner', target: 'end' },
  ],
});
声明什么
object扫描其记录的对象(机器名)
dateField相对今天求值的 date / datetime 字段(按天粒度)
offsetDays偏移模式 —— 当 dateField 恰好等于今天 + 所列各偏移时触发([60, 30, 7];负数 = 过去,如 [-1] 表示次日)
withinDays区间模式 —— dateField 落在距今天 N 天内的每一天都触发:正数 = 即将到来("即将过期"),负数 = 有界的逾期回看,0 = 今天到期
filter可选的 ObjectQL where 映射,与计算出的日期窗口做 AND(如 { status: 'active' }
maxRecords每次扫描启动流程的记录数上限(默认 1000;触发收拢时扫描会记日志)

offsetDayswithinDays 必须恰好设置一个。另外两种常见形态:

// "Expiring soon" — fires every day a document is within 30 days of expiry.
timeRelative: { object: 'hr_document', dateField: 'expires_on', withinDays: 30 }

// Overdue sweep — fires for POs up to 14 days past due (bounded lookback).
timeRelative: { object: 'purchase_order', dateField: 'due_date',
                withinDays: -14, filter: { status: 'open' } }

每次启动都会把匹配记录放到自动化上下文中,因此开始节点的 condition{record.<field>} 插值与记录变更流程完全一致。发现查询以 system 身份运行,并做按记录的故障隔离。os validate 新增就绪性检查 —— 当 timeRelative.object 指向 stack 未定义的对象、或自动触发的流程停留在 draft 状态时都会警告。

**真正的"当天"检查现在没问题了(#3183)。**16.0 中 record.due_date == today() 能匹配了 —— 引擎会对时间类 == / != 比较做强制转换,让日期字段与 today() 正确比较。真正的"今天到期"条件用它;而任何"某日期前/后 N 天"形态的需求,用 timeRelative

Manual:Action 与审批

export const approveInvoice = defineFlow({
  name: 'approve_invoice',
  type: 'manual',
  inputs: {
    invoice_id: { type: 'lookup', reference: 'invoice', required: true },
    note:       { type: 'textarea' },
  },
  steps: [
    {
      type: 'update',
      record: '{!inputs.invoice_id}',
      fields: { status: 'approved', approved_by: '{!user.id}' },
    },
  ],
});

在 Console 中作为 Invoice 视图上的按钮暴露,或通过 REST 调用:

curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice \
  -H 'Authorization: Bearer <token>' \
  -d '{"inputs": {"invoice_id": "inv_123", "note": "OK"}}'

步骤类型

步骤用途
query通过 ObjectQL 读取记录
create / update / delete写对象
action调用内置或插件注册的 Action(email、webhook、AI 调用……)
condition按 CEL 表达式分支
foreach遍历集合
parallel并发执行子步骤
wait暂停一段时长 / 至时间戳 / 至条件成立
subflow调用另一个 Flow
approval阻塞直至用户审批(需要 @objectstack/plugin-approvals)

条件与分支

{
  type: 'condition',
  when: 'trigger.record.amount > 10000',
  then: [
    { type: 'action', action: 'send_slack', inputs: { /* ... */ } },
  ],
  else: [
    { type: 'update', record: '{!trigger.record.id}', fields: { status: 'auto_approved' } },
  ],
}

错误处理

每个步骤接受:

{
  type: 'action',
  action: 'send_email',
  inputs: { /* ... */ },
  retry: { attempts: 3, backoffMs: 1000, multiplier: 2 },
  onError: 'continue' | 'fail' | 'rollback',
}

对于 autolaunched 的 before_* 流程,onError: 'fail'(默认)会终止原始写事务。对 after_* 流程,原始写已经提交,失败的流程运行会落到任务重试队列。

公式与表达式(CEL)

条件、动态字段值和过滤表达式都接受 CEL(Common Expression Language) —— 谷歌为安全表达式求值设计的语言:

'amount > 10000 && account.tier == "enterprise"'
'duration(now() - created_at) > duration("30d")'
'has(record.notes) && record.notes != ""'

CEL 是沙盒化的(无副作用、无 I/O)、服务端求值、并可在流程构建器中审计。

可视化构建器

Console 自带一个可视化流程构建器,它与声明式元数据来回往返 —— 非工程师可以编辑流程,序列化结果与你手写的 TypeScript 同形。

测试流程

os test --scenario "welcome email fires on signup"

限制与最佳实践

  • **保持 before-hook 简短。**它们会阻塞写事务。
  • **用 wait 代替长时运行的步骤。**休眠的流程会占用 worker;wait until 会把 worker 还回池里。
  • **用 parallel 跑独立步骤。**默认是顺序执行。
  • **幂等很重要。**重试可能让同一步骤跑两次;外部副作用应去重(用流程运行 id 作为键)。
  • **审计敏感的 Action。**改权限或删记录的流程自身也应记录到 sys_audit_log

下一步

On this page