关于 objectstack-automation
ObjectStack Automation Protocol 的 skill,供构建 ObjectStack 自动化流程的开发者参考,涵盖可视化流程、触发器、调度和审批等设计。内容还说明 CEL 条件表达式、节点行为及常见配置陷阱。
主要功能
- 01设计自动启动、屏幕和定时流程
- 02配置记录变更与 webhook 触发器
- 03使用分支、循环和并行节点
- 04通过记录节点执行数据操作
- 05设置故障路由与人工审批
适用场景
- 01构建记录创建或更新后的自动处理
- 02编排带审批步骤的业务对象状态机
- 03安排定期报告或数据清理
- 04调用外部 HTTP API 或集成连接器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ objectstack-ai/ objectstack --skill objectstack-automation安装后在 Claude Code 中输入/objectstack-automation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/flows-triggers-approvals.jsonevals/approvals/test-revise-loop.mdreferences/_index.mdexamples-flows.mdstate-machines-and-approvals.md详细介绍
自动化设计 — ObjectStack 自动化协议
何时使用此技能
- 你正在构建可视化流程(自动启动、屏幕交互或定时运行)。
- 你需要为业务对象设置状态机或审批流程。
- 你正在设置事件驱动触发器(记录创建/更新/删除)。
- 你需要设置定时自动化(每日报告、数据清理)。
谓词和条件使用 CEL——此处每个
condition/guard/entryCondition/ 过滤器value都是由@objectstack/formula求值的 Expression 封装。 槽位接受纯 CEL 字符串;P\...`/cel`...`` 标签则会对同一字符串进行编写时验证。 两种形式都能解析——每个文件选用其中一种(示例应用使用纯字符串)。CEL 规范、标准库以及旧版语法 → CEL 对照表,请参阅 objectstack-formula。
流程 — 可视化逻辑编排
流程是一种由节点组成的有向图,是自动化的主要构建单元。
流程类型
| 类型 | 适用场景 |
|---|---|
autolaunched | 无需用户交互即可运行——由事件、API 或其他流程触发 |
screen | 交互式——向用户展示 UI 屏幕(向导、表单) |
schedule | 按 开始节点的 config.schedule 中声明的 cron/间隔频率运行(每日清理、每周报告)——也可通过 config.timeRelative 执行逐记录日期扫描,参见 相对时间触发器 |
record_change | 在记录创建/更新/删除时自动触发(通过 start 节点的 triggerType 绑定)。autolaunched 使用相同的 record-* 绑定时行为完全相同——引擎在两种情况下都会读取开始节点;record_change 还会启用触发器就绪性检查 |
api | 入站 webhook——需要在开始节点设置 secret(见下文的 入站 webhook 触发器);仅由显式调用启动的流程始终属于 autolaunched |
流程节点类型
流程由 20 种内置节点类型构成(插件可通过
registerNodeExecutor 注册更多类型,例如下文的 approval):
控制流
| 节点 | 用途 |
|---|---|
start | 入口点——每个流程必须且只能有一个 |
end | 出口点——可以有多个(提前退出、错误退出) |
decision | 条件分支——由边上的 condition 谓词路由,而非节点配置(见下文的审批示例) |
loop | 针对 config.collection 中的每个项,将嵌套的 config.body 区域迭代一次——config.collection 是 CEL 值封装({ dialect: 'cel', source: 'rows' })或内联列表,绝不能是字符串;iteratorVariable(默认为 item)和可选的 indexVariable 会在其中绑定,maxIterations 用于限制迭代次数 |
parallel | 并发执行 config.branches[] 中的分支(≥ 2 个区域),并在块结束时隐式汇合——无需担心拆分/汇合节点配对错误 |
try_catch | 运行 config.try;失败时使用 errorVariable 中的错误运行 config.catch(默认为 $error);config.retry 会先通过退避策略重新运行 try。没有 finally——容器的常规出边负责后续流程 |
map | 顺序多实例——针对 config.collection 中的每个项调用一次子流程(形状与 loop 相同);每次迭代都可以暂停(批量审批) |
wait | 暂停执行,直到计时器到期或收到命名信号 |
subflow | 调用另一个流程(可复用的组合) |
parallel_gateway / join_gateway / boundary_event | 不面向作者——BPMN 互操作形式,由映射器将 parallel / try_catch 容器降级为这些形式(automation/control-flow.zod.ts)。请编写容器 |
数据操作
| 节点 | 用途 |
|---|---|
assignment | 设置变量值 |
create_record | 插入新记录 |
update_record | 修改现有记录 |
delete_record | 移除记录 |
get_record | 使用筛选条件获取记录——没有 query_record 节点(该名称没有执行器,会导致抛出错误) |
外部集成
| 节点 | 用途 |
|---|---|
http | 调用外部 HTTP API — 始终设置 timeoutMs(未设置 = 无超时)。url 和 headers 会按原样随定义提供给每位可以读取工作流的成员 — 只有 signingSecret 和开始节点的 secret 会被隐藏 — 因此绝不要在其中放置凭据。按形式选择路由 — 标头凭据 → 声明式连接器(bearer / basic / api-key),并设置 auth.credentialRef;查询字符串密钥 → 使用 paramName 的 api-key;路径中含密钥的 webhook url → 使用令牌认证的连接器,例如 slack — 通过 connector_action 调用 |
notify | 通过消息服务发送通知(默认使用收件箱频道) |
connector_action | 调用预构建的集成连接器 |
script | 调用由 config.function 指定名称的已注册函数(参见 有效但会悄悄出错 #3) |
screen | 向用户显示 UI 表单(仅限屏幕工作流) |
人工决策
| 节点 | 用途 |
|---|---|
approval | 将记录提交人工签核 — 决策前会暂停运行,之后沿 approve / reject 分支继续(由 plugin-approvals 提供) |
notify — 使用最多的节点类型
NotifyConfigSchema(automation/io-node-config.zod.ts)是 strictObject — 未声明的键会导致带有键名的解析错误。RAW 键会按原样读取:其中的 {token} 会被拒绝。
{ id: 'tell_owner', type: 'notify', label: 'Notify Owner', config: {
recipients: { dialect: 'cel', source: 'record.assignee' }, // REQUIRED — id, list, or envelope; a CSV is ONE id
title: 'Done: {{ record.title }}', // inline; XOR `template` (RAW, localizable)
message: 'Closed {{ record.closed_at | date:long }}', // body; only with inline `title`
topic: 'task', // RAW; default 'notify'
severity: 'warning', // RAW; CLOSED enum info|warning|critical
channels: ['inbox'], // RAW; default inbox
sourceObject: 'task', // click-through: a PAIR, else dropped
sourceId: { dialect: 'cel', source: 'record.id' }, // (a value slot)
actionUrl: '/tasks/{{ record.id }}', // text slot; overrides the synthesized link
} }
流变量
每个工作流都定义输入/输出变量。variables 是一个包含
{ name, type, isInput, isOutput } 条目的数组 — 不是以名称为键的映射,变量也没有
label 属性:
variables: [
{
name: 'case_id',
type: 'text',
isInput: true, // passed in when flow is invoked
isOutput: false,
},
{
name: 'approval_result',
type: 'boolean',
isInput: false,
isOutput: true, // returned when flow completes
},
],
→ 已移至 references/examples-flows.md § 工作流示例 — 自动升级逾期案例。
失败路由与 runAs
处理失败节点:使用
fault边。{ source, target, type: 'fault' }会将失败节点路由到处理器,而不是结束运行。**只有type: 'fault'才能执行路由——单独设置label: 'error'不会产生任何效果:**这条边仍是 普通边,且每条无条件出边都会在成功时遍历,因此处理器会在节点成功时运行, 节点失败时却不会运行(objectstack validate会报告flow-error-label-not-fault)。 已处理的失败不会消耗流程级别的errorHandling.retry,后者会从头重新执行流程—— 如果失败仅局限于本地,优先使用 fault 边。处理器可读取{{ <nodeId>.error }}(或运行级别的{{ $error.message }};在 CEL 中为vars["$error"])。随后运行 会报告成功,失败步骤仍会保留在追踪记录中。
| 路由(运行时失败) | 不会路由(无论如何都会致命失败) |
|---|---|
| 404、速率限制、写入被拒、子流程失败 | 缺少必需配置键(objectName、url、flowName、connectorId/actionId);求值结果为 null 的筛选条件操作数;超出嵌套上限的图;未限定范围的运行 |
要写入
readonly字段?请设置runAs: 'system'。readonly: true管控的是终端用户界面:在默认的runAs: 'user'下,引擎会从任何非系统写入中 剥离readonly字段——无论是create_record还是update_record都一样——步骤会报告成功,但值不会写入,且丢弃操作会记录在步骤 的警告中。维护readonly字段(审批时间戳、转换标志、SLA 标记、汇总值)的流程 必须使用runAs: 'system',即可信写入通道。os validate/os build会使写入readonly字段的runAs:'user'update_record验证失败,因此不匹配的问题会在构建时暴露,而不是 几天后才以错误数据的形式出现。(readonlyWhen字段也有相同情况,具体取决于 记录状态——会标记为警告。)不要通过移除readonly来规避此问题; 这样会失去该字段的编辑保护。
提升写入权限,而不是提升整个流程。
screen流程应保持runAs: 'user'。 如果其中某个步骤必须写入readonly字段,请将该步骤移入专用的runAs: 'system'流程,并通过subflow节点调用——提升整个流程会悄无声息地 提升其中所有其他写入操作的权限。
runAs: 'system'的全量扫描必须指定组织范围。 系统上下文没有触发用户, 因此查询不会被自动限制范围:没有组织条件的扫描或汇总会读取并写入所有租户的数据。 租户列由平台注入——应对其进行筛选,切勿在每个对象中重复声明。
钩子应通过自身的
runAs提升权限,绝不能使用sudo。 对象钩子 (objectstack-data)需自行声明runAs: 'system' | 'user' | 'inherit'— 默认值为'inherit',即触发该钩子的写入操作所用的上下文 — 仅限定该钩子的ctx.api数据操作,进程内的handler和沙盒化的body均适用。若触发器解析不到 用户,'user'钩子便没有可限定范围的用户:其ctx.api数据操作会被拒绝 (HOOK_UNSCOPED_DATA_ACCESS,403),而不会在未限定范围的情况下运行 — 需要提升权限时请声明runAs: 'system'。sudo不是钩子配置键。
筛选操作数(config.filter)
- 每个操作数都是一个值槽位——可以是字面值,也可以是 CEL 值封套,在查询前求值(
filter: { id: { dialect: 'cel', source: 'record.id' } })。完整字符串形式的查询占位符({current_user_id}、{today}、{current_year_start})会原样传递,由查询引擎展开;其他所有{…}标记都会连同其封套一起被拒绝(expression-invalid),且流程变量不会覆盖占位符。只有filter会进行这种移交——在文本槽位中,{{ current_year_start }}是无效引用。 - 求值结果为
null的操作数会拒绝该节点(不可通过fault路由):丢弃条件不会缩小查询范围,反而会扩大范围,因此路由后的delete_record会清空对象,同时运行报告成功。如果值可能缺失,请用decision绕开该节点;要匹配空字段,请写字面值null。关系跳转(record.owner.email)需要起始节点的config.expand列出该字段,否则错误会指出缺失的跳转,而不是解决方法。
有效但会悄然出错(构建通过,运行时失败)
合法的元数据,但作者(尤其是 AI)常会写错;大多数问题会被
objectstack build 捕获,但最好一开始就写对:
-
值槽位只能接受字面值或 CEL 封套,绝不能接受
{…}标记。 在assignments.*、fields.*、subflow/map的input.*、script的inputs.*、loop/map的collection、notify 的recipients/templateData.*/sourceId、http的body以及所有 CRUDfilter操作数中,字符串就是它所写出的字面文本,而{…}标记会被拒绝——在objectstack validate(expression-invalid)、registerFlow和执行器中都会如此, 并指出对应的 CEL 写法。用{ dialect: 'cel', source: '…' }进行计算:'{record.owner}'→source: 'has(record.owner) ? record.owner : null'— CEL 遇到模板中原本不输出任何内容的缺失键时会拒绝,因此应输出什么:空值、null还是默认值,现在由你决定;'{record.tags.0}'→'record.tags[0]'; 深层路径需逐级防护:has(vars.source) && has(vars.source.id) ? vars.source.id : null- 金额:
source: 'round(x * 100) / 100.0'— CEL 将两个整数相除时会进行整数除法, 因此/ 100会把123.46变成123 - 日期:
{TODAY()}→isoDate(today()),{TODAY() ± N}→isoDate(daysFromNow(N))/isoDate(daysAgo(N)),{NOW()}→isoDatetime(now()),{NOW() ± N}→isoDatetime(addDays(now(), N)) - 运行用户:
{$User.Id}→current_user.id;没有用户也能运行的流程需要进行保护:current_user != null ? current_user.id : null。current_user仅包含id、positions、organizationId、isPlatformAdmin——邮箱或姓名需通过该 id 查询sys_user
文本槽位(notify 的
title/message/actionUrl、screen 的title/description、end 的message、http的url/headers)会渲染{{ }}插值——变量路径,可选格式化器,不含逻辑;其中的{…}标记会被拒绝,并指出对应的{{ }}写法。日期或运行用户没有可用的插值: 先赋值(assignments: { v: { dialect: 'cel', source: 'current_user.id' } }),再写{{ v }}。 -
create_record的outputVariable保存的是已创建的记录,而不是其 id。 请显式引用字段。 ❌fields: { ref: { dialect: 'cel', source: 'newRec' } }→ 写入整个记录对象 ✅source: 'newRec.id' -
script节点只会调用已注册的函数。 将config.function设为通过defineStack({ functions: { my_fn: (ctx) => … } })注册的函数。该项为必填:空的script节点会在执行时被拒绝,而指向未注册函数的节点会明确报错。没有其他分发形式:投递请使用
notify,Slack 请使用slack连接器上的connector_action,逻辑请使用函数。 流function是一个纯计算步骤——它不会读写数据库。 它接收ctx.input并返回一个值;config.outputVariable会将该值公开为流变量,后续的声明式节点会持久化它。将数据操作保留在流图中(清晰可见、受治理、可在构建时检查):// ✅ function returns values → outputVariable → update_record persists { id: 'ai', type: 'script', config: { function: 'helpdesk.aiTriageStub', // returns { ai_category, ai_sentiment, … } inputs: { ticketId: { dialect: 'cel', source: 'record.id' } }, // CEL values or literals outputVariable: 'ai', } }, { id: 'apply', type: 'update_record', config: { objectName: 'helpdesk_ticket', filter: { id: { dialect: 'cel', source: 'record.id' } }, fields: { ai_category: { dialect: 'cel', source: 'ai.ai_category' } }, } },如果确实需要数据生命周期的副作用(读取或写入其他记录),那属于 L2 钩子(objectstack-data)——钩子可使用
ctx.api;流函数则不能。如果函数写入平台无法看到的内容,就声明这一点,因此运行报告会显示“无法确定”,而不是
acted: 0:defineStack({ functions: { 'helpdesk.aiTriageStub': (ctx) => ({ ai_category: 'other' }), // pure — the default 'billing.sync': { handler: syncBilling, effect: 'writes' }, // declared writer } }); -
条件是纯 CEL——可以裸调用的是标准库。 该表是 objectstack-formula 的
CEL_STDLIB_FUNCTIONS,即可裸调用的公开子集;接收者方法(在值上调用,不能裸调用)不在其中。未知函数(PRIOR()、拼写错误的名称)和用{…}包裹的字段引用都会导致构建失败:花括号表示模板,不是 CEL——请写record.x,不要写{record.x}。 -
缺少
messaging能力时,notify仍会报告成功。 执行器会记录no messaging service registered,并返回成功,结果为output: { delivered: 0, failed: 0, skipped: true }和metrics.acted: 0——运行显示为绿色,但实际上没有发送任何内容。请在requires中声明messaging。
→ 状态机与审批已移至 references/state-machines-and-approvals.md——其状态内省路由为 GET /api/v1/meta/object/:name/state/:field?from=:state。
触发器——事件驱动自动化
record_change 流会在数据事件发生时自动触发。不存在独立的触发器对象,也没有顶层 trigger / event 键——绑定完全位于流的**start 节点 config** 中。
前提条件——声明节点所需的能力
即使没有令牌,元数据也会注册;只是对应功能不会运行——其中两项甚至不会给出提示:
requires 令牌 | 启用的功能 | 缺少时 ⇒ |
|---|---|---|
automation | 流引擎和节点执行器 | 流永远不会执行 |
triggers | record-* / schedule / api 启动绑定 | 流已注册,但永远不会触发 |
job | cron 频率和 timeRelative 扫描 | 定时流永远不会启动 |
queue | 入站 Webhook 消费者 | 入站 POST 返回 503 |
approvals | approval / approval_revise(plugin-approvals) | 没有对应节点类型的执行器 |
messaging | 向收件箱(sys_inbox_message)发送 notify | **无提示:**成功,但 skipped: true |
webhooks | defineStack({ webhooks }) 出站注册 | 不会发送任何出站内容 |
defineStack({
// …
requires: ['automation', 'triggers', 'job', 'queue', 'approvals', 'messaging'],
});
入站 Webhook(api)触发器(ADR-0041 Tier 1)
api 流绑定到一个入站 HTTP 端点:
POST /api/v1/automation/hooks/:flowName/:hookId。请在启动节点的 config 中配置(启动节点的 config 是一个没有 Zod 形状限制的自由格式记录):
config 键 | 用途 |
|---|---|
hookId | URL 路径令牌(默认值为 'default')。轮换它以撤销已泄露的端点 |
secret | HMAC-SHA256 共享密钥。必填——若未提供非空密钥,注册会被拒绝(os validate 也会拒绝),启动时也不会启用;签名放在 x-objectstack-signature 中 |
- 签名:
t=<unix seconds>,v1=<hex>——v1是在secret下对<t>.<raw body>进行 HMAC-SHA256 计算所得;若t与服务器时钟相差超过 300 秒,则会被拒绝。 - 幂等性:
x-idempotency-key用于去重重试——请将流设计为幂等(投递至少一次)。 - **由队列支持:**端点返回
202并将请求入队;流由消费者运行,不会在请求处理期间运行。 - JSON 正文会作为触发器记录(
record.*/ 裸字段)以及params提供给流。
触发器类型(起始节点 config.triggerType)
triggerType | 触发时机 | ObjectQL 钩子 |
|---|---|---|
record-before-create | 插入前(可修改/拒绝) | beforeInsert |
record-after-create | 插入后 | afterInsert |
record-before-update | 更新前 | beforeUpdate |
record-after-update | 更新后 | afterUpdate |
record-before-delete | 删除前 | beforeDelete |
record-after-delete | 删除后 | afterDelete |
record-before-write / record-after-write | 创建或更新——一个工作流,涵盖两个事件 | 插入和更新钩子 |
触发器配置——在 start 节点上
绑定对象是 START NODE——就是这个节点,而不是整个工作流(一个工作流还包含 label、type、nodes,以及 FlowSchema 要求的 edges):
{
id: 'start',
type: 'start',
label: 'On Case Escalated',
config: {
objectName: 'support_case',
triggerType: 'record-after-update',
// bare CEL; gates whether the flow launches on the event
condition: "previous.status != 'escalated' && record.status == 'escalated'",
},
}
优先使用
record-after-*,除非你必须修改或拒绝记录; 为会写入自身对象的触发器添加保护条件,否则会不断触发自身。抛出错误的record-before-*工作流会静默阻止写入——请提供面向用户的提示信息。
更新触发器条件中的
previous(更改前)与record(更改后):参见 objectstack-formula §5。
→ 已移至 references/examples-flows.md § 基于时间的触发器——按计划扫描每条记录的日期。
CRM 自动化蓝图
元数据应用的默认方法——首先通过 Flow/Approval 元数据对业务生命周期建模,仅在边缘场景集成中使用自定义代码——在此 CRM 风格结构中:
| 自动化类型 | 常见位置 | 模式 |
|---|---|---|
| 屏幕流 | src/flows/*.flow.ts | 使用显式的 variables、节点图(nodes + edges)和决策分支 |
| 审批流 | src/flows/*.flow.ts | 包含一个或多个 approval 节点的工作流;在节点 config 中设置 approvers / behavior / lockRecord / approvalStatusField,并基于 approve / reject 边进行分支 |
| 工作流注册表 | src/flows/index.ts | 导出 allFlows: Flow[],并在 defineStack({ flows }) 中集中注册 |
验证你的工作
裸字段引用(status == 'open')可以解析——引擎会将记录字段展平到作用域中——因此其中的拼写错误只会触发“你是不是想输入……”提示。
record.status 仍是规范写法,而对未知的 record.<field>,os validate 会报错:
os validate # CEL/predicate validation (record.<field> existence) + schema
# or: os build # the same gates, plus emits dist/
它会对每个条件、边守卫、验证规则和共享规则运行 ADR-0032 检查。在脚手架生成的项目中:npm run validate。
参考资料
发送回修改的结构有对应的分级评估,见 evals/approvals/test-revise-loop.md。
Zod 架构见 references/_index.md(包含单行说明,以及指向 node_modules/@objectstack/spec/src/ 的链接)。对于精确字段结构,请始终Read源文件;不要凭记忆猜测属性名称。