Shopify
Connecting a store does two things:
- Workflows get
ctx.SHOPIFY, which reads and changes that store’s products, orders, customers, subscriptions, and discounts. - Store events, such as an order, a new customer, a checkout, or a subscription change, can start a workflow.
One organization connects one store. Every Shopify call a loop makes goes to that store and no other.
Connect
- Go to Settings → Shopify.
- Enter your store’s domain, for example
your-store.myshopify.com, and click Connect Shopify. - Approve the app in Shopify. You come back to the same page, now showing the store as connected and the access it granted.
Installing from the Shopify App Store lands you on the same page with the store filled in. Sign in, then click Connect Shopify.
To disconnect, uninstall the app from your Shopify admin. Loops can’t call the store from then on. Reconnect runs the approval again, which you need after we add new access.
Access the app asks for
| Access | Used for |
|---|---|
read_products, read_inventory | Product search, product details, stock |
read_orders, write_orders | Order lookups, order notes and tags, order and checkout events |
read_customers, write_customers | Customer lookups, customer notes and tags, sign-up events |
read_own_subscription_contracts, write_own_subscription_contracts | Subscription lookups, pause, resume, skip, and cancel |
read_discounts, write_discounts | Single-use discount codes |
Shopify only lets an app manage subscription contracts that app created. Contracts sold through another subscription app are not visible to ctx.SHOPIFY.
Start a workflow from a store event
Open a workflow loop, go to Settings, and under Run this loop when… pick the events:
| Event | Shopify topic |
|---|---|
| An order is placed | orders/create |
| A customer signs up | customers/create |
| A checkout is started or abandoned | checkouts/update |
| A subscription changes | subscription_contracts/update |
Each event starts one run of every loop that picked it. The run’s event.body is:
type ShopifyEvent = {
source: "shopify";
topic: "orders/create" | "customers/create" | "checkouts/update" | "subscription_contracts/update";
shop: string;
webhookId: string | null;
triggeredAt: string | null;
customerId: string | null;
payload: unknown;
};payload is the webhook body exactly as Shopify sent it. When the event names a customer, the run is linked to the user shopify:<customerId> in Users.
checkouts/update fires every time a checkout changes, not only when it is abandoned.
import { workflow } from "@loops/sdk";
type ShopifyEvent = {
source: "shopify";
topic: string;
shop: string;
webhookId: string | null;
triggeredAt: string | null;
customerId: string | null;
payload: unknown;
};
export const firstOrderWorkflow = workflow<ShopifyEvent>({
id: "first-order",
execute: async ({ step, event }) => {
const { customerId } = event.body;
if (!customerId) return { skipped: "no customer" };
const orders = await step.do("Count their orders", ({ ctx }) =>
ctx.SHOPIFY.listCustomerOrders({ customerId, first: 2 }),
);
if (orders.length > 1) return { skipped: "returning customer" };
return step.do("Tag a first-time customer", ({ ctx }) =>
ctx.SHOPIFY.updateCustomer({ id: customerId, addTags: ["first-order"] }),
);
},
});Call the store
ctx.SHOPIFY is on ctx inside step.do. Ids accept a number (7208609448069) or a Shopify GraphQL id (gid://shopify/Customer/7208609448069). Every call throws if no store is connected or Shopify rejects it.
| Method | What it does |
|---|---|
shop() | The connected store’s domain, or null |
searchProducts({ query?, first? }) | Products matching Shopify search syntax, for example title:*magnesium* or tag:sleep |
getProduct({ id }) | One product, every variant, and the subscription selling plans it offers |
getInventory({ variantId }) | Stock for one variant at each location |
getOrder({ id }) | One order with line items, status, and tracking |
listCustomerOrders({ customerId, first? }) | A customer’s orders, newest first |
getCustomer({ id }) | Contact details, tags, note, order count, total spent |
searchCustomers({ query, first? }) | Customers matching Shopify search syntax, for example email:ana@example.com |
updateCustomer({ id, addTags?, removeTags?, note? }) | Tag a customer or replace their note |
updateOrder({ id, addTags?, removeTags?, note? }) | Tag an order or replace its note |
listSubscriptions({ customerId, first? }) | A customer’s subscription contracts |
getSubscription({ id }) | Status, next billing date, cadence, and lines |
pauseSubscription({ id }) | Pause a subscription |
resumeSubscription({ id }) | Resume a paused subscription |
skipSubscriptionCycle({ id, cycleIndex?, date? }) | Skip the next billing cycle, or the one you name |
cancelSubscription({ id }) | Cancel a subscription |
createDiscountCode({ percentage?, amount?, code?, title?, customerId?, endsAt?, minimumSubtotal? }) | A single-use code. Pass exactly one of percentage or amount |
refundOrder({ orderId, amount?, note?, notify? }) | Refund to the original payment. Leave out amount to refund what is left |
verifyCustomer({ customerId, signature }) | Check a signed Shopify customer id |
createDiscountCode and refundOrder return { status: "pending_approval", approvalId, message } instead of acting when Discounts or Refunds need approval in Controls.
Let an agent use the store
An agent reaches the store through a workflow. Give it a tool that triggers one:
import { workflow } from "@loops/sdk";
type Change = { contractId: string; choice: "skip" | "pause" | "cancel" };
export const changeSubscriptionWorkflow = workflow<Change>({
id: "change-subscription",
execute: async ({ step, event }) => {
const { contractId, choice } = event.body;
return step.do(`Apply ${choice}`, ({ ctx }) => {
if (choice === "skip") return ctx.SHOPIFY.skipSubscriptionCycle({ id: contractId });
if (choice === "pause") return ctx.SHOPIFY.pauseSubscription({ id: contractId });
return ctx.SHOPIFY.cancelSubscription({ id: contractId });
});
},
});See Calling a workflow for the tool. Check who is asking before a tool changes a customer’s orders or subscriptions.
Customer privacy
Shopify sends the app its privacy requests, and Loops handles them:
- Customer data request. Recorded in your audit log so you can answer it.
- Customer erasure. Deletes the user
shopify:<customerId>with their timeline, and clears the input of every run that store events started for that customer. - Shop erasure, 48 hours after uninstall. Deletes the store connection and clears the input of every run a store event started.