本文へスキップ

エンジニアリング

ShopifyのSidekickアプリ拡張を作る:ドキュメントが控えめに書いていること

著者 Jahangir Alam · 2026年7月4日 · 約10分で読めます

要約: Sidekickアプリ拡張は三つの層でできています。宣言(tools.json とアプリ全体の一行の要約)、ツールを登録してバックエンドを呼ぶ薄いランタイム、そして既存のAPIです。ドキュメントは形を説明していますが、運用上の現実を控えめに書いています。私たちが最も時間を奪われた二つは、Sidekickがツールへ振り分けるのはアプリ全体の extensions_summary が厳しい上限に収まっているときだけであること、そして結果が開けるようになるのはディープリンクが _meta の中ではなく第一級の url フィールドであるときだけ、というものです。この記事は、BtoB見積アプリであるQuotWayへこの連携を実装した経験から、最初に読んでおきたかった手引きとしてまとめたものです。

更新、2026年8月31日。 以下で述べているアクション拡張の制限は、その後解消されました。私たちはRFCを提出し、Shopifyは2026年8月に application/quote のインテント種別をプラットフォームへ追加しました。ですからSidekickの回答の中の見積は、今では開けます。その続きと、インテントの仕組みについて私たちが取り違えていたことは、Shopifyに新しいSidekickインテント種別を依頼したら、実装されましたにあります。この記事の残りは2026年7月に書かれたままにしてあります。

Sidekickアプリ拡張とは実際に何か

SidekickはShopifyの管理画面にあるAIアシスタントです。アプリ拡張により、Sidekickがマーチャントの質問、たとえば「どの見積が承認待ちですか」に答えるためにあなたのアプリを呼び出し、マーチャントがページを離れることなく結果をその場に表示できます。

二つの種類があり、この区別はコードを一行書く前に重要になります。

データ拡張 アクション拡張
ターゲット admin.app.tools.data admin.app.intent.link / admin.app.intent.render
役割 読み取り:検索、参照、要約 書き込み:マーチャントが確定する変更を用意する
インテントの語彙 なし - 自由な形式のツール 固定のShopifyの語彙(email、ticket、review など)
今日リリースできるか はい、どのアプリでも 対応するインテントの type が存在する場合のみ

私たちは15の読み取り専用ツール(見積の検索、パイプラインのKPI、承認待ち、会社の分析など)を持つデータ拡張をリリースしました。その時点ではアクション拡張はリリースしていませんでした。早めに理解しておく価値のある制限で(詳しくは後述します)、その後解消されたものです。

頭の中のモデル:三つの層

私たちが犯したほぼすべての間違いは、これらを混同したことから生まれました。分けて考えてください。

1. 宣言(Sidekickがあなたを呼ぶかどうかを判断するために読むもの)。 二つのファイルと、アプリ全体の一つの文字列です。

  • shopify.app.tomlextensions_summary を含む [sidekick] ブロックを置きます。これは自分のアプリが何に答えられるかを平易な言葉で説明したものです。これが振り分け役です。Sidekickはこれを読んで、ある質問がそもそも自分のアプリに属するかどうかを判断します。
  • tools.json が各ツールを宣言します。namedescriptioninputSchema です。この説明文が、Sidekickがどのツールを呼ぶかを選ぶ手がかりになります。
  • instructions.md は、結果の使い方についてSidekickに渡される案内です。
# shopify.app.toml
[sidekick]
extensions_summary = "QuotWay answers questions about B2B quotes: search and filter by status/buyer/company/amount, look up a quote's status and next step, list quotes pending approval, report pipeline analytics, and summarize B2B companies."
# extensions/sidekick-data/shopify.extension.toml
api_version = "2026-04"

[[extensions]]
name = "QuotWay quote tools"
handle = "quotway-sidekick-data"
type = "ui_extension"

  [[extensions.targeting]]
  module = "./src/index.js"
  target = "admin.app.tools.data"
  tools = "./tools.json"
  instructions = "./instructions.md"

2. ランタイム(サンドボックス化されたつなぎ)。 あなたの module はShopifyのサンドボックスの中で、画面を持たずに動きます。宣言された各ツールを登録し、Sidekickがそのどれかを呼んだときにバックエンドを取得して、応答をSidekickの結果の形に写します。ここは薄く保ってください。業務ロジックは置きません。

