要点: Shopify Flowの連携には三つの可動部があります。トリガー(アプリがFlowに送るイベント)、アクション(Flowがアプリを呼び出すステップ)、そしてテンプレート(出来合いのワークフロー)です。拡張機能の設定は素朴なTOMLですが、罠はプラットフォームの文書化されていない規則の中にあります。私たちが実際にデバッグの一周期を費やした二つは、トリガーのフィールドキーとアクションのフィールドキーで記法の慣習が逆であること、そしてトリガーで宣言したすべての項目が、すべてのペイロードで必須であることです。つまり「省略可能な参照フィールド」は矛盾であり、ゲストのイベントを静かに壊します。QuotWayで10個のトリガー、3個のアクション、6個のテンプレートを提供した経験から、その全体像を示します。
Flow連携とは何か(そして誰が使えるのか)
Shopify FlowはShopifyのノーコード自動化ビルダーです。アプリは三つの方法で接続します。
- トリガー(
flow_trigger) - アプリがイベント(「見積が送信された」)を送り、マーチャントはそれを起点にワークフローを組み立てます。 - アクション(
flow_action) - Flowのワークフローがアプリを呼び出して何かを行わせ(「見積の状況を更新する」)、マーチャントが設定した項目を渡します。 - テンプレート(
flow_template) - Flowのテンプレートギャラリーに現れる、あらかじめ作られたワークフロー。マーチャントは白紙ではなく動く例から始められます。
まず一つ、よくある誤解を潰しておきます。アプリが提供する拡張機能について、FlowはPlus専用ではありません。 公開されているApp Storeアプリのトリガーとアクションは、Plusに限らずどの有料Shopifyプランのマーチャントでも利用できます。(マーチャント自身が作るFlowのワークフローには別のプラン規則がありますが、それは拡張機能が利用可能かどうかとは別の話です。)私たちが区分しているのはパッケージ上の選択によるもので、トリガーはProfessional、アクションはEnterpriseです。プラットフォームがそれを強制しているわけではありません。
三つの構成要素
トリガー、アクション、テンプレートはそれぞれ独立した拡張機能のディレクトリで、shopify.extension.toml を持ちます。共通の「Flowアプリ」オブジェクトのようなものはなく、多数の小さな拡張機能から連携を組み立てます。
トリガー:アプリ → Flow
トリガーは名前と、ワークフローで使えるデータになる項目の集合を宣言します。フィールドキーは公開されたペイロードの契約です。
# extensions/flow-quote-submitted/shopify.extension.toml
[[extensions]]
type = "flow_trigger"
name = "Quote submitted"
handle = "quote-submitted"
[[settings.fields]]
type = "single_line_text_field"
key = "Quote id" # ← 単語頭大文字、スペース区切り
[[settings.fields]]
type = "number_decimal"
key = "Total amount" # マーチャントは「Total amount >= X」の条件を作ります
[[settings.fields]]
type = "single_line_text_field"
key = "Customer id" # ゲストの場合は空文字列 — 落とし穴 #2 を参照
イベントの送信は、トリガーのハンドルと、まさにその項目名をキーとするペイロードを添えて flowTriggerReceive ミューテーションを呼び出すことで行います。私たちは状態が遷移するたびに、小さな flow/emit サービスから送信しています(送信、提案の送付、承認、再見積、辞退、期限切れ、変換、そして承認関連の三つで、合計10個です)。
アクション:Flow → アプリ
アクションは、マーチャントが入力する項目と、ステップの実行時にFlowがPOSTする runtime_url を宣言します。私たちはすべてのアクションを一つのエンドポイントに送り、ハンドルで振り分けています。
# extensions/flow-update-quote-status/shopify.extension.toml
[[extensions]]
type = "flow_action"
name = "Update quote status"
handle = "update-quote-status"
runtime_url = "/api/flow/action" # すべてのアクションで一つのエンドポイント
validation_url = "/api/flow/validate" # 保存時の項目検証
schema = "./schema.graphql"
return_type_ref = "QuoteActionResult"
[[settings.fields]]
type = "single_line_text_field"
key = "quote_id" # ← スネークケース(トリガーとは逆!)
required = true
[[settings.fields]]
type = "single_line_text_field"
key = "target_status" # IN_REVIEW | DECLINED | EXPIRED
required = true
/api/flow/action のハンドラーはShopifyのHMACを検証し、アクションのハンドルで振り分け、action_run_id について冪等であり(Flowは再試行します)、return_type_ref に一致する型付きの結果を返します。私たちは三つのアクションを提供しました。状況の更新、通知の送信、担当者への割り当てです。いずれもルートではなく、サービスの入口でEnterpriseに区分しています。
テンプレート:動く出発点
flow_template の拡張機能は、.flow ファイル(ワークフローの定義)とロケールの文言をまとめたものです。マーチャントが自分で配線せずに「高額の見積が届いたらSlackに投稿する」を得られるようにします。私たちは六つ提供しました。注意点として、テンプレートはFlowチームの審査を経てからギャラリーに現れます(数営業日)。そのため、アプリの他の部分とは別の時計で進みます。
落とし穴(どれも傷跡です)
| 落とし穴 | 何が起きるか | 対処 |
|---|---|---|
| 記法が逆 | トリガーのフィールドキーは Title Case With Spaces(「Total amount」)、アクションのフィールドキーは snake_case(quote_id)です。取り違えると項目が結び付きません。 |
この分かれ方を覚えてください。トリガーは単語頭大文字、アクションはスネークケースです。 |
| トリガーの全項目が必須 | 宣言したすべての項目が、すべてのペイロードに存在しなければなりません。「省略可能な参照フィールド」は矛盾です。一度でも省く(たとえば顧客のないゲスト見積)と、そのイベント全体が拒否されます。 | 参照フィールドではなく、"" になりうる素のテキスト項目を使い、Flow側で「空でない」による分岐をマーチャントに任せてください。 |
| 設定画面は保存できない | ステップに対してアプリが提供する設定画面は読み取り専用で、intent.finish({properties}) は何もしません(Shopifyのスタッフに確認済み)。独自の選択UIを作っても、選択内容を保持できません。 |
標準の [[settings.fields]] と、保存時に不正な値を拒否する validation_url を使ってください。 |
| CLI定義のトリガーにライフサイクルのコールバックがない | TOMLで定義したトリガーには購読・購読解除のWebhookがないため、誰が待ち受けているか分かりません。購読の記録を条件に送信すると閉じる側に失敗し、誰にもイベントが届きません。 | 権利のあるすべてのストアに対して送信してください。待ち受けがなければ flowTriggerReceive は単に何もしません。 |
.flow ファイルは自己検証する |
テンプレートのファイルは自身のSHA-256のダイジェストを持ち、エディターからの書き出しです。手で編集すると拒否されます。 | .flow を手で編集しないでください。マーチャント固有の項目を空にしたまま、Flowのエディターから書き出し直します。preInstallNote はTOMLではなくロケールのJSONにあります。 |
| スキーマの変更には協調したリリースが必要 | トリガーの項目を変更すると、送信するペイロードはデプロイ済みのスキーマと一致していなければなりません。送信側を出さずに拡張機能をデプロイする(またはその逆)と、すべてのイベントが拒否されます。 | 同時にリリースしてください。shopify app deploy とバックエンドの反映を歩調を合わせて行います。 |
| 型に厳密 | number_decimal は文字列ではなくJSONの数値である必要があり、*_reference の項目はGIDではなく数値のレガシーIDを求めます。 |
送信の境界で型を変換し、参照フィールドより素の項目を優先してください(落とし穴 #2 を参照)。 |
最初の二つが、本番でのデバッグ時間を費やしたものです。「全項目必須」は特に厄介です。項目が欠けた一部のイベントだけが壊れるため、私たちの場合はゲスト見積(顧客IDなし)といくつかの境界的な遷移でしたが、構造的な問題だと気づくまでは断続的な不具合に見えます。
本番でのFlowのデバッグ
Flowの失敗は静かです。拒否されたペイロードはマーチャントには表示されません。境界の両側でログを取り、それを検索してください。
- 送信側: 成功時に
flow.emit.sentを、flowTriggerReceiveがuserErrorsを返したときにflow.emit.user_errorsを記録します(スキーマの不一致はここに現れます)。 - アクション側: 振り分けたハンドル、
action_run_id、結果を記録します。HMACの失敗と冪等性による短絡も、それぞれ一行残すべきです。
トリガーが「発火しない」ときは、まず送信のログを確認してください。エラーなしで sent が出ているならペイロードは受理されており、問題はマーチャントのワークフロー側です。user_errors が出ているなら、ペイロードがデプロイ済みのトリガーのスキーマと一致していません(ほぼ必ず落とし穴 #6 です)。
現実的な最初の目標
- 最も重要なイベントについてトリガーを一つ、ワークフローが実際に必要とする項目だけで作ります(項目が少ないほど「必須」の罠も減ります)。キーは単語頭大文字です。
- 既存の状態遷移のコードから、権利のあるすべてのストアに対して送信します。購読による絞り込みはしません。
- 単一の
runtime_urlにPOSTするアクションを一つ、ハンドルで振り分け、HMACを検証し、action_run_idについて冪等にし、設定画面ではなくvalidation_urlを使って作ります。 - 拡張機能とバックエンドを同時にデプロイし、開発ストアで実際のワークフローを使って確認します。
- テンプレートは最後に追加します。Flowチームの審査が入るため、トリガーとアクションの提供を遅らせないようにしてください。
トリガーとアクションはそれぞれ小さなものです。連携の難しさは、ここまでに挙げたプラットフォームの規則に尽きます。記法と「全項目必須」の契約を初日に正しく押さえておけば、残りはすでに送信しているイベントを配線するだけです。
参考資料
- Shopify:アプリ開発者向けFlow、トリガー、アクション、テンプレート
- 姉妹記事:Shopify Sidekickのアプリ拡張を作る
関連記事
QuotWayがこれをあなたのストアでどう扱うかをご覧ください。