objectstack-automation について
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 Automation Protocol
このスキルを使う場面
- ビジュアルフロー(自動起動、画面、またはスケジュール)を構築する場合。
- ビジネスオブジェクトのステートマシンまたは承認プロセスが必要な場合。
- イベント駆動型トリガー(レコードの作成/更新/削除)を設定する場合。
- スケジュールされたオートメーション(日次レポート、データクリーンアップ)が必要な場合。
述語と条件には 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 はトリガー準備状況の lint にも含まれます |
api | インバウンド webhook です。開始ノードに secret が必要です(以下のインバウンド webhook トリガーを参照)。明示的な起動のみを行うフローには autolaunched を使用します |
フローノードの種類
フローは 20 種類の組み込みノードで構成されます(プラグインは、以下の approval のように registerNodeExecutor でノードを追加登録できます)。
制御フロー
| ノード | 用途 |
|---|---|
start | エントリーポイントです。各フローに必ず 1 つだけ存在します |
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(デフォルトは $error)に格納して config.catch を実行します。config.retry を指定すると、まずバックオフを挟んで try を再実行します。finally はありません。コンテナーの通常の出力エッジが後続処理になります |
map | 順次実行のマルチインスタンスです。loop と同じ形式の config.collection の各項目について、サブフローを 1 回呼び出します。各反復で一時停止できます(バッチ承認など) |
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 のみです — そのため、ここに認証情報を記載しないでください。形式に応じてルーティングします — ヘッダー認証情報の場合は auth.credentialRef を指定した宣言型コネクター(bearer / basic / api-key)、クエリ文字列のキーの場合は 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'だけでは何も起きません。**エッジは通常のままで、無条件の出力エッジはすべて SUCCESS 時にたどられるため、ノードが成功するとハンドラーが実行され、失敗しても実行されません (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のどちらでも、ステップは成功を報告しますが値は反映されず、除外されたことがステップの警告に記録されます。承認スタンプ、変換フラグ、SLA マーカー、集計値などのreadonlyフィールドを管理するフローは、信頼された書き込みチャネルであるrunAs: 'system'で実行する必要があります。os validate/os buildは、readonlyフィールドに書き込むrunAs:'user'のupdate_recordを失敗として扱うため、不整合は数日後に誤ったデータとして現れるのではなく、ビルド時に検出されます。(readonlyWhenフィールドも、レコードの状態に応じて同様に警告としてフラグが付けられます。)readonlyを削除して回避してはいけません。フィールドの編集保護が失われます。
フロー全体ではなく、書き込みを昇格させてください。
screenフローはrunAs: 'user'のままにします。 その中のあるステップでreadonlyフィールドへの書き込みが必要な場合は、そのステップを専用のrunAs: 'system'フローに移し、subflowノードから呼び出してください。フロー全体を昇格させると、ほかのすべての書き込みも気付かないうちに昇格してしまいます。
runAs: 'system'の一括処理では、対象組織を明示してください。 システムコンテキストにはトリガーとなったユーザーが存在しないため、クエリの範囲は自動的に絞られません。組織条件のないスキャンや集計は、すべてのテナントを対象に読み書きします。テナント列はプラットフォームによって注入されます。オブジェクトごとに再宣言せず、その列をフィルターしてください。
フックは
sudoではなくrunAsで権限を昇格させます。 オブジェクトフック(objectstack-data)は、独自のrunAs: 'system' | 'user' | 'inherit'を宣言します。デフォルトは'inherit'で、フックを発火させた書き込みのコンテキストです。これにより、インプロセスのhandlerとサンドボックス化されたbodyのどちらでも、そのフックのctx.apiデータ操作のみがスコープされます。トリガーでユーザーが解決されなかった'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、executor の各段階で拒否され、対応する 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に入るのは作成された RECORD であり、その 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 のみに設定します。
前提条件 — ノードが必要とする機能を宣言する
メタデータはトークンがなくても登録されますが、該当する機能は実行されません。うち 2 つは警告もなく機能しません。
requires トークン | 有効になる機能 | 不在の場合 |
|---|---|---|
automation | フローエンジンとノード実行エンジン | フローは実行されません |
triggers | record-* / スケジュール / api の開始バインディング | フローは登録されますが、実行されません |
job | cron の実行間隔と timeRelative の一括処理 | スケジュールされたフローは起動しません |
queue | インバウンド Webhook のコンシューマー | インバウンド POST は 503 を返します |
approvals | approval / approval_revise(plugin-approvals) | ノードタイプに対応する実行エンジンがありません |
messaging | inbox(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で再試行の重複を排除します。配信は少なくとも 1 回行われるため、フローが冪等になるように作成してください。 - キュー経由: エンドポイントは
202を返してキューに追加します。フローはコンシューマー上で実行され、リクエスト処理中には実行されません。 - JSON ボディは、トリガーレコード(
record.*/ フィールド名をそのまま指定)とparamsとしてフローに渡されます。
トリガータイプ(start-node 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 | 作成または更新 — 1つのフローで両方のイベントに対応 | 挿入と更新の両方のフック |
トリガーの設定 — start ノード上
バインド先は START NODE です。これはそのノードであり、フロー全体ではありません(フローには、FlowSchema が要求する label、type、nodes、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 を参照してください(1行の説明と、node_modules/@objectstack/spec/src/ 内の該当箇所へのポインターがあります)。フィールドの正確な形式を確認するには、必ずソースを Read してください。プロパティ名の記憶に頼らないでください。