export default () => {
  shopify.tools.register("pending_approvals", async () => {
    const res = await fetch("/api/sidekick/quotes?status=awaiting_approval", {
      headers: { Accept: "application/json" },
    });
    const data = res.ok ? await res.json() : null;
    return { results: (data?.quotes ?? []).map(quoteLink) };
  });
};

3. バックエンド(アプリの実際のAPI)。 サンドボックスは同じドメイン上の自前のエンドポイントを fetch し、Shopifyがマーチャントのセッショントークンを付与するため、既存の認証、テナントの絞り込み、権限の確認がそのまま適用されます。私たちはアプリの他の部分と同じセッショントークン認証とRBACを再利用する、小さな /api/sidekick/* のルート群を追加しました。

export const loader = async ({ request }) =>
  withAdminShopContext(request, async ({ shopId, staff }) => {
    requirePermission(staff.role, "quote.view");
    // ...query, shape a plain DTO, return CORS-enabled JSON
  });

サンドボックスからの呼び出しはクロスオリジンなので、すべてのルートがCORSのプリフライト(OPTIONS → 204)に応答し、許可的なCORSヘッダーを返します。認証はCookieではなくBearerのセッショントークンに乗るため、ここではワイルドカードの Access-Control-Allow-Origin でも安全です。

結果の契約と、リンクを開けるようにする一つのフィールド

データツールはModel Context Protocolのリソースリンクを返します。

{
  "results": [
    {
      "type": "resource_link",
      "uri": "gid://application/quotway.quote/123",
      "name": "QW-1042 — Acme Co.",
      "mimeType": "application/vnd.quotway.quote",
      "_meta": { "status": "Proposal sent", "total": "USD 5,000.00" }
    }
  ]
}

ここからが、私たちが学んだ中で最も重要なことで、ドキュメントではほとんど一文しか割かれていません。_meta は表示のための要約データであり、遷移先ではありません。 私たちは最初、ディープリンクを _meta.url に入れました。カードは問題なく表示されましたが、クリックしても何も起きませんでした。Sidekickにはそのリソースを開く手段がなかったのです。uri が解決できない合成された gid://application/... であり、本当のURLは装飾として扱われるメタデータに埋もれていたからです。

修正は、結果に第一級の url フィールドを置き、埋め込みアプリには app: プロトコルを使うことです。

function quoteLink(q) {
  return {
    type: "resource_link",
    uri: `gid://application/quotway.quote/${q.id}`,
    name: `${q.reference} — ${q.buyerName}`,
    mimeType: "application/vnd.quotway.quote",
    url: `app://app/quotes/${q.id}`, // ← first-class, openable
    _meta: { status: q.statusLabel, total: q.total },
  };
}

Shopifyは app://<path> を埋め込みアプリのベースURLに対して解決し、アプリの中へ直接遷移します。ストアのハンドルもAPIキーも、絶対URLの組み立ても不要です。(絶対パスの admin.shopify.com/store/<store>/apps/<client_id>/<path> も機能しますが、app: のほうが単純でストアに依存しません。)url_meta の外に引き上げたことで、「見積QW-1007を見せて」に対して要約、その見積そのものをアプリで開くリンクの両方が返るようになりました。

落とし穴(丸一日を奪ったもの)

症状 原因 対処
Sidekickが一般的な知識で答え、バックエンドへの呼び出しがゼロ extensions_summary がデプロイの検証機の上限を超えて黙って捨てられ、[sidekick] ブロックが登録されない 要約を実際に適用される長さに収める(私たちの場合、本当の上限はドキュメントが示唆する「256トークン」ではなく500文字でした)。サーバーのログで /api/sidekick への到達を確認してください。到達ゼロは、バックエンドの不具合ではなく振り分けられていないことを意味します。
結果のカードは出るが、リンクが開かない ディープリンクが _meta にあるか、uri が解決できない gid://application/... である 第一級の url フィールドに app://<path>(または絶対URL)を置く
デプロイ直後に「壊れている」 Sidekickのツールの利用可否はセッション単位で、反映に遅れがある まったく新しいSidekickのチャットで試す。古いセッションから壊れていると判断しない
本番でバックエンドのツール呼び出しが404、またはCORSで失敗する 拡張はデプロイしたがバックエンドのルートはしていない(またはその逆) 両方を一緒にデプロイする。 拡張は shopify app deploy、バックエンドのルートはホスティング経由で。
ホスティングへのプッシュが黙ってデプロイされない (ホスティング固有)たとえば「署名済みコミットを必須にする」設定が、署名のないプッシュを取り消す コミットに署名し、バックエンドが拡張と一緒に確実に反映されるようにする
CLIのプレビューの挙動が想定と違う ツールは実際の管理画面を経由する CLIのプレビューではなく、管理画面のSidekickでテストする

このうち二つは詳しく述べる価値があります。

自分のログで振り分けを診断する。 Sidekickが「動かない」とき、最も速い手がかりは、そもそもバックエンドが呼ばれたかどうかです。私たちは本番のログを /api/sidekick で検索し、到達ゼロを確認しました。これによりCORS、認証、バックエンドの不具合が即座に除外され、振り分け役(extensions_summary)に目が向きました。バックエンドが無言であることは、質問がそもそもアプリへ振り分けられていないことを意味します。

説明文こそが製品です。 Sidekickの振り分けは、extensions_summary とツールの description に対する言語モデルの判断です。曖昧な説明は飛ばされます。マーチャントが実際に使う言葉(「見積」「RFQ」「承認待ち」「成約率」「パイプライン」)を盛り込みつつ、事実に留めてください。Shopifyのコンテンツポリシーはこれらをデプロイ時実行時に検査し、売り込み、競合との比較、そしてSidekickの振る舞いを誘導しようとする指示を禁じています。私たちはSidekickに何を言うべきかを指示していた instructions.md の一行を削除しなければなりませんでした。自分のデータを説明するのであって、アシスタントの台本を書くのではありません。

読み取りは今日リリースできる。書き込みは制限されている

これが、計画に織り込むべき戦略上の制限です。データ拡張(読み取り)は、今すぐどのアプリでも機能します。アクション拡張(書き込み、つまり作成、編集、送信、変換)は固定のShopifyのインテントの語彙に紐づき、shopify app deploy はそこに含まれないインテントの type を拒否します。これを書いた時点では application/quote の種別が存在しなかったため、「Sidekickからこの見積を開く」というネイティブのアクションはリリースできませんでした。(Shopifyは2026年8月にこの種別を追加しました。冒頭の更新をご覧ください。)

現実的な橋渡しは、開発者フォーラムでShopifyのスタッフにも確認されているとおり、上で述べた url フィールドです。Sidekickの中から操作することはできませんが、開けるディープリンクを渡すことで、マーチャントは操作すべきページにワンクリックで到達できます。同じ状況にいるなら、Shopifyのapp-intent-typesのRFCリポジトリに自分のインテント種別の提案を出しつつ、読み取りとディープリンクの体験を今リリースするのが正しい一手です。私たちは両方を行いました。

覚えておく価値のある制約

  • 応答の予算: 各ツールの応答を4,000トークン未満に保ち、1秒未満を目指してください。これらはチャットの一往復に答える読み取りツールです。オブジェクトグラフ全体ではなく、尋ねられたものを返します。
  • アプリごとの予算: ツール(とインテント)の数には上限があります。データベースのテーブルではなく、マーチャントの実際の質問を軸に設計してください。
  • 認証は無料で手に入る: サンドボックスはアプリのセッションを再利用するため、既存のテナントの絞り込みと権限の確認に寄りかかれます。読み取りツールは、それが映し出すページとまったく同じだけ厳格であるべきです。

妥当な最初の到達点

  1. データ拡張を一つ、実際の質問に対応する二つのツール(「Xを検索する」「Xを一件参照する」)。
  2. 長さの上限に収まる [sidekick] extensions_summary と、キーワードを含んだ事実に基づくツールの説明文。
  3. 既存の認証とCORSのプリフライトを再利用する二つの /api/* エンドポイントを fetch する、薄いサンドボックスのつなぎ。
  4. 各結果に第一級の url を置き、アプリ内の適切なページが開くようにすること。
  5. 拡張バックエンドを一緒にデプロイし、実際の管理画面の新しいチャットでテストすること。

本当に役立つ連携が、これで一日の作業です。その大半は、すでに持っているAPIの再利用です。難しいのはコードではなく、上に挙げた三つか四つの運用上の事実です。

さらに読む

関連記事

QuotWayがこれをあなたのストアでどう扱うかをご覧ください。

サイトの利用状況を把握するため、分析用Cookieを設定したいと考えています。必須ではありません。拒否してもサイトの動作は変わらず、選択はいつでも変更できます。変更はこちらから: プライバシーページ.