mcpField
O
objectstack-automation

objectstack-automation

github.com/objectstack-ai开发76230 次安装今天更新
代码仓库

关于 objectstack-automation

ObjectStack Automation Protocol 的 skill,供构建 ObjectStack 自动化流程的开发者参考,涵盖可视化流程、触发器、调度和审批等设计。内容还说明 CEL 条件表达式、节点行为及常见配置陷阱。

主要功能

  1. 01设计自动启动、屏幕和定时流程
  2. 02配置记录变更与 webhook 触发器
  3. 03使用分支、循环和并行节点
  4. 04通过记录节点执行数据操作
  5. 05设置故障路由与人工审批

适用场景

  1. 01构建记录创建或更新后的自动处理
  2. 02编排带审批步骤的业务对象状态机
  3. 03安排定期报告或数据清理
  4. 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.json
evals/approvals/
test-revise-loop.md
references/
_index.md
examples-flows.md
state-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 捕获,但最好一开始就写对:

  1. 值槽位只能接受字面值或 CEL 封套,绝不能接受 {…} 标记。 在 assignments.*、fields.*、subflow / map 的 input.*、script 的 inputs.*、loop / map 的 collection、notify 的 recipients / templateData.* / sourceId、http 的 body 以及所有 CRUD filter 操作数中,字符串就是它所写出的字面文本,而 {…} 标记会被拒绝——在 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 }}。

  2. create_record 的 outputVariable 保存的是已创建的记录,而不是其 id。 请显式引用字段。 ❌ fields: { ref: { dialect: 'cel', source: 'newRec' } } → 写入整个记录对象 ✅ source: 'newRec.id'

  3. 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
    } });
    
  4. 条件是纯 CEL——可以裸调用的是标准库。 该表是 objectstack-formula 的 CEL_STDLIB_FUNCTIONS,即可裸调用的公开子集;接收者方法(在值上调用,不能裸调用)不在其中。未知函数(PRIOR()、拼写错误的名称)和用 {…} 包裹的字段引用都会导致构建失败:花括号表示模板,不是 CEL——请写 record.x,不要写 {record.x}。

  5. 缺少 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流引擎和节点执行器流永远不会执行
triggersrecord-* / schedule / api 启动绑定流已注册,但永远不会触发
jobcron 频率和 timeRelative 扫描定时流永远不会启动
queue入站 Webhook 消费者入站 POST 返回 503
approvalsapproval / approval_revise(plugin-approvals)没有对应节点类型的执行器
messaging向收件箱(sys_inbox_message)发送 notify**无提示:**成功,但 skipped: true
webhooksdefineStack({ 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 键用途
hookIdURL 路径令牌(默认值为 'default')。轮换它以撤销已泄露的端点
secretHMAC-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源文件;不要凭记忆猜测属性名称。

评论

支持 Markdown