仕入価格が変更された際に、販売価格と比較価格を自動更新する
September 3, 2026
仕入先が、ある商品のバリエーションの原価を4.00から5.20に引き上げました。しかし、Shopifyでは何の反応もありません。小売価格は以前のまま変わらず、計画していた利益率は、数週間後の棚卸しで誰かが気づくまで、いつの間にか消えてしまいます。
Shopify Flow Shopifyには、1個あたりの単価が変更された際のトリガーが組み込まれていないため、この件については単独では対応できません。また、単価から価格を再計算するアクションもありません。
このガイドでは、その両方の課題を解決します。いずれかのバリエーションで商品単価が変更されると、フローが開始され、短い関数が新しい価格と比較対象価格をShopifyに書き戻します。設定には約10分かかりますが、その後は価格の変更があるたびに、半永久的に実行され続けます。
制作内容
- Workflow Trigger Extensions 原価の変動を検知し、そのバリエーション、SKU、および新旧両方の原価情報を含んだフローを開始します。
- Shopify Flow それらの値をJSONとして関数に渡します。
- 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呼び出しで関数が失敗します。
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つを選択してください。
送信が正常に届いたことを確認できるよう、自分でもわかるような価格を設定してください。
{
"variantId": "gid://shopify/ProductVariant/54589751394579",
"price": "24.99",
"compareAtPrice": "39.99"
}ステップ4:フローの配線を行う
Shopify Flow でワークフローを作成します:
- トリガー:製品のバリエーションの価格が変更された
- 操作:「Workflow Functions」から_関数を実行し_、先ほど作成した関数を選択します
JSON入力フィールドに、これを貼り付けてください。Flowは実行されるたびに、Liquidを実際の値に置き換えます。
{
"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 では表示時に自動的に換算が行われるため、ユーザー側で換算を行う必要はありません。
エンドツーエンドで正常に動作することを確認する
- Shopifyの管理画面で任意の商品バリエーションを開き、「1個あたりの原価」を変更してください
- 保存
- 数秒以内に、価格と比較対象価格が自動的に更新されます
何も起こらない場合は、以下の順序で順を追って確認してください。各手順は、その前の手順を排除していく形になるためです:
| 確認 | どこで |
|---|---|
| 引き金は引かれたのか? | Workflow Trigger Extensions のイベント履歴 |
| Flowは動作しましたか? | Shopify Flow アクティビティログ |
| その関数は実行されましたか?また、ログには何が記録されましたか? | Workflow Functionsでの実行履歴 |
実行が失敗したと報告される最も一般的な原因は、スコープの欠落です。ctx.shopify は、「Shopify **データが必要」**がオンになっている場合にのみ利用可能であり、製品への書き込み権限が付与されて初めて書き込みが成功します。
次はどこへ行こうか
このパターンは、原価の変動が影響を与えるあらゆる場面で応用できます。関数本体を変更すれば、原価が10%以上上昇した際にその商品を審査対象としてマークしたり、Slackに新旧の利益率を投稿したり、レポート用にメタフィールドに変更内容を書き込んだりすることが可能です。
トリガーとは、Shopify では提供されない部分のことです。その後の処理は通常のコードです。
Workflow Functions ドキュメント
- 権限とストアデータへのアクセス - 「Shopify」データの必要条件を有効にし、スコープを許可する
- 関数への変数の引き渡し - JSON 入力と Liquid の仕組み
- Shopify Flowで「関数の実行」アクションを設定する
- 実行履歴とトラブルシューティング

