流程与自动化
声明式业务逻辑 —— 无论是描述给 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) | "用户注册时发欢迎邮件" |
| Scheduled | Cron 表达式或间隔 | "每晚 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;触发收拢时扫描会记日志) |
offsetDays 与 withinDays 必须恰好设置一个。另外两种常见形态:
// "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。
下一步
- Webhooks —— 出站通知,常由流程触发
- Email ——
send_emailAction 的传输层 - AI Service —— 用于 LLM 步骤的
ai_callAction - API Access —— 从外部系统调用手动流程
- @objectstack/service-automation —— 执行引擎源码