Beansmile
Blog

The Shopify AI Toolkit in Practice: Turning AI into Your Development Accelerator

A hands-on guide to the Shopify AI Toolkit, showing through real scenarios how to generate GraphQL queries, scaffold apps, automate bulk operations, handle webhooks, and build UI extensions faster.

Introduction

From reading documentation to writing code, from debugging errors to optimizing performance, a Shopify developer's day is filled with repetitive work.

The launch of the Shopify AI Toolkit is changing all of that. It isn't a simple code generator, but an intelligent development assistant with deep, built-in knowledge of the Shopify platform.

This article skips the abstract concepts and goes straight to real-world examples, showing how to use the Shopify AI Toolkit to boost your development efficiency.

Part One: Quick Start

Installation (one command and you're done)

# Claude Code 用户
/plugin marketplace add Shopify/shopify-ai-toolkit
/plugin install shopify-plugin@shopify-ai-toolkit

# MCP 方式(推荐)
npm install -g @shopify/dev-mcp

# 配置到 Claude Desktop
# 在 ~/Library/Application Support/Claude/claude_desktop_config.json 添加:
{
  "mcpServers": {
    "shopify-dev-mcp": {
      "command": "npx",
      "args": ["-y", "@shopify/dev-mcp@latest"]
    }
  }
}

Prerequisites: Node.js 18+, with Claude Code / Cursor / VS Code already installed

That's all it takes. Now let's look at how to use it.

Part Two: Real-World Scenarios

Scenario 1: Quickly generating GraphQL queries

The traditional approach

Open the Shopify GraphQL docs -> find the fields you need -> write the query by hand -> test and debug

The AI Toolkit approach

Prompt:

我需要获取最近 7 天内的所有订单,包括客户信息、订单金额和物流状态。请生成对应的 GraphQL 查询。

AI output:

query GetRecentOrders($first: Int!) {
  orders(first: $first, query: "created_at:>=7days ago") {
    edges {
      node {
        id
        orderNumber
        createdAt
        currentTotalPrice {
          amount
          currencyCode
        }
        customer {
          id
          email
          firstName
          lastName
        }
        fulfillments(first: 10) {
          edges {
            node {
              id
              status
              trackingInfo {
                company
                trackingNumber
              }
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Key advantages:

  • - Accurate fields that match the latest Shopify GraphQL spec
  • - Pagination logic included
  • - Time-based filtering handled automatically
  • - No need to dig through the docs

Going further: automatically generating TypeScript types

Prompt:

基于上面的查询,生成 TypeScript 接口定义

AI output:

interface Money {
  amount: string;
  currencyCode: string;
}

interface Customer {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
}

interface TrackingInfo {
  company: string;
  trackingNumber: string;
}

interface Fulfillment {
  id: string;
  status: string;
  trackingInfo: TrackingInfo;
}

interface Order {
  id: string;
  orderNumber: number;
  createdAt: string;
  currentTotalPrice: Money;
  customer: Customer;
  fulfillments: {
    edges: Array<{
      node: Fulfillment;
    }>;
  };
}

interface OrdersResponse {
  orders: {
    edges: Array<{
      node: Order;
    }>;
    pageInfo: {
      hasNextPage: boolean;
      endCursor: string | null;
    };
  };
}

Scenario 2: Scaffolding a Shopify App

Prompt:

创建一个 Shopify Remix app,包含以下功能:
1. 产品批量价格更新
2. 使用 Shopify Admin API
3. 包含权限请求配置
4. 使用 Polaris 组件库

The AI will generate:

shopify.app.toml

name = "Batch Price Updater"
id = "your-app-id"

[access_scopes]
scopes = "write_products,read_products"

[app_settings]
embedded = true

[auth]
redirect_urls = [
  "https://your-app.com/auth/callback",
  "https://your-app.com/auth/shopify/callback"
]

[pos]
embedded = false

app/routes/app.products.tsx

import { json, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { useLoaderData, Form } from "@remix-run/react";
import {
  Page,
  LegacyCard,
  TextField,
  Button,
  Layout
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { admin, session } = await authenticate.admin(request);

  const response = await admin.graphql(
    `#graphql
    query GetProducts($first: Int!) {
      products(first: $first) {
        edges {
          node {
            id
            title
            handle
            variants(first: 1) {
              edges {
                node {
                  id
                  price
                  unitPrice {
                    amount
                    currencyCode
                  }
                }
              }
            }
          }
        }
      }
    }`,
    {
      variables: { first: 50 }
    }
  );

  const data = await response.json();
  return json(data.data.products);
};

export const action = async ({ request }: ActionFunctionArgs) => {
  const { admin } = await authenticate.admin(request);
  const formData = await request.formData();

  const updates: Array<{ id: string; price: string }> = [];

  for (const [key, value] of formData.entries()) {
    if (key.startsWith("price-")) {
      const variantId = key.replace("price-", "");
      updates.push({
        id: variantId,
        price: value.toString(),
      });
    }
  }

  const UPDATE_MUTATION = `
    mutation productVariantUpdate($input: ProductVariantInput!) {
      productVariantUpdate(input: $input) {
        productVariant {
          id
          price
        }
        userErrors {
          field
          message
        }
      }
    }
  `;

  try {
    for (const update of updates) {
      await admin.graphql(UPDATE_MUTATION, {
        variables: {
          input: {
            id: update.id,
            price: update.price,
          },
        },
      });
    }

    return json({ success: true, updatedCount: updates.length });
  } catch (error) {
    return json(
      { success: false, error: error.message },
      { status: 500 }
    );
  }
};

export default function Products() {
  const products = useLoaderData();

  return (
    <Page title="批量价格更新">
      <Layout>
        <Layout.Section>
          <LegacyCard sectioned>
            <Form method="post">
              {products?.edges?.map(({ node }: any) => (
                <LegacyCard.Section
                  key={node.id}
                  title={node.title}
                >
                  <TextField
                    label="价格"
                    name={`price-${node.variants.edges[0]?.node.id}`}
                    defaultValue={node.variants.edges[0]?.node.price}
                    type="number"
                    prefix="$"
                  />
                </LegacyCard.Section>
              ))}
              <Button submit variant="primary">
                更新所有价格
              </Button>
            </Form>
          </LegacyCard>
        </Layout.Section>
      </Layout>
    </Page>
  );
}

Key points:

  • - Authentication handled automatically: authenticate.admin() validates the Shopify session for you
  • - GraphQL integration: use the admin GraphQL client directly
  • - Polaris components: built on Shopify's design system for consistency
  • - Type safety: GraphQL response types are inferred automatically
  • - Full CRUD: the loader reads data and the action updates it

Scenario 3: Automating bulk operations

Requirement: translate all product descriptions into Chinese

Prompt:

使用 Shopify Bulk Operations 编写一个批量操作,将所有英文产品描述翻译成中文。要求:
1. 使用 GraphQL bulkOperationRunQuery
2. 分批处理,每批 500 个产品
3. 包含错误处理和重试机制
4. 使用 Remix action 实现
5. 添加 API 速率限制处理

AI-generated code:

import { json, type ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import pLimit from 'p-limit';

const PRODUCT_UPDATE_MUTATION = `
  mutation productUpdate($input: ProductInput!) {
    productUpdate(input: $input) {
      product {
        id
        descriptionHtml
      }
      userErrors {
        field
        message
      }
    }
  }
`;

const BULK_QUERY = `
  mutation {
    bulkOperationRunQuery(
      query: """
        {
          products {
            edges {
              node {
                __typename
                id
                title
                descriptionHtml
              }
            }
          }
        }
      """
    ) {
      bulkOperation {
        id
        status
      }
      userErrors {
        field
        message
      }
    }
  }
`;

const limit = pLimit(2);

export const action = async ({ request }: ActionFunctionArgs) => {
  const { admin } = await authenticate.admin(request);

  try {
    const bulkResponse = await admin.graphql(BULK_QUERY);
    const bulkData = await bulkResponse.json();

    if (bulkData.data?.bulkOperationRunQuery?.userErrors?.length > 0) {
      throw new Error(bulkData.data.bulkOperationRunQuery.userErrors[0].message);
    }

    let isComplete = false;
    let attempts = 0;
    const maxAttempts = 60;

    while (!isComplete && attempts < maxAttempts) {
      await new Promise(resolve => setTimeout(resolve, 10000));

      const statusResponse = await admin.graphql(`
        {
          currentBulkOperation {
            id
            status
            errorCode
            createdAt
            completedAt
            objectCount
            fileSize
            url
          }
        }
      `);

      const statusData = await statusResponse.json();
      const status = statusData.data.currentBulkOperation;

      if (status.status === 'COMPLETED') {
        isComplete = true;

        const results = await fetchBulkResults(status.url);

        const batches = chunkArray(results, 500);
        for (const batch of batches) {
          await updateProductsBatch(admin, batch);
        }

        return json({
          success: true,
          updatedCount: results.length
        });
      } else if (status.status === 'FAILED') {
        throw new Error(`Bulk operation failed: ${status.errorCode}`);
      }

      attempts++;
    }

    if (!isComplete) {
      throw new Error('Bulk operation timed out');
    }

  } catch (error) {
    return json(
      { success: false, error: error.message },
      { status: 500 }
    );
  }
};

async function fetchBulkResults(url: string): Promise<any[]> {
  const response = await fetch(url);
  const text = await response.text();

  const products: any[] = [];
  const lines = text.split('\n').filter(line => line.trim());

  for (const line of lines) {
    try {
      const data = JSON.parse(line);
      if (data.__typename === 'Product') {
        products.push(data);
      }
    } catch (e) {
      console.error('Failed to parse line:', line);
    }
  }

  return products;
}

function chunkArray<T>(array: T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

async function updateProductsBatch(
  admin: any,
  products: any[]
): Promise<void> {
  const updatePromises = products.map((product) =>
    limit(async () => {
      const translatedDescription = await translateDescription(
        product.descriptionHtml
      );

      return admin.graphql(PRODUCT_UPDATE_MUTATION, {
        variables: {
          input: {
            id: product.id,
            descriptionHtml: translatedDescription,
          },
        },
      });
    })
  );

  await Promise.all(updatePromises);
}

async function translateDescription(text: string): Promise<string> {
  const apiKey = process.env.GOOGLE_TRANSLATE_API_KEY;

  if (!apiKey) {
    return `[中文翻译] ${text}`;
  }

  const response = await fetch(
    `https://translation.googleapis.com/language/translate/v2?key=${apiKey}`,
    {
      method: "POST",
      body: JSON.stringify({
        q: text,
        target: "zh-CN",
        source: "en",
      }),
    }
  );

  const data = await response.json();
  return data.data.translations[0].translatedText;
}

Code highlights:

  • - Complete error-handling flow
  • - A polling mechanism to track bulk operation status
  • - Batched processing to avoid timeouts
  • - API rate-limit handling (using p-limit)
  • - Integration with a real translation API

Scenario 4: Handling webhooks

Prompt:

实现一个 Shopify App webhook 处理器,监听 orders/create 事件:
1. 验证 webhook 真实性
2. 解析订单数据
3. 调用第三方 API(如 ERP 系统)
4. 异步处理,立即响应
5. 使用 Remix 实现

AI output:

import { type ActionFunctionArgs } from "@remix-run/node";
import crypto from "crypto";

export const action = async ({ request }: ActionFunctionArgs) => {
  const rawBody = await request.text();
  const hmac = request.headers.get("X-Shopify-Hmac-Sha256");

  if (!hmac) {
    return new Response("Missing HMAC header", { status: 401 });
  }

  const computedHmac = crypto
    .createHmac("sha256", process.env.SHOPIFY_API_SECRET!)
    .update(rawBody, "utf8")
    .digest("base64");

  if (hmac !== computedHmac) {
    return new Response("Invalid HMAC", { status: 401 });
  }

  const topic = request.headers.get("X-Shopify-Topic");
  const shop = request.headers.get("X-Shopify-Shop-Domain");

  if (topic !== "orders/create") {
    return new Response("Invalid topic", { status: 400 });
  }

  try {
    const order = JSON.parse(rawBody);

    processOrderInERP(order).catch((error) => {
      console.error("ERP processing failed:", error);
      sendErrorNotification(error, order.id);
    });

    return new Response(JSON.stringify({ success: true }), {
      status: 200,
      headers: { "Content-Type": "application/json" }
    });

  } catch (error) {
    console.error("Webhook error:", error);

    return new Response(JSON.stringify({ error: "Processing failed" }), {
      status: 500,
      headers: { "Content-Type": "application/json" }
    });
  }
};

async function processOrderInERP(order: any): Promise<void> {
  const erpResponse = await fetch(process.env.ERP_API_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${process.env.ERP_API_KEY}`,
    },
    body: JSON.stringify({
      external_id: order.id,
      customer_email: order.email,
      total_price: order.total_price,
      line_items: order.line_items.map((item: any) => ({
        product_id: item.product_id,
        quantity: item.quantity,
        price: item.price,
      })),
      created_at: order.created_at,
    }),
  });

  if (!erpResponse.ok) {
    throw new Error(`ERP API error: ${erpResponse.statusText}`);
  }
}

async function sendErrorNotification(error: any, orderId: string): Promise<void> {
  console.error(`Order ${orderId} failed to process in ERP:`, error);

  if (process.env.SLACK_WEBHOOK_URL) {
    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: "POST",
      body: JSON.stringify({
        text: `Order ${orderId} failed to process in ERP`,
        error: error.message,
      }),
    });
  }
}

shopify.app.toml configuration:

[[webhooks.subscriptions]]
topic = "orders/create"
uri = "/webhooks"

[webhooks]
api_version = "2024-01"

Key points:

  • - HMAC verification keeps the webhook secure
  • - Asynchronous processing avoids timeouts
  • - Shopify's built-in retry mechanism (returning a non-2xx status code triggers an automatic retry)
  • - Error notifications added

Scenario 5: Building UI extensions

Prompt:

创建一个 Shopify Checkout UI 扩展,在结账页面显示:
1. 自定义横幅,显示促销信息
2. 条件:订单金额超过 $100 才显示
3. 使用 React 组件
4. 遵循 Shopify 设计规范

AI output:

extensions/checkout-banner/src/index.jsx

import {
  extend,
  Banner,
  BlockStack,
  Text,
  useCartCost,
} from "@shopify/ui-extensions-react/checkout";

export default extend("Checkout::RenderBlock::RenderBlock", (root, api) => {
  const { cartCost } = api;
  const totalAmount = parseFloat(cartCost.totalAmount.current.amount);

  if (totalAmount < 100) {
    return;
  }

  const banner = root.createComponent(
    Banner,
    { status: "success" },
    [
      root.createComponent(
        BlockStack,
        { spacing: "base" },
        [
          root.createComponent(
            Text,
            { variant: "headingMd", as: "h3" },
            "🎉 免运费优惠"
          ),
          root.createComponent(
            Text,
            { variant: "bodyMd" },
            `订单金额 $${totalAmount.toFixed(2)} 已超过 $100,自动免运费!`
          ),
          totalAmount < 150 ? root.createComponent(
            Text,
            { variant: "bodySm", emphasis: "subtle" },
            `再购买 $${(150 - totalAmount).toFixed(2)} 即可获得额外礼品`
          ) : null,
        ]
      ),
    ]
  );

  root.appendChild(banner);
});

extensions/checkout-banner/shopify.ui.extension.toml

name = "促销横幅"
type = "checkout_ui_extension"

[[extensions.targeting]]
target = "purchase.checkout.block.render"
module = "./index.jsx"

[extensions.capabilities]
api_access = true
block_progress = true

Or, in a React component style

import {
  extend,
  Banner,
  BlockStack,
  Text,
  useCartCost,
  useTranslate,
} from "@shopify/ui-extensions-react/checkout";

export default extend("Checkout::RenderBlock::RenderBlock", () => {
  const cartCost = useCartCost();
  const translate = useTranslate();

  const totalAmount = parseFloat(cartCost.totalAmount.current.amount);

  if (totalAmount < 100) {
    return null;
  }

  return (
    <Banner status="success">
      <BlockStack spacing="base">
        <Text variant="headingMd" as="h3">
          🎉 免运费优惠
        </Text>
        <Text variant="bodyMd" as="p">
          订单金额 ${totalAmount.toFixed(2)} 已超过 $100,自动免运费!
        </Text>
        {totalAmount < 150 && (
          <Text variant="bodySm" emphasis="subtle">
            再购买 ${(150 - totalAmount).toFixed(2)} 即可获得额外礼品
          </Text>
        )}
      </BlockStack>
    </Banner>
  );
});

Key points:

  • - Uses @shopify/ui-extensions-react/checkout
  • - Conditional rendering avoids unnecessary components
  • - Internationalization (i18n) support
  • - Two implementation styles: the traditional API and React components

Part Three: Advanced Techniques

1. A multi-step development workflow

Don't generate all the code at once -- working in steps is more efficient:

第一步:先创建数据库模型
创建一个 Prisma schema,包含产品、订单和客户的关联关系

第二步:基于模型生成 API 路由
使用 Remix 和刚才的 Prisma schema,创建 CRUD API

第三步:添加前端界面
使用 Polaris 组件创建产品管理界面

第四步:添加错误处理和测试
为所有 API 添加错误处理和单元测试

2. Iterative refinement

After generating the initial code, keep refining it:

优化上面的产品管理界面:
1. 添加搜索和筛选功能
2. 实现分页加载
3. 添加加载状态和错误提示
4. 优化移动端显示

3. Code review mode

Let the AI review your code:

请审查以下代码,检查:
1. 安全漏洞
2. 性能问题
3. Shopify 最佳实践
4. TypeScript 类型安全
[粘贴你的代码]

Part Four: Handling Common Issues

Q1: The GraphQL query is too complex -- how do I break it up?

Prompt:

我有一个复杂的查询需要获取产品详情、库存和订单数据。帮我拆分成多个小查询,每个查询负责一个领域,避免 N+1 问题

The AI will generate:

  • - A standalone products query
  • - A standalone inventoryLevels query
  • - A standalone orders query
  • - Batching code that uses the DataLoader pattern

Q2: How do I handle API rate limits?

Prompt:

实现一个 Shopify API 请求队列,具备以下功能:
1. 自动检测速率限制(429 响应)
2. 指数退避重试
3. 请求优先级
4. 并发控制(使用 p-limit)

Q3: How do I debug GraphQL errors?

Prompt:

创建一个 GraphQL 错误处理中间件:
1. 解析 userErrors 字段
2. 格式化错误信息
3. 提供修复建议
4. 记录错误日志

Part Five: A Real-World Project Example

Project: an inventory sync tool

Requirements:

创建一个 Shopify App,实现以下功能:
1. 每 5 分钟同步一次库存到 ERP 系统
2. 使用 Shopify InventoryLevelUpdated webhook 实时更新
3. 批量处理 API 调用
4. 包含完整的错误处理和监控
5. 使用 Admin API 和 Webhooks

The AI will generate a complete project structure:

shopify-app/
├── shopify.app.toml
├── app/
│   ├── routes/
│   │   ├── app.sync.tsx          # 同步管理界面
│   │   ├── app.webhooks.tsx       # webhook 配置
│   │   └── webhooks.ts            # webhook 处理
│   ├── services/
│   │   ├── sync.service.ts        # 同步逻辑
│   │   ├── queue.service.ts       # 任务队列
│   │   └── monitor.service.ts     # 监控服务
│   └── utils/
│       ├── api-client.ts          # API 客户端
│       └── error-handler.ts       # 错误处理
└── prisma/
    └── schema.prisma              # 数据库模型

Every file comes with a full implementation, including:

  • - Scheduled tasks
  • - Webhook handling
  • - Bulk operations
  • - Error retries
  • - A monitoring dashboard

Conclusion

The real value of the Shopify AI Toolkit isn't "automatically generating code" -- it's that it:

  1. 1. Reduces context switching: no more constantly flipping through the docs
  2. 2. Improves code quality: best practices are followed automatically
  3. 3. Flattens the learning curve: newcomers get up to speed faster
  4. 4. Keeps you focused on core logic: let the AI handle the boilerplate

Key recommendations:

  • - Start with small tasks and gradually expand where you use it
  • - Always review the code the AI generates
  • - Keep building your own understanding of the Shopify platform
  • - Build your own library of prompt templates
  • - Watch out for API version compatibility
  • - Handle API rate limits
  • - Use asynchronous processing to avoid timeouts

The Shopify AI Toolkit is the tool; you are the one using it. Make the most of the tool, but keep thinking for yourself.

References:

  • - Shopify AI Toolkit official documentation
  • - Shopify GraphQL API reference
  • - Shopify App development best practices
  • - Shopify CLI documentation
  • - Polaris component library

Originally published by 乐豆说