---
title: "ShopifyのSidekickアプリ拡張を作る：ドキュメントが控えめに書いていること"
description: "Shopify Sidekickアプリ拡張の開発者向けガイド。三つの層、リソースリンクとディープリンクの契約、そして時間を奪った落とし穴を解説します。"
url: "https://www.quotway.com/ja/blog/building-shopify-sidekick-app-extensions"
type: "blog post"
category: "Engineering"
published: "2026-07-04"
updated: "2026-10-07"
verified: "2026-10-07"
shopify_api_version: "2026-10"
audience: "Sidekick拡張を作るShopifyアプリ開発者"
scope: "API 2026-10におけるSidekickアプリ拡張。三つの層、リソースリンクとディープリンクの契約、アプリごとの予算（ツール20、インテント5、4,000トークン、データ1秒・アクション3秒）、そして現時点のインテント語彙"
locale: "ja"
source: "QuotWay - B2B Quote & Negotiation App for Shopify"
---

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

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

> **更新、2026年8月31日。** 以下で述べているアクション拡張の制限は、その後解消されました。私たちはRFCを提出し、Shopifyは2026年8月に `application/quote` のインテント種別をプラットフォームへ追加しました。ですからSidekickの回答の中の見積は、今では開けます。その続きと、インテントの仕組みについて私たちが取り違えていたことは、[Shopifyに新しいSidekickインテント種別を依頼したら、実装されました](/blog/shopify-sidekick-intent-types)にあります。この記事の残りは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.toml` に `extensions_summary` を含む `[sidekick]` ブロックを置きます。これは*自分のアプリが何に答えられるか*を平易な言葉で説明したものです。これが**振り分け役**です。Sidekickはこれを読んで、ある質問がそもそも自分のアプリに属するかどうかを判断します。
- `tools.json` が各ツールを宣言します。`name`、`description`、`inputSchema` です。この説明文が、Sidekickが*どの*ツールを呼ぶかを選ぶ手がかりになります。
- `instructions.md` は、結果の使い方についてSidekickに渡される案内です。

```toml
# 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."
```

```toml
# 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の結果の形に写します。ここは薄く保ってください。業務ロジックは置きません。

```js
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/*` のルート群を追加しました。

```ts
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ヘッダーを返します。Shopifyのガイドは、サンドボックスのオリジン `https://extensions.shopifycdn.com` をCORSの許可リストに加えるよう求めています。ワイルドカードの `Access-Control-Allow-Origin` でも動作し、認証はCookieではなくBearerのセッショントークンに乗るため、ここでは安全です。

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

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

```json
{
  "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:` プロトコルを使うことです。

```js
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を見せて」に対して要約*と*、その見積そのものをアプリで開くリンクの両方が返るようになりました。

2026年10月7日に再確認したShopifyのデータ拡張のガイドは、現在、二つの別々の結果の形を記載しています。リソースリンク（`uri`、`name`、`mimeType`）は、その `mimeType` が登録済みのインテント種別と一致すると操作可能になります。埋め込みアプリのリンクは `url` だけです。上の組み合わせた結果は、この記事を書いた時点で私たちがリリースしたものです。`application/quote` が存在するいま、ドキュメントに記載された方法はインテントの経路です。2行の変更は[インテント種別の記事](/blog/shopify-sidekick-intent-types)にあります。

## 落とし穴（丸一日を奪ったもの）

| 症状 | 原因 | 対処 |
|---|---|---|
| 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はRFCを受けて2026年8月にこの種別を追加し、2026年10月7日時点でShopifyのアプリアクションのガイドに載っている種別は15件です。`application/*` が11件（ad、campaign、email、faq、loyalty-program、optimization-plan、quote、return、review、shipment、ticket）、`shopify/*` が4件（Customer、Discount、Order、Product）。`application/*` の種別は `create` と `edit` のみに対応し、`shopify/Customer`、`shopify/Order`、`shopify/Product` は `import` と `import+bulk` に対応します。2026年9月30日にShopify Functionで作られた割引アプリ向けに追加された `shopify/Discount` は、`create` と `edit` に対応します。

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

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

- **応答の予算：** 各ツールの応答を**4,000トークン**未満に保ってください。データ拡張は**1秒**以内、アクションは**3秒**以内に応答する必要があります。これらはチャットの一往復に答える読み取りツールです。オブジェクトグラフ全体ではなく、尋ねられたものを返します。
- **アプリごとの予算：ツールは全拡張あわせて20、インテントは5。** これが割り当ての全部で、先に尽きるのはツールです。充実したデータ拡張に、アクションを準備するツールをいくつか足すだけで10台後半に届きます。データベースのテーブルではなく、マーチャントの実際の質問を軸に設計し、インテントより先にツールの枠を見積もってください。
- **認証は無料で手に入る：** サンドボックスはアプリのセッションを再利用するため、既存のテナントの絞り込みと権限の確認に寄りかかれます。読み取りツールは、それが映し出すページとまったく同じだけ厳格であるべきです。

## 妥当な最初の到達点

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

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

開発者ではなくマーチャントの方へ。ここで説明した拡張は、QuotWayでは[BtoB見積のためのShopify Sidekick](/features/shopify-sidekick)として動いており、すべてのプランでご利用いただけます。詳しくは[料金](/pricing)をご覧ください。

## さらに読む

2026年10月7日にShopifyのページで再確認しました（上の予算は最初のページ、語彙はアプリアクションのガイドから）。

- Shopify：[Sidekickアプリ拡張](https://shopify.dev/docs/apps/build/sidekick)と[アプリのデータを表に出す](https://shopify.dev/docs/apps/build/sidekick/build-app-data)
- Shopify：[Sidekick向けのアプリアクションを構築する](https://shopify.dev/docs/apps/build/sidekick/build-app-actions)（対応するインテント種別と二つのインテントのターゲット）
- Shopify：[`app-intent-types`](https://github.com/Shopify/app-intent-types)のRFCリポジトリ（自分のアクションのインテント種別を提案する）
- マーチャント向けの対となる記事：[Shopify SidekickでBtoBの見積を管理する方法](/blog/shopify-sidekick-b2b-quotes)
