仕入価格が変更された際に、販売価格と比較価格を自動更新する

September 3, 2026

仕入先が、ある商品のバリエーションの原価を4.00から5.20に引き上げました。しかし、Shopifyでは何の反応もありません。小売価格は以前のまま変わらず、計画していた利益率は、数週間後の棚卸しで誰かが気づくまで、いつの間にか消えてしまいます。

Shopify Flow Shopifyには、1個あたりの単価が変更された際のトリガーが組み込まれていないため、この件については単独では対応できません。また、単価から価格を再計算するアクションもありません。

このガイドでは、その両方の課題を解決します。いずれかのバリエーションで商品単価が変更されると、フローが開始され、短い関数が新しい価格と比較対象価格をShopifyに書き戻します。設定には約10分かかりますが、その後は価格の変更があるたびに、半永久的に実行され続けます。

制作内容

  1. Workflow Trigger Extensions 原価の変動を検知し、そのバリエーション、SKU、および新旧両方の原価情報を含んだフローを開始します。
  2. Shopify Flow それらの値をJSONとして関数に渡します。
  3. Workflow Functions 新しい価格を計算し、Admin API を通じてその価格を書き戻す、数行の JavaScript を実行します。

価格設定ルールは、あなたが管理するコード内の1か所にのみ定義されているため、後で変更する場合でも、ワークフローを再構築する必要はなく、1行を編集するだけで済みます。

手順 1:コストトリガーを有効にする

Workflow Trigger Extensions で、**「トリガー」**ページを開き、「**在庫」**グループを見つけて、「商品バリエーションのコストが変更された」を有効にします。

このトリガーには「在庫データへのアクセス」権限が必要です。これは、アイテムごとの原価がバリアント自体ではなく、在庫アイテムに紐づいているためです。まだこの権限が付与されていない場合、トリガーを有効にすると、1つのダイアログで権限の付与が求められます。権限が付与されるまでは、トリガーは有効なままですが、「権限が必要です」というフラグが表示され、発動することはありません。

このトリガーがもたらすもの

起動すると、Flowは以下の機能を利用できるようになります:

フィールド
variantCostChange.variantId gid://shopify/ProductVariant/54589751394579
variantCostChange.variantTitle Medium / Black
variantCostChange.sku TSHIRT-M-BLK
variantCostChange.oldCost.amount 4.00
variantCostChange.newCost.amount 5.20
variantCostChange.newCost.currencyCode EUR

製品自体も製品参照として含まれているため、価格に手を加える前に、ベンダー、タイプ、タグ、またはコレクションに基づいてFlowの条件を追加することができます。

ステップ 2: 関数を作成する

Workflow Functions で、新しい関数を作成し、以下のコードを貼り付けてください。

保存する前に、**「Shopifyデータが必要」**をオンにしてください。これにより、ctx.shopify.graphql(...) がコードから利用可能になり、ストアのデータを読み取ったり変更したりできるようになります。その後、Workflow Functions で「権限」ページを開き、「製品の書き込み」を許可してください。これは、この関数が価格を変更するために必要なスコープです。

そのトグルがないと、ctx.shopifyオブジェクトは利用できず、最初のAdmin API呼び出しで関数が失敗します。

update-variant-price.jsjavascript
export default async function (input, ctx) {
  const toGid = (value, type) => {
    if (value === undefined || value === null) return null;
    const str = String(value).trim();
    if (!str) return null;
    if (str.startsWith('gid://')) return str;
    if (/^\d+$/.test(str)) return `gid://shopify/${type}/${str}`;
    return str;
  };

  try {
    const variantId = toGid(input.variantId || input.variant_id || input.id, 'ProductVariant');
    if (!variantId) throw new Error('Missing "variantId" in input.');

    const hasPrice = input.price !== undefined && input.price !== null && String(input.price).trim() !== '';
    const hasCompareAt = Object.prototype.hasOwnProperty.call(input, 'compareAtPrice') || Object.prototype.hasOwnProperty.call(input, 'compare_at_price');
    const rawCompareAt = input.compareAtPrice !== undefined ? input.compareAtPrice : input.compare_at_price;

    if (!hasPrice && !hasCompareAt) throw new Error('Nothing to update: provide "price" and/or "compareAtPrice".');

    const asMoney = (value) => {
      const num = Number(value);
      if (!Number.isFinite(num) || num < 0) throw new Error(`Invalid money value: ${JSON.stringify(value)}`);
      return num.toFixed(2);
    };

    let productId = toGid(input.productId || input.product_id, 'Product');

    if (!productId) {
      const readQuery = `
        query VariantForPriceUpdate($id: ID!) {
          productVariant(id: $id) { id product { id } }
        }`;
      const readRes = await ctx.shopify.graphql(readQuery, { id: variantId });
      if (readRes.errors) throw new Error('Failed to read variant: ' + JSON.stringify(readRes.errors));
      const variant = readRes.data && readRes.data.productVariant;
      if (!variant) throw new Error(`Variant not found: ${variantId}`);
      productId = variant.product && variant.product.id;
      if (!productId) throw new Error('Could not resolve the parent product id for the variant.');
    }

    const variantInput = { id: variantId };
    if (hasPrice) variantInput.price = asMoney(input.price);
    if (hasCompareAt) {
      variantInput.compareAtPrice = rawCompareAt === null || String(rawCompareAt).trim() === '' ? null : asMoney(rawCompareAt);
    }

    ctx.log('Updating variant', variantId, 'on product', productId, variantInput);

    const mutation = `
      mutation UpdateVariantPrice($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
        productVariantsBulkUpdate(productId: $productId, variants: $variants) {
          productVariants { id }
          userErrors { field message }
        }
      }`;

    const res = await ctx.shopify.graphql(mutation, { productId, variants: [variantInput] });
    if (res.errors) throw new Error('GraphQL error updating variant: ' + JSON.stringify(res.errors));

    const payload = res.data && res.data.productVariantsBulkUpdate;
    if (!payload) throw new Error('Unexpected empty response from productVariantsBulkUpdate.');
    if (payload.userErrors && payload.userErrors.length) {
      throw new Error('Variant update failed: ' + payload.userErrors.map(e => `${(e.field || []).join('.')}: ${e.message}`).join('; '));
    }

    return { success: true };
  } catch (err) {
    ctx.log('Error updating variant price:', err.message);
    return { success: false };
  }
}

