The syncing system for the Shopify Connection works by leveraging actions. Each time Gadget goes to sync the connected shop, Gadget creates a new shopifySync record and dispatches the run and either the complete or error actions based on the outcome of the sync.
There are two types of syncs for the Shopify connection: API-triggered or manual.
API-triggered Syncs
An API-triggered sync can be initiated via the API Playground, JS clients, or the api object in any action or route code. There are many ways that you can use API-triggered syncs. The three main ways are after a shop installation, on a model's action, and syncing data within a specified date range.
Do not add API-triggered syncs to the shopifySync model
Avoid adding an API-triggered sync to the shopifySync model's actions! This will cause an infinite loop of syncs. Instead, we recommend
adding API-triggered syncs to the shopifyShop model's install action to sync store data on app installation, or using a custom action
to trigger data syncs.
Sync on shop install
It may be necessary to run an initial sync upon installation. For example, you may want to display all products on a user interface immediately after a merchant installs your app. To do this, use the onSuccess function on the Shopify Shop model's install action with the following code:
You can optionally pass a models parameter to the runSync call to specify which particular Shopify models to sync. For example, if you wish to sync all products in your backend, but not orders, the models should be passed as an array of model API identifiers. If the models parameter is omitted or is an empty array, all Shopify models will be synced.
You may want to limit syncing to recent records. To do this, you can pass the syncSince parameter to indicate which records Gadget should sync. Without this parameter, the API-triggered sync will copy every record to Gadget, regardless of when it was created or updated.
By default, Gadget will check the shopifyUpdatedAt field for each Shopify record, and only sync records created or updated within the specified window.
The timestamp used to sync records can be configured using syncSinceBy:
syncSinceBy: "updated_at": Use shopifyUpdatedAt to sync records updated in the syncSince time window (default).
syncSinceBy: "created_at": Use shopifyCreatedAt to sync records created in the syncSince time window.
You can limit the number of records synced with the syncLast parameter. This will sync only the most recent N records, which is useful when you only need a subset of the latest data rather than all records.
By default, records are ordered by the updated_at timestamp coming from Shopify (the shopifyUpdatedAt field in Gadget), but you can control this with the syncLastBy parameter:
syncLastBy: "updated_at": Use shopifyUpdatedAt to sort records by last updated (default).
syncLastBy: "created_at": Use shopifyCreatedAt to sort records by created date.
Sync last 100 updated records
// in an action file
export const run: ActionRun = async ({ api, params }) => {
const shop = await api.shopifyShop.findOne("123");
await api.shopifySync.run({
domain: shop.domain,
shop: { _link: params.shopId },
// sync only the last 100 products by updated date
syncLast: 100,
syncLastBy: "updated_at", // optional - this is the default
});
};
export const params = {
shopId: { type: "string" },
};
mutation ($shopifySync: RunShopifySyncInput) {
runShopifySync(shopifySync: $shopifySync) {
success
errors {
message
... on InvalidRecordError {
validationErrors {
apiIdentifier
message
}
record
model {
apiIdentifier
}
}
}
shopifySync {
__typename
id
state
createdAt
domain
errorDetails
errorMessage
models
syncSince
syncLast
syncLastBy
updatedAt
}
}
}
// in an action file
const millisecondsPerDay = 1000 * 60 * 60 * 24;
export const run: ActionRun = async ({ api, params }) => {
const shop = await api.shopifyShop.findOne("123");
const syncSince = new Date();
// sync from 30 days ago
syncSince.setTime(syncSince.getTime() - 30 * millisecondsPerDay);
await api.shopifySync.run({
domain: shop.domain,
shop: { _link: params.shopId },
syncSince,
// sync only the last 50 records updated in the past 30 days
syncLast: 50,
syncLastBy: "updated_at",
});
};
export const params = {
shopId: { type: "string" },
};
Forced syncs
A sync will not update any fields or run any actions if the shopifyUpdatedAt value in the payload is less than or equal to the shopifyUpdatedAt value currently stored on the record. There are certain situations, such as Shopify API version upgrades, where you may want to backfill new values or re-run your actions on existing records. In these situations, you can run a sync with the force field set to true. A forced sync will update all values and re-run all actions on applicable records.
You can run a forced sync on an individual store by calling the runSync function on the shopifySync model:
A sync will be queued (without a syncSince set) when clicking the "Sync" button found on the Installs page. This will make a call to the shopifySync model's run Action. The install page can be found via the left nav bar or by navigating to Settings -> Plugins -> Shopify.
This sync may take some time to complete, depending on how much data the shop has.
Sync-only models
When adding a Shopify Connection, you may have opted to connect to Shopify models that do not offer webhooks. Gadget refers to these as sync-only models, meaning that they can only be kept up to date by running a sync on a schedule.
When you add one or more sync-only models to your Gadget database, Gadget will also add a global action, api/actions/scheduledShopifySync.js, that runs once per day to fetch and save data for those models. You can customize this action's schedule by altering its trigger and change how it behaves by editing the code.
A small number of fields on Shopify models are also sync-only. This means that they are not created or updated on webhooks, but are set only during a sync. To handle changes to these fields you can detect the trigger type in your onSuccess function by checking the trigger property in the action context.
api/models/shopifyProduct/update.js
JavaScript
export const onSuccess: ActionOnSuccess = async ({ trigger }) => {
if (trigger.type == "shopify_sync") {
// do something with a sync only field
}
};
export const onSuccess: ActionOnSuccess = async ({ trigger }) => {
if (trigger.type == "shopify_sync") {
// do something with a sync only field
}
};
For models that support webhooks, sync-only fields will initially be null when a record is first created via a webhook. These fields are populated during the nightly reconciliation sync process.
If you need these fields to be populated immediately you should fetch the data yourself using the Shopify API and update the record manually.
To detect changes in sync-only fields and perform actions based on those changes, use the record.changed() method in your action code. For example:
api/models/shopifyProduct/update.js
JavaScript
export const onSuccess: ActionOnSuccess = async ({
api,
record,
params,
logger,
}) => {
if (record.changed("someField")) {
// This code will run when someField is updated during a sync
logger.info(`someField was updated to: ${record.someField}`);
}
};
export const onSuccess: ActionOnSuccess = async ({
api,
record,
params,
logger,
}) => {
if (record.changed("someField")) {
// This code will run when someField is updated during a sync
logger.info(`someField was updated to: ${record.someField}`);
}
};
Coordinates ValidatedFormattedFormatted AreaLatitudeLongitudeTimezoneValidation Result Summary
Shopify Customer
Number Of OrdersPayment MethodsStatisticsMergeableLocaleAmount SpentCan DeleteDisplay NameLegacy Resource IdLifetime DurationMarketProduct Subscriber StatusUnsubscribe UrlValid Email AddressHas Timeline CommentData Sale Opt Out
Shopify Customer Payment Method
Subscription Contracts
Shopify Discount
Applies On SubscriptionApplies On One Time PurchaseRecurring Cycle LimitCodes CountCombines WithDiscount ClassEnds AtStarts AtAsync Usage CountShort SummarySummaryMinimum RequirementHas Timeline CommentTotal SalesDestination SelectionMaximum Shipping PriceTypeCustomer GetsCustomer BuysCodesDiscount IDApp Discount TypeError HistoryUses Per Order LimitApplies Once Per CustomerShareable URLsUsage Limit
Shopify Discount Customer Gets Product
DiscountProduct
Shopify Discount Customer Buys Product
DiscountProduct
Shopify Discount Customer Gets Product Variant
DiscountProduct Variant
Shopify Discount Customer Buys Product Variant
DiscountProduct Variant
Shopify Discount Customer Gets Collection
DiscountCollection
Shopify Discount Customer Buys Collection
DiscountCollection
Shopify Dispute
Amount SetReason Details
Shopify Draft Order
Discount CodesAccept Automatic DiscountsAllow Discount Codes In CheckoutWarningsBilling Address Matches Shipping AddressDefault CursorHas Timeline CommentInvoice Email Template SubjectLegacy Resource IdLine Items Subtotal PriceMarket NameMarket Region Country CodePhonePlatform DiscountsPresentment Currency CodeReadyReserve Inventory UntilSubtotal Price SetTotal Discounts SetTotal Price SetTotal Line Items Price SetTotal Quantity Of Line ItemsTotal Shipping Price SetTotal Tax SetTotal WeightTransformer FingerprintVisible to CustomerPurchasing Entity
Shopify Draft Order Line Item
Approximate Discounted Unit Price SetBundle ComponentsDiscounted Total SetFulfillment Service HandleOriginal Total SetOriginal Unit Price SetOriginal Unit Price With CurrencyUUIDCustom AttributesCustom Attributes V2Weight
Shopify Draft Order Platform Discount
IDDraft OrderAllocationsAutomatic DiscountBxgy DiscountCodediscountClassPresentation LevelShort SummarySummarytitleTotal AmountTotal Amount Price Set
Shopify Draft Order Platform Discount Allocation
IDDraft Order Platform DiscountQuantityReduction AmountReduction Amount SetTarget
Shopify Duty
Price Set
Shopify Fulfillment
Delivered AtDisplay StatusEstimated Delivery AtIn Transit AtRequires ShippingTotal QuantityTracking Info
Shopify Fulfillment Line Item
FulfillmentAdmin GraphQL API ID
Shopify Inventory Item
Duplicate Sku CountLegacy Resource IdTracked EditableUnit CostInventory History UrlMeasurementLocations Count
TransactionsAdditional FeesPurchasing EntityCancellationShopify ProtectPayment TermsRiskRetail LocationFulfillments CountTransactions CountTotal Cash Rounding AdjustmentBusiness EntityBilling Status Matches Shipping AddressAlertsCan Mark As PaidCan Notify CustomerCapturableCart Discount Amount SetClosedCurrent Cart Discount Amount SetCurrent Subtotal Line Items QuantityCurrent Total WeightEditedFulfillableFully PaidHas Timeline CommentLegacy Resource IdMerchant EditableMerchant Editable ErrorsNet Payment SetOriginal Total Price SetRefundableRefund Discrepency SetRequires ShippingRestockableReturn StatusTotal Capturable SetTotal Outstanding SetTotal Received SetTotal Refunded SetTotal Refunded Shipping SetTotal Tip Received SetUnpaidCustomer Journey Summary
Shopify Order Line Item
Discounted Unit Price After All Discounts SetDiscounted Unit Price SetOriginal Total SetUnfulfilled Discounted Total SetUnfulfilled Original Total SetMerchant EditableNon Fulfillable QuantityRefundable QuantityRestockableUnfulfilled Quantity
PlanPlan Public Display NameBilling AddressAlertsCountries In Shipping ZonesCurrency FormatsCustomer AccountsDescriptionOrder Number Format PrefixOrder Number Format SuffixResource LimitsRich Text Editor UrlShips To CountriesTimezone AbbreviationTimezone OffsetTimezone Offset MinutesUnit SystemUrlCustomer Accounts V2
Shopify Selling Plan Group
Shopify Created At
Shopify Selling Plan
CategoryInventory PolicyShopify Created AtIDSelling Plan Group
Shopify Subscription Billing Attempt
Origin TimeCompleted AtNext Action URL
Shopify Subscription Contract
Last Billing Attempt Error TypeDelivery MethodAppApp Admin URLBilling AttemptsCustomer Payment MethodDelivery PriceDiscountsLast Payment StatusLinesNext Billing DateNoteOrdersOrigin OrderShopify Created AtShopify Updated At
Shopify Tender Transaction
Amount Set
Shopify Theme
PrefixProcessing Failed
scheduledShopifySync global action
If you add any sync-only models to your Shopify connection, Gadget will automatically place a global action called scheduledShopifySync in your project's global actions folder.
This action allows you to control how the scheduled sync is run.
It calls on the globalShopifySync function to run the sync process. This function takes in the following parameters for the sync:
apiKeys: the list of Shopify APP API keys to trigger a sync for
syncSince: the start date to start syncing data from, by default: In the action code we create a new Date object representing a specific point in time, const syncSince = new Date(Date.now() - 25 * HourInMs) sets syncSince to a Date object that represents the time exactly 25 hours before the current time. The reason we've set the start of the sync to 25 hours before the current time is as a best practice to leave additional leeway time to avoid any possible errors there might be in the exact timing of the sync.
syncSinceBy: whether to use "created_at" or "updated_at" as a time window when using syncSince (optional, defaults to "updated_at")
syncLast: the maximum number of records to sync per model (optional)
syncLastBy: whether to order by "created_at" or "updated_at" when using syncLast (optional, defaults to "updated_at")
models: the list of model API identifiers to trigger syncs for, by default: We create a syncOnlyModels object that maps through an array of the existing available models with the Shopify connection and filter through identifying for sync-only models
You can also edit the rest of the action to configure it to your preference, by default the action is scheduled to run daily but you can configure the time it's run, head to the scheduler trigger associated with the action and set a time interval of your preference, like below:
If you also want to configure and add additional models to be synced with the sync-only models, you can do so by adding your custom preference in the models param as an array like below:
api/actions/scheduledShopifySync.js
JavaScript
import { globalShopifySync } from "gadget-server";
const HourInMs = 60 * 60 * 1000;
export const run: ActionRun = async ({ params, logger, api, connections }) => {
const syncOnlyModels = connections.shopify.enabledModels
.filter((model) => model.syncOnly)
.map((model) => model.apiIdentifier);
const syncSince = new Date(Date.now() - 24 * HourInMs);
await globalShopifySync({
apiKeys: connections.shopify.apiKeys,
syncSince,
// Pass the list of models to sync as an array of strings
models: [...syncOnlyModels, "shopifyRefund"],
});
};
import { globalShopifySync } from "gadget-server";
const HourInMs = 60 * 60 * 1000;
export const run: ActionRun = async ({ params, logger, api, connections }) => {
const syncOnlyModels = connections.shopify.enabledModels
.filter((model) => model.syncOnly)
.map((model) => model.apiIdentifier);
const syncSince = new Date(Date.now() - 24 * HourInMs);
await globalShopifySync({
apiKeys: connections.shopify.apiKeys,
syncSince,
// Pass the list of models to sync as an array of strings
models: [...syncOnlyModels, "shopifyRefund"],
});
};
To remove a sync-only model from being included in the scheduled sync, head to Settings -> Plugins -> Shopify and click the Edit button to edit your API scopes and simply unselect the model.
Don't remove the `globalShopifySync` call
Removing the globalShopifySync call from this action will stop syncing any data from Shopify, stopping your app from getting any updates.
Removing your Shopify connection will not remove the scheduledShopifySync action file. You must remove that yourself if you no longer need it.
Filtering syncs
Gadget supports filtering which records are synced when you run a sync by only these parameters:
syncSince to sync data changed since a specific date
syncSinceBy to control whether syncSince uses "created_at" or "updated_at"
syncLast to sync only the most recent N records per model
syncLastBy to control whether syncLast orders by "created_at" or "updated_at"
models to sync only specific set of models
apiKeys to sync data for only specific Shopify apps in your Gadget app
Filtering by other parameters is not supported, but you can always modify your model actions to return without saving, or modify the parameters before saving to adjust what data is synced.
Syncs and webhook filters
When a sync is run, it syncs all the enabled fields for the models you have selected, and all rows in the models you have selected. Webhook filters or the includeFields option that apply to webhooks will have no effect on the sync.
Sync error handling
If an error occurs during a sync, the Shopify Sync model's Error action will be called. For example, here's how you teach Gadget to alert you via SMS when a sync fails.
api/models/shopifySync/actions/error.js
JavaScript
import { ActionOptions } from "gadget-server";
import Twilio from "twilio";
// Environment variables set in Gadget
const twilio = Twilio(
process.env["TWILIO_ACCOUNT_SID"],
process.env["TWILIO_ACCOUNT_AUTH_TOKEN"]
);
export const run: ActionRun = async ({ params, logger }) => {
const errors = params.shopifySync?.errorDetails?.split("\n") ?? [];
// send the errors individually to the logger
for (const error of errors) {
logger.error({ error }, "an error occurred syncing");
}
// send an SMS notification
await twilio.messages.create({
to: "+11112223333",
from: "+11112223333",
body: `Shopify sync failed with message: ${params.shopifySync?.errorMessage}`,
});
};
export const options: ActionOptions = {
actionType: "update",
};
import { ActionOptions } from "gadget-server";
import Twilio from "twilio";
// Environment variables set in Gadget
const twilio = Twilio(
process.env["TWILIO_ACCOUNT_SID"],
process.env["TWILIO_ACCOUNT_AUTH_TOKEN"]
);
export const run: ActionRun = async ({ params, logger }) => {
const errors = params.shopifySync?.errorDetails?.split("\n") ?? [];
// send the errors individually to the logger
for (const error of errors) {
logger.error({ error }, "an error occurred syncing");
}
// send an SMS notification
await twilio.messages.create({
to: "+11112223333",
from: "+11112223333",
body: `Shopify sync failed with message: ${params.shopifySync?.errorMessage}`,
});
};
export const options: ActionOptions = {
actionType: "update",
};
id is the Shopify Sync id that was created
errorMessage contains a generate description of the error or errors
errorDetails contains a all error messages (if more than 1 occurred) joined by a \n
The best way to debug why a sync may have failed is to search for errors through your logs. Use the following log query to filter by sync ID (replace {syncId} with ID of the sync that has failed):
If you've accidentally synced the wrong model or perhaps have a long ongoing sync, you do have the ability to abort a running sync.
During an ongoing sync, head over to your shopifySync model.
Select the abort action and then click Run Action
Then invoke your action within the GraphQL playground, and once ran, your sync is then successfully aborted
Adding the abort action to older shopifySync models
You only need to add the abort action to your shopifySync model if it was created before November 17, 2023 and you wish to enable the ability to abort a running sync
To add the abort action, head to your shopifySync model and create a new action called abort
Then paste the following code within the abort action code file