diff --git a/README.md b/README.md
index c15185d8..58e81b45 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
- **GitHub**: Create Issue, List Issues, Get Issue, Update Issue
- **Linear**: Create Ticket, Find Issues
- **Resend**: Send Email
+- **Shopify**: Get Order, List Orders, Create Product, Update Inventory
- **Slack**: Send Slack Message
- **Superagent**: Guard, Redact
- **v0**: Create Chat, Send Message
diff --git a/plugins/index.ts b/plugins/index.ts
index af795cb5..0954a3f4 100644
--- a/plugins/index.ts
+++ b/plugins/index.ts
@@ -13,7 +13,7 @@
* 1. Delete the plugin directory
* 2. Run: pnpm discover-plugins (or it runs automatically on build)
*
- * Discovered plugins: ai-gateway, blob, firecrawl, github, linear, resend, slack, superagent, v0
+ * Discovered plugins: ai-gateway, blob, firecrawl, github, linear, resend, shopify, slack, superagent, v0
*/
import "./ai-gateway";
@@ -22,6 +22,7 @@ import "./firecrawl";
import "./github";
import "./linear";
import "./resend";
+import "./shopify";
import "./slack";
import "./superagent";
import "./v0";
diff --git a/plugins/shopify/credentials.ts b/plugins/shopify/credentials.ts
new file mode 100644
index 00000000..124c5afa
--- /dev/null
+++ b/plugins/shopify/credentials.ts
@@ -0,0 +1,4 @@
+export type ShopifyCredentials = {
+ SHOPIFY_STORE_DOMAIN?: string;
+ SHOPIFY_ACCESS_TOKEN?: string;
+};
diff --git a/plugins/shopify/icon.tsx b/plugins/shopify/icon.tsx
new file mode 100644
index 00000000..f2acecd4
--- /dev/null
+++ b/plugins/shopify/icon.tsx
@@ -0,0 +1,14 @@
+export function ShopifyIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/plugins/shopify/index.ts b/plugins/shopify/index.ts
new file mode 100644
index 00000000..235a6a76
--- /dev/null
+++ b/plugins/shopify/index.ts
@@ -0,0 +1,292 @@
+import type { IntegrationPlugin } from "../registry";
+import { registerIntegration } from "../registry";
+import { ShopifyIcon } from "./icon";
+
+const shopifyPlugin: IntegrationPlugin = {
+ type: "shopify",
+ label: "Shopify",
+ description: "Manage orders, products, and inventory in your Shopify store",
+
+ icon: ShopifyIcon,
+
+ formFields: [
+ {
+ id: "storeDomain",
+ label: "Store Domain",
+ type: "text",
+ placeholder: "your-store.myshopify.com",
+ configKey: "storeDomain",
+ envVar: "SHOPIFY_STORE_DOMAIN",
+ helpText: "Your Shopify store domain (e.g., your-store.myshopify.com)",
+ },
+ {
+ id: "accessToken",
+ label: "Admin API Access Token",
+ type: "password",
+ placeholder: "shpat_...",
+ configKey: "accessToken",
+ envVar: "SHOPIFY_ACCESS_TOKEN",
+ helpText: "Create an access token from ",
+ helpLink: {
+ text: "Shopify Admin > Apps > Develop apps",
+ url: "https://help.shopify.com/en/manual/apps/app-types/custom-apps",
+ },
+ },
+ ],
+
+ testConfig: {
+ getTestFunction: async () => {
+ const { testShopify } = await import("./test");
+ return testShopify;
+ },
+ },
+
+ actions: [
+ {
+ slug: "get-order",
+ label: "Get Order",
+ description: "Retrieve details of a specific order by ID",
+ category: "Shopify",
+ stepFunction: "getOrderStep",
+ stepImportPath: "get-order",
+ outputFields: [
+ { field: "id", description: "Unique ID of the order" },
+ { field: "orderNumber", description: "Human-readable order number" },
+ { field: "name", description: "Order name (e.g., #1001)" },
+ { field: "email", description: "Customer email address" },
+ { field: "totalPrice", description: "Total price of the order" },
+ { field: "currency", description: "Currency code (e.g., USD)" },
+ {
+ field: "financialStatus",
+ description: "Payment status (pending, paid, refunded, etc.)",
+ },
+ {
+ field: "fulfillmentStatus",
+ description: "Fulfillment status (unfulfilled, fulfilled, partial)",
+ },
+ { field: "createdAt", description: "ISO timestamp when order was created" },
+ { field: "lineItems", description: "Array of line item objects" },
+ {
+ field: "shippingAddress",
+ description: "Shipping address object (if available)",
+ },
+ { field: "customer", description: "Customer information object" },
+ ],
+ configFields: [
+ {
+ key: "orderId",
+ label: "Order ID",
+ type: "template-input",
+ placeholder: "450789469 or {{NodeName.orderId}}",
+ example: "450789469",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "list-orders",
+ label: "List Orders",
+ description: "Search and list orders with optional filters",
+ category: "Shopify",
+ stepFunction: "listOrdersStep",
+ stepImportPath: "list-orders",
+ outputFields: [
+ { field: "orders", description: "Array of order objects" },
+ { field: "count", description: "Number of orders returned" },
+ ],
+ configFields: [
+ {
+ key: "status",
+ label: "Order Status",
+ type: "select",
+ defaultValue: "any",
+ options: [
+ { value: "any", label: "Any" },
+ { value: "open", label: "Open" },
+ { value: "closed", label: "Closed" },
+ { value: "cancelled", label: "Cancelled" },
+ ],
+ },
+ {
+ key: "financialStatus",
+ label: "Financial Status",
+ type: "select",
+ defaultValue: "",
+ options: [
+ { value: "", label: "Any" },
+ { value: "pending", label: "Pending" },
+ { value: "paid", label: "Paid" },
+ { value: "refunded", label: "Refunded" },
+ { value: "voided", label: "Voided" },
+ { value: "partially_refunded", label: "Partially Refunded" },
+ ],
+ },
+ {
+ key: "fulfillmentStatus",
+ label: "Fulfillment Status",
+ type: "select",
+ defaultValue: "",
+ options: [
+ { value: "", label: "Any" },
+ { value: "unfulfilled", label: "Unfulfilled" },
+ { value: "fulfilled", label: "Fulfilled" },
+ { value: "partial", label: "Partial" },
+ ],
+ },
+ {
+ key: "createdAtMin",
+ label: "Created After (ISO date)",
+ type: "template-input",
+ placeholder: "2024-01-01 or {{NodeName.date}}",
+ },
+ {
+ key: "createdAtMax",
+ label: "Created Before (ISO date)",
+ type: "template-input",
+ placeholder: "2024-12-31 or {{NodeName.date}}",
+ },
+ {
+ key: "limit",
+ label: "Limit",
+ type: "number",
+ min: 1,
+ defaultValue: "50",
+ },
+ ],
+ },
+ {
+ slug: "create-product",
+ label: "Create Product",
+ description: "Create a new product in your Shopify store",
+ category: "Shopify",
+ stepFunction: "createProductStep",
+ stepImportPath: "create-product",
+ outputFields: [
+ { field: "id", description: "Unique ID of the created product" },
+ { field: "title", description: "Title of the product" },
+ { field: "handle", description: "URL-friendly handle for the product" },
+ { field: "status", description: "Product status (active, draft, archived)" },
+ { field: "variants", description: "Array of product variants" },
+ { field: "createdAt", description: "ISO timestamp when product was created" },
+ ],
+ configFields: [
+ {
+ key: "title",
+ label: "Product Title",
+ type: "template-input",
+ placeholder: "Awesome T-Shirt or {{NodeName.title}}",
+ example: "Awesome T-Shirt",
+ required: true,
+ },
+ {
+ key: "bodyHtml",
+ label: "Description (HTML)",
+ type: "template-textarea",
+ placeholder: "
Product description...
",
+ rows: 4,
+ example: "A comfortable cotton t-shirt
",
+ },
+ {
+ key: "vendor",
+ label: "Vendor",
+ type: "template-input",
+ placeholder: "Your Brand or {{NodeName.vendor}}",
+ example: "Acme Inc",
+ },
+ {
+ key: "productType",
+ label: "Product Type",
+ type: "template-input",
+ placeholder: "T-Shirts or {{NodeName.type}}",
+ example: "Clothing",
+ },
+ {
+ key: "tags",
+ label: "Tags (comma-separated)",
+ type: "template-input",
+ placeholder: "summer, sale, new",
+ example: "summer, featured",
+ },
+ {
+ key: "status",
+ label: "Status",
+ type: "select",
+ defaultValue: "draft",
+ options: [
+ { value: "draft", label: "Draft" },
+ { value: "active", label: "Active" },
+ { value: "archived", label: "Archived" },
+ ],
+ },
+ {
+ key: "price",
+ label: "Price",
+ type: "template-input",
+ placeholder: "29.99 or {{NodeName.price}}",
+ example: "29.99",
+ },
+ {
+ key: "sku",
+ label: "SKU",
+ type: "template-input",
+ placeholder: "TSHIRT-001 or {{NodeName.sku}}",
+ example: "TSHIRT-001",
+ },
+ {
+ key: "inventoryQuantity",
+ label: "Inventory Quantity",
+ type: "number",
+ min: 0,
+ defaultValue: "0",
+ },
+ ],
+ },
+ {
+ slug: "update-inventory",
+ label: "Update Inventory",
+ description: "Update inventory levels for a product variant",
+ category: "Shopify",
+ stepFunction: "updateInventoryStep",
+ stepImportPath: "update-inventory",
+ outputFields: [
+ {
+ field: "inventoryItemId",
+ description: "ID of the inventory item updated",
+ },
+ { field: "locationId", description: "ID of the inventory location" },
+ { field: "available", description: "New available inventory quantity" },
+ { field: "previousQuantity", description: "Previous inventory quantity" },
+ ],
+ configFields: [
+ {
+ key: "inventoryItemId",
+ label: "Inventory Item ID",
+ type: "template-input",
+ placeholder: "808950810 or {{NodeName.inventoryItemId}}",
+ example: "808950810",
+ required: true,
+ },
+ {
+ key: "locationId",
+ label: "Location ID",
+ type: "template-input",
+ placeholder: "655441491 or {{NodeName.locationId}}",
+ example: "655441491",
+ required: true,
+ },
+ {
+ key: "adjustment",
+ label: "Quantity Adjustment",
+ type: "template-input",
+ placeholder: "10 or -5 or {{NodeName.adjustment}}",
+ example: "10",
+ required: true,
+ },
+ ],
+ },
+ ],
+};
+
+registerIntegration(shopifyPlugin);
+
+export default shopifyPlugin;
diff --git a/plugins/shopify/steps/create-product.ts b/plugins/shopify/steps/create-product.ts
new file mode 100644
index 00000000..df473ad3
--- /dev/null
+++ b/plugins/shopify/steps/create-product.ts
@@ -0,0 +1,197 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import { getErrorMessage } from "@/lib/utils";
+import type { ShopifyCredentials } from "../credentials";
+
+type ShopifyVariant = {
+ id: number;
+ product_id: number;
+ title: string;
+ price: string;
+ sku?: string;
+ inventory_quantity?: number;
+ inventory_item_id?: number;
+};
+
+type ShopifyProduct = {
+ id: number;
+ title: string;
+ handle: string;
+ status: string;
+ body_html?: string;
+ vendor?: string;
+ product_type?: string;
+ tags: string;
+ created_at: string;
+ updated_at: string;
+ variants: ShopifyVariant[];
+};
+
+type CreateProductResult =
+ | {
+ success: true;
+ id: number;
+ title: string;
+ handle: string;
+ status: string;
+ variants: Array<{
+ id: number;
+ title: string;
+ price: string;
+ sku?: string;
+ inventoryItemId?: number;
+ }>;
+ createdAt: string;
+ }
+ | { success: false; error: string };
+
+export type CreateProductCoreInput = {
+ title: string;
+ bodyHtml?: string;
+ vendor?: string;
+ productType?: string;
+ tags?: string;
+ status?: string;
+ price?: string;
+ sku?: string;
+ inventoryQuantity?: number;
+};
+
+export type CreateProductInput = StepInput &
+ CreateProductCoreInput & {
+ integrationId?: string;
+ };
+
+function normalizeStoreDomain(domain: string): string {
+ return domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
+}
+
+async function stepHandler(
+ input: CreateProductCoreInput,
+ credentials: ShopifyCredentials
+): Promise {
+ const storeDomain = credentials.SHOPIFY_STORE_DOMAIN;
+ const accessToken = credentials.SHOPIFY_ACCESS_TOKEN;
+
+ if (!storeDomain) {
+ return {
+ success: false,
+ error:
+ "SHOPIFY_STORE_DOMAIN is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!accessToken) {
+ return {
+ success: false,
+ error:
+ "SHOPIFY_ACCESS_TOKEN is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ try {
+ const normalizedDomain = normalizeStoreDomain(storeDomain);
+ const url = `https://${normalizedDomain}/admin/api/2024-01/products.json`;
+
+ // Build the product payload
+ const productPayload: Record = {
+ title: input.title,
+ };
+
+ if (input.bodyHtml) {
+ productPayload.body_html = input.bodyHtml;
+ }
+
+ if (input.vendor) {
+ productPayload.vendor = input.vendor;
+ }
+
+ if (input.productType) {
+ productPayload.product_type = input.productType;
+ }
+
+ if (input.tags) {
+ productPayload.tags = input.tags;
+ }
+
+ if (input.status) {
+ productPayload.status = input.status;
+ }
+
+ // Add variant with price/sku if provided
+ if (input.price || input.sku) {
+ const variant: Record = {};
+
+ if (input.price) {
+ variant.price = input.price;
+ }
+
+ if (input.sku) {
+ variant.sku = input.sku;
+ }
+
+ productPayload.variants = [variant];
+ }
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "X-Shopify-Access-Token": accessToken,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ product: productPayload }),
+ });
+
+ if (!response.ok) {
+ const errorData = (await response.json()) as { errors?: unknown };
+ const errorMessage =
+ typeof errorData.errors === "string"
+ ? errorData.errors
+ : JSON.stringify(errorData.errors);
+ return {
+ success: false,
+ error: errorMessage || `HTTP ${response.status}`,
+ };
+ }
+
+ const data = (await response.json()) as { product: ShopifyProduct };
+ const product = data.product;
+
+ return {
+ success: true,
+ id: product.id,
+ title: product.title,
+ handle: product.handle,
+ status: product.status,
+ variants: product.variants.map((v) => ({
+ id: v.id,
+ title: v.title,
+ price: v.price,
+ sku: v.sku,
+ inventoryItemId: v.inventory_item_id,
+ })),
+ createdAt: product.created_at,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to create product: ${getErrorMessage(error)}`,
+ };
+ }
+}
+
+export async function createProductStep(
+ input: CreateProductInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+
+export const _integrationType = "shopify";
diff --git a/plugins/shopify/steps/get-order.ts b/plugins/shopify/steps/get-order.ts
new file mode 100644
index 00000000..5b35ea23
--- /dev/null
+++ b/plugins/shopify/steps/get-order.ts
@@ -0,0 +1,215 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import { getErrorMessage } from "@/lib/utils";
+import type { ShopifyCredentials } from "../credentials";
+
+type ShopifyLineItem = {
+ id: number;
+ title: string;
+ quantity: number;
+ price: string;
+ sku?: string;
+ variant_id?: number;
+ product_id?: number;
+};
+
+type ShopifyAddress = {
+ first_name?: string;
+ last_name?: string;
+ address1?: string;
+ address2?: string;
+ city?: string;
+ province?: string;
+ country?: string;
+ zip?: string;
+ phone?: string;
+};
+
+type ShopifyCustomer = {
+ id: number;
+ email?: string;
+ first_name?: string;
+ last_name?: string;
+};
+
+type ShopifyOrder = {
+ id: number;
+ order_number: number;
+ name: string;
+ email?: string;
+ total_price: string;
+ currency: string;
+ financial_status: string;
+ fulfillment_status: string | null;
+ created_at: string;
+ updated_at: string;
+ line_items: ShopifyLineItem[];
+ shipping_address?: ShopifyAddress;
+ customer?: ShopifyCustomer;
+};
+
+type GetOrderResult =
+ | {
+ success: true;
+ id: number;
+ orderNumber: number;
+ name: string;
+ email?: string;
+ totalPrice: string;
+ currency: string;
+ financialStatus: string;
+ fulfillmentStatus: string | null;
+ createdAt: string;
+ lineItems: Array<{
+ id: number;
+ title: string;
+ quantity: number;
+ price: string;
+ sku?: string;
+ variantId?: number;
+ productId?: number;
+ }>;
+ shippingAddress?: {
+ firstName?: string;
+ lastName?: string;
+ address1?: string;
+ address2?: string;
+ city?: string;
+ province?: string;
+ country?: string;
+ zip?: string;
+ phone?: string;
+ };
+ customer?: {
+ id: number;
+ email?: string;
+ firstName?: string;
+ lastName?: string;
+ };
+ }
+ | { success: false; error: string };
+
+export type GetOrderCoreInput = {
+ orderId: string;
+};
+
+export type GetOrderInput = StepInput &
+ GetOrderCoreInput & {
+ integrationId?: string;
+ };
+
+function normalizeStoreDomain(domain: string): string {
+ return domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
+}
+
+async function stepHandler(
+ input: GetOrderCoreInput,
+ credentials: ShopifyCredentials
+): Promise {
+ const storeDomain = credentials.SHOPIFY_STORE_DOMAIN;
+ const accessToken = credentials.SHOPIFY_ACCESS_TOKEN;
+
+ if (!storeDomain) {
+ return {
+ success: false,
+ error:
+ "SHOPIFY_STORE_DOMAIN is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!accessToken) {
+ return {
+ success: false,
+ error:
+ "SHOPIFY_ACCESS_TOKEN is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ try {
+ const normalizedDomain = normalizeStoreDomain(storeDomain);
+ const url = `https://${normalizedDomain}/admin/api/2024-01/orders/${input.orderId}.json`;
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ "X-Shopify-Access-Token": accessToken,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = (await response.json()) as { errors?: string };
+ return {
+ success: false,
+ error: errorData.errors || `HTTP ${response.status}`,
+ };
+ }
+
+ const data = (await response.json()) as { order: ShopifyOrder };
+ const order = data.order;
+
+ return {
+ success: true,
+ id: order.id,
+ orderNumber: order.order_number,
+ name: order.name,
+ email: order.email,
+ totalPrice: order.total_price,
+ currency: order.currency,
+ financialStatus: order.financial_status,
+ fulfillmentStatus: order.fulfillment_status,
+ createdAt: order.created_at,
+ lineItems: order.line_items.map((item) => ({
+ id: item.id,
+ title: item.title,
+ quantity: item.quantity,
+ price: item.price,
+ sku: item.sku,
+ variantId: item.variant_id,
+ productId: item.product_id,
+ })),
+ shippingAddress: order.shipping_address
+ ? {
+ firstName: order.shipping_address.first_name,
+ lastName: order.shipping_address.last_name,
+ address1: order.shipping_address.address1,
+ address2: order.shipping_address.address2,
+ city: order.shipping_address.city,
+ province: order.shipping_address.province,
+ country: order.shipping_address.country,
+ zip: order.shipping_address.zip,
+ phone: order.shipping_address.phone,
+ }
+ : undefined,
+ customer: order.customer
+ ? {
+ id: order.customer.id,
+ email: order.customer.email,
+ firstName: order.customer.first_name,
+ lastName: order.customer.last_name,
+ }
+ : undefined,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to get order: ${getErrorMessage(error)}`,
+ };
+ }
+}
+
+export async function getOrderStep(
+ input: GetOrderInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+
+export const _integrationType = "shopify";
diff --git a/plugins/shopify/steps/list-orders.ts b/plugins/shopify/steps/list-orders.ts
new file mode 100644
index 00000000..01943ae9
--- /dev/null
+++ b/plugins/shopify/steps/list-orders.ts
@@ -0,0 +1,183 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import { getErrorMessage } from "@/lib/utils";
+import type { ShopifyCredentials } from "../credentials";
+
+type ShopifyLineItem = {
+ id: number;
+ title: string;
+ quantity: number;
+ price: string;
+};
+
+type ShopifyOrder = {
+ id: number;
+ order_number: number;
+ name: string;
+ email?: string;
+ total_price: string;
+ currency: string;
+ financial_status: string;
+ fulfillment_status: string | null;
+ created_at: string;
+ updated_at: string;
+ line_items: ShopifyLineItem[];
+};
+
+type OrderSummary = {
+ id: number;
+ orderNumber: number;
+ name: string;
+ email?: string;
+ totalPrice: string;
+ currency: string;
+ financialStatus: string;
+ fulfillmentStatus: string | null;
+ createdAt: string;
+ updatedAt: string;
+ itemCount: number;
+};
+
+type ListOrdersResult =
+ | {
+ success: true;
+ orders: OrderSummary[];
+ count: number;
+ }
+ | { success: false; error: string };
+
+export type ListOrdersCoreInput = {
+ status?: string;
+ financialStatus?: string;
+ fulfillmentStatus?: string;
+ createdAtMin?: string;
+ createdAtMax?: string;
+ limit?: number;
+};
+
+export type ListOrdersInput = StepInput &
+ ListOrdersCoreInput & {
+ integrationId?: string;
+ };
+
+function normalizeStoreDomain(domain: string): string {
+ return domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
+}
+
+async function stepHandler(
+ input: ListOrdersCoreInput,
+ credentials: ShopifyCredentials
+): Promise {
+ const storeDomain = credentials.SHOPIFY_STORE_DOMAIN;
+ const accessToken = credentials.SHOPIFY_ACCESS_TOKEN;
+
+ if (!storeDomain) {
+ return {
+ success: false,
+ error:
+ "SHOPIFY_STORE_DOMAIN is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!accessToken) {
+ return {
+ success: false,
+ error:
+ "SHOPIFY_ACCESS_TOKEN is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ try {
+ const normalizedDomain = normalizeStoreDomain(storeDomain);
+ const params = new URLSearchParams();
+
+ if (input.status && input.status !== "any") {
+ params.set("status", input.status);
+ }
+
+ if (input.financialStatus) {
+ params.set("financial_status", input.financialStatus);
+ }
+
+ if (input.fulfillmentStatus) {
+ params.set("fulfillment_status", input.fulfillmentStatus);
+ }
+
+ if (input.createdAtMin) {
+ params.set("created_at_min", input.createdAtMin);
+ }
+
+ if (input.createdAtMax) {
+ params.set("created_at_max", input.createdAtMax);
+ }
+
+ if (input.limit) {
+ params.set("limit", String(input.limit));
+ } else {
+ params.set("limit", "50");
+ }
+
+ const url = `https://${normalizedDomain}/admin/api/2024-01/orders.json${
+ params.toString() ? `?${params.toString()}` : ""
+ }`;
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ "X-Shopify-Access-Token": accessToken,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = (await response.json()) as { errors?: string };
+ return {
+ success: false,
+ error: errorData.errors || `HTTP ${response.status}`,
+ };
+ }
+
+ const data = (await response.json()) as { orders: ShopifyOrder[] };
+
+ const orders: OrderSummary[] = data.orders.map((order) => ({
+ id: order.id,
+ orderNumber: order.order_number,
+ name: order.name,
+ email: order.email,
+ totalPrice: order.total_price,
+ currency: order.currency,
+ financialStatus: order.financial_status,
+ fulfillmentStatus: order.fulfillment_status,
+ createdAt: order.created_at,
+ updatedAt: order.updated_at,
+ itemCount: order.line_items.reduce((sum, item) => sum + item.quantity, 0),
+ }));
+
+ return {
+ success: true,
+ orders,
+ count: orders.length,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to list orders: ${getErrorMessage(error)}`,
+ };
+ }
+}
+
+export async function listOrdersStep(
+ input: ListOrdersInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+
+export const _integrationType = "shopify";
diff --git a/plugins/shopify/steps/update-inventory.ts b/plugins/shopify/steps/update-inventory.ts
new file mode 100644
index 00000000..1a90d683
--- /dev/null
+++ b/plugins/shopify/steps/update-inventory.ts
@@ -0,0 +1,169 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import { getErrorMessage } from "@/lib/utils";
+import type { ShopifyCredentials } from "../credentials";
+
+type InventoryLevel = {
+ inventory_item_id: number;
+ location_id: number;
+ available: number;
+ updated_at: string;
+};
+
+type UpdateInventoryResult =
+ | {
+ success: true;
+ inventoryItemId: number;
+ locationId: number;
+ available: number;
+ previousQuantity: number;
+ }
+ | { success: false; error: string };
+
+export type UpdateInventoryCoreInput = {
+ inventoryItemId: string;
+ locationId: string;
+ adjustment: string;
+};
+
+export type UpdateInventoryInput = StepInput &
+ UpdateInventoryCoreInput & {
+ integrationId?: string;
+ };
+
+function normalizeStoreDomain(domain: string): string {
+ return domain.replace(/^https?:\/\//, "").replace(/\/$/, "");
+}
+
+async function stepHandler(
+ input: UpdateInventoryCoreInput,
+ credentials: ShopifyCredentials
+): Promise {
+ const storeDomain = credentials.SHOPIFY_STORE_DOMAIN;
+ const accessToken = credentials.SHOPIFY_ACCESS_TOKEN;
+
+ if (!storeDomain) {
+ return {
+ success: false,
+ error:
+ "SHOPIFY_STORE_DOMAIN is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!accessToken) {
+ return {
+ success: false,
+ error:
+ "SHOPIFY_ACCESS_TOKEN is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ const adjustmentValue = Number.parseInt(input.adjustment, 10);
+ if (Number.isNaN(adjustmentValue)) {
+ return {
+ success: false,
+ error: "Adjustment must be a valid integer (e.g., 10 or -5)",
+ };
+ }
+
+ try {
+ const normalizedDomain = normalizeStoreDomain(storeDomain);
+
+ // First, get the current inventory level
+ const getUrl = `https://${normalizedDomain}/admin/api/2024-01/inventory_levels.json?inventory_item_ids=${input.inventoryItemId}&location_ids=${input.locationId}`;
+
+ const getResponse = await fetch(getUrl, {
+ method: "GET",
+ headers: {
+ "X-Shopify-Access-Token": accessToken,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!getResponse.ok) {
+ const errorData = (await getResponse.json()) as { errors?: string };
+ return {
+ success: false,
+ error:
+ errorData.errors ||
+ `Failed to get current inventory: HTTP ${getResponse.status}`,
+ };
+ }
+
+ const getCurrentData = (await getResponse.json()) as {
+ inventory_levels: InventoryLevel[];
+ };
+
+ if (getCurrentData.inventory_levels.length === 0) {
+ return {
+ success: false,
+ error:
+ "Inventory level not found for the specified item and location. Make sure the inventory item is stocked at this location.",
+ };
+ }
+
+ const previousQuantity = getCurrentData.inventory_levels[0].available;
+
+ // Now adjust the inventory
+ const adjustUrl = `https://${normalizedDomain}/admin/api/2024-01/inventory_levels/adjust.json`;
+
+ const response = await fetch(adjustUrl, {
+ method: "POST",
+ headers: {
+ "X-Shopify-Access-Token": accessToken,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ location_id: Number.parseInt(input.locationId, 10),
+ inventory_item_id: Number.parseInt(input.inventoryItemId, 10),
+ available_adjustment: adjustmentValue,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = (await response.json()) as { errors?: unknown };
+ const errorMessage =
+ typeof errorData.errors === "string"
+ ? errorData.errors
+ : JSON.stringify(errorData.errors);
+ return {
+ success: false,
+ error: errorMessage || `HTTP ${response.status}`,
+ };
+ }
+
+ const data = (await response.json()) as {
+ inventory_level: InventoryLevel;
+ };
+ const level = data.inventory_level;
+
+ return {
+ success: true,
+ inventoryItemId: level.inventory_item_id,
+ locationId: level.location_id,
+ available: level.available,
+ previousQuantity,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to update inventory: ${getErrorMessage(error)}`,
+ };
+ }
+}
+
+export async function updateInventoryStep(
+ input: UpdateInventoryInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+
+export const _integrationType = "shopify";
diff --git a/plugins/shopify/test.ts b/plugins/shopify/test.ts
new file mode 100644
index 00000000..84332181
--- /dev/null
+++ b/plugins/shopify/test.ts
@@ -0,0 +1,81 @@
+type ShopInfo = {
+ shop: {
+ name: string;
+ email: string;
+ };
+};
+
+export async function testShopify(credentials: Record) {
+ try {
+ const storeDomain = credentials.SHOPIFY_STORE_DOMAIN;
+ const accessToken = credentials.SHOPIFY_ACCESS_TOKEN;
+
+ if (!storeDomain) {
+ return {
+ success: false,
+ error: "SHOPIFY_STORE_DOMAIN is required",
+ };
+ }
+
+ if (!accessToken) {
+ return {
+ success: false,
+ error: "SHOPIFY_ACCESS_TOKEN is required",
+ };
+ }
+
+ // Normalize store domain (remove protocol and trailing slashes)
+ const normalizedDomain = storeDomain
+ .replace(/^https?:\/\//, "")
+ .replace(/\/$/, "");
+
+ // Make a lightweight API call to verify credentials
+ const response = await fetch(
+ `https://${normalizedDomain}/admin/api/2024-01/shop.json`,
+ {
+ method: "GET",
+ headers: {
+ "X-Shopify-Access-Token": accessToken,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ return {
+ success: false,
+ error:
+ "Invalid access token. Please check your Shopify Admin API access token.",
+ };
+ }
+ if (response.status === 404) {
+ return {
+ success: false,
+ error:
+ "Store not found. Please check your store domain (e.g., your-store.myshopify.com).",
+ };
+ }
+ return {
+ success: false,
+ error: `API validation failed: HTTP ${response.status}`,
+ };
+ }
+
+ const data = (await response.json()) as ShopInfo;
+
+ if (!data.shop?.name) {
+ return {
+ success: false,
+ error: "Failed to verify Shopify connection",
+ };
+ }
+
+ return { success: true };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}