このコードについて、いくつか注目すべき点があります:

  • 単純な数値IDまたは完全なGIDを受け付けます。toGidはどちらの形式も正規化するため、Flowからどの形式が返されても心配する必要はありません。
  • 必要な場合にのみ、親商品を検索します。productVariantsBulkUpdate には商品 ID が必要です。ID を渡さない場合、この関数がそれを取得します。入力として productId を渡すと、その往復通信を省略できます。
  • **これは、単なる転送エラーだけでなく、userErrorsもチェックします。**GraphQL呼び出しはHTTP 200を返す場合でも、書き込みを拒否することがあります。これが、価格の更新が成功したように見えても実際には何も変更されない、最も一般的な原因です。
  • compare-at のクリアは意図的なものです。compareAtPrice: null または空の文字列を渡すと、それがエラーとして扱われるのではなく、**compare-at** が削除されます。

ステップ3:配線を行う前に、関数の動作を確認する

テスト」タブでは、ご自身のストアの実際のバリアントIDを使用してください。管理画面の任意の商品URL、またはトリガーのイベント履歴から1つを選択してください。

送信が正常に届いたことを確認できるよう、自分でもわかるような価格を設定してください。

test-input.jsonjson
{
  "variantId": "gid://shopify/ProductVariant/54589751394579",
  "price": "24.99",
  "compareAtPrice": "39.99"
}

ステップ4:フローの配線を行う

Shopify Flow でワークフローを作成します:

  1. トリガー製品のバリエーションの価格が変更された
  2. 操作:「Workflow Functions」から_関数を実行し_、先ほど作成した関数を選択します

JSON入力フィールドに、これを貼り付けてください。Flowは実行されるたびに、Liquidを実際の値に置き換えます。

flow-input.jsonjson
{
  "variantId": "{{variantCostChange.variantId}}",
  "price": "{{variantCostChange.newCost.amount | times: 5}}",
  "compareAtPrice": "{{variantCostChange.newCost.amount | times: 7}}"
}

価格設定のルールは、これだけです。販売価格は原価の5倍、比較価格は原価の7倍とすることで、商品には常に一貫した割引率が反映されるようになります。

これらの乗数はあくまで一例であり、推奨値ではありません。times: 5 は、お使いのカテゴリで実際にサポートされている値に変更してください。Liquid が計算を処理するため、乗数の代わりに固定のマークアップを使用する場合は plus: 12、パーセンテージのマージンを使用する場合は times: 1.6 となります。

複数の通貨で販売する場合、newCost.amount の値はショップの通貨で表示される点にご注意ください。Markets では表示時に自動的に換算が行われるため、ユーザー側で換算を行う必要はありません。

エンドツーエンドで正常に動作することを確認する

  1. Shopifyの管理画面で任意の商品バリエーションを開き、「1個あたりの原価」を変更してください
  2. 保存
  3. 数秒以内に、価格と比較対象価格が自動的に更新されます

何も起こらない場合は、以下の順序で順を追って確認してください。各手順は、その前の手順を排除していく形になるためです:

確認 どこで
引き金は引かれたのか? Workflow Trigger Extensions のイベント履歴
Flowは動作しましたか? Shopify Flow アクティビティログ
その関数は実行されましたか?また、ログには何が記録されましたか? Workflow Functionsでの実行履歴

実行が失敗したと報告される最も一般的な原因は、スコープの欠落です。ctx.shopify は、「Shopify **データが必要」**がオンになっている場合にのみ利用可能であり、製品への書き込み権限が付与されて初めて書き込みが成功します。

次はどこへ行こうか

このパターンは、原価の変動が影響を与えるあらゆる場面で応用できます。関数本体を変更すれば、原価が10%以上上昇した際にその商品を審査対象としてマークしたり、Slackに新旧の利益率を投稿したり、レポート用にメタフィールドに変更内容を書き込んだりすることが可能です。

トリガーとは、Shopify では提供されない部分のことです。その後の処理は通常のコードです。

Related article権限とデータへのアクセス「在庫データへのアクセス権」によって可能になること、およびその権限の付与・取り消し方法について。Related articleトリガーの完全なリファレンスWorkflow Trigger Extensions 内のすべてのトリガーと、各トリガーが持つフィールド。

Workflow Functions ドキュメント