Syncing
The syncing system for the Shopify Connection works by leveraging actions. Each time Gadget goes to sync the connected shop, Gadget creates a new Shopify Sync record and dispatches the Run and either the Complete or Error Actions based on the outcome of the sync.
There are three types of syncs for the Shopify connection: API-triggered, Manual and Scheduled.
API-triggered Syncs
An API-triggered sync can be initiated via the GraphQL API, JS clients, or the api
object in the actions 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.
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:
1// in api/models/shopifyShop/actions/install.ts23export const onSuccess: ActionOnSuccess = async ({ record, api }) => {4 await api.shopifySync.run({5 shopifySync: {6 domain: record.domain,7 shop: {8 _link: record.id,9 },10 },11 });12};
1mutation ($shopifySync: RunShopifySyncInput) {2 runShopifySync(shopifySync: $shopifySync) {3 success4 errors {5 message6 ... on InvalidRecordError {7 validationErrors {8 apiIdentifier9 message10 }11 record12 model {13 apiIdentifier14 }15 }16 }17 shopifySync {18 __typename19 id20 state21 createdAt22 domain23 errorDetails24 errorMessage25 models26 syncSince27 updatedAt28 }29 }30}
{ "shopifySync": { "shop": { "_link": "SHOPID" }, "domain": "SHOPDOMAIN" } }
1// in api/models/shopifyShop/actions/install.ts23export const onSuccess: ActionOnSuccess = async ({ record, api }) => {4 await api.shopifySync.run({5 shopifySync: {6 domain: record.domain,7 shop: {8 _link: record.id,9 },10 },11 });12};
Sync by model
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.
1// in an action file2export const run: ActionRun = async ({ params, logger, api, connections }) => {3 const shop = await api.shopifyShop.findOne("123");4 await api.shopifySync.run({5 shopifySync: {6 domain: shop.domain,7 shop: {8 _link: params.shopId,9 },10 // optional parameter11 models: ["shopifyProduct"],12 },13 });14};1516export const params = {17 shopId: { type: "string" },18};
1mutation ($shopifySync: RunShopifySyncInput) {2 runShopifySync(shopifySync: $shopifySync) {3 success4 errors {5 message6 ... on InvalidRecordError {7 validationErrors {8 apiIdentifier9 message10 }11 record12 model {13 apiIdentifier14 }15 }16 }17 shopifySync {18 __typename19 id20 state21 createdAt22 domain23 errorDetails24 errorMessage25 models26 syncSince27 updatedAt28 }29 }30}
1{2 "shopifySync": {3 "shop": { "_link": "SHOPID" },4 "domain": "SHOPDOMAIN",5 "models": ["shopifyProduct"]6 }7}
1// in an action file2export const run: ActionRun = async ({ params, logger, api, connections }) => {3 const shop = await api.shopifyShop.findOne("123");4 await api.shopifySync.run({5 shopifySync: {6 domain: shop.domain,7 shop: {8 _link: params.shopId,9 },10 // optional parameter11 models: ["shopifyProduct"],12 },13 });14};1516export const params = {17 shopId: { type: "string" },18};
Sync by date-time
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. Gadget will check the shopifyUpdateAt
field for each Shopify record, and only sync records created or updated within the specified window. Without this parameter, the API-triggered sync will copy every record to Gadget, regardless of when it was created or updated.
1// in an action file2const millisecondsPerDay = 1000 * 60 * 60 * 24;34export const run: ActionRun = async ({ api, params }) => {5 const shop = await api.shopifyShop.findOne("123");6 const syncSince = new Date();7 // sync from 5 days ago8 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);9 await api.shopifySync.run({10 shopifySync: {11 domain: shop.domain,12 // optional parameter13 syncSince,14 shop: { _link: params.shopId },15 },16 });17};1819export const params = {20 shopId: { type: "string" },21};
1mutation ($shopifySync: RunShopifySyncInput) {2 runShopifySync(shopifySync: $shopifySync) {3 success4 errors {5 message6 ... on InvalidRecordError {7 validationErrors {8 apiIdentifier9 message10 }11 record12 model {13 apiIdentifier14 }15 }16 }17 shopifySync {18 __typename19 id20 state21 createdAt22 domain23 errorDetails24 errorMessage25 models26 syncSince27 updatedAt28 }29 }30}
1{2 "shopifySync": {3 "shop": { "_link": "SHOPID" },4 "domain": "SHOPDOMAIN",5 "syncSince": "ISOSTRING"6 }7}
1// in an action file2const millisecondsPerDay = 1000 * 60 * 60 * 24;34export const run: ActionRun = async ({ api, params }) => {5 const shop = await api.shopifyShop.findOne("123");6 const syncSince = new Date();7 // sync from 5 days ago8 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);9 await api.shopifySync.run({10 shopifySync: {11 domain: shop.domain,12 // optional parameter13 syncSince,14 shop: { _link: params.shopId },15 },16 });17};1819export const params = {20 shopId: { type: "string" },21};
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.
For applications on framework versions prior to framework v0.3.1 after connecting to Shopify, a globalShopifySync
global action has been added to your Gadget app. This global action runs a forced sync across all stores your app has been installed on.
If you go to Global Actions, then select the globalShopifySync action and click Run Action, you will be brought to the API Playground with the globalShopifySync
mutation loaded for you. To run a forced sync, you need to include "force": true
in your variables, as well as the Client ID (aka API Key) of your connected Shopify app. You can also include syncSince and models variables to limit the scope of the sync.
Variables for the scheduledShopifySync mutationjson{"apiKeys": ["<your Shopify app's API key>"],"force": true}
You can also run a forced sync on an individual store by calling the runSync
function on the shopifySync
model:
1// in an action file2const millisecondsPerDay = 1000 * 60 * 60 * 24;34export const run: ActionRun = async ({ api, params }) => {5 const shop = await api.shopifyShop.findOne("123");6 const syncSince = new Date();7 // sync from 5 days ago8 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);9 await api.shopifySync.run({10 shopifySync: {11 domain: shop.domain,12 // optional parameter13 syncSince,14 shop: {15 _link: shop.id,16 },17 force: true,18 },19 });20};2122export const params = {23 shopId: { type: "string" },24};
1mutation ($shopifySync: RunShopifySyncInput) {2 runShopifySync(shopifySync: $shopifySync) {3 success4 errors {5 message6 ... on InvalidRecordError {7 validationErrors {8 apiIdentifier9 message10 }11 record12 model {13 apiIdentifier14 }15 }16 }17 shopifySync {18 __typename19 id20 state21 createdAt22 domain23 errorDetails24 errorMessage25 models26 syncSince27 updatedAt28 }29 }30}
1{2 "shopifySync": {3 "shop": { "_link": "SHOPID" },4 "domain": "SHOPDOMAIN",5 "syncSince": "ISOSTRING",6 "force": true7 }8}
1// in an action file2const millisecondsPerDay = 1000 * 60 * 60 * 24;34export const run: ActionRun = async ({ api, params }) => {5 const shop = await api.shopifyShop.findOne("123");6 const syncSince = new Date();7 // sync from 5 days ago8 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);9 await api.shopifySync.run({10 shopifySync: {11 domain: shop.domain,12 // optional parameter13 syncSince,14 shop: {15 _link: shop.id,16 },17 force: true,18 },19 });20};2122export const params = {23 shopId: { type: "string" },24};
Manual syncs
A sync will be queued (without a syncSince
set) when clicking the "Sync" button next to your Shopify connection. This will make a call to the Shopify Sync model's Run Action.
This sync may take some time to complete, depending on how much data the shop has.
Scheduled syncs
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.
Once the sync runs Gadget will automatically fetch all recent data on a schedule in the background as defined within the scheduler trigger within the scheduledShopifySync
global action. If the sync fails or has scheduling issues, there's a possibility you may not have the most up-to-date data in Gadget. If this does occur, check the Shopify Sync data viewer or logs via the Sync History to identify any errors.
For a full list of sync-only models operating on scheduled syncs read about it here.
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)
setssyncSince
to aDate
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. - 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
1import { globalShopifySync } from "gadget-server";2const HourInMs = 60 * 60 * 1000;34export const run: ActionRun = async ({ params, logger, api, connections }) => {5 const syncOnlyModels = connections.shopify.enabledModels6 .filter((model) => model.syncOnly)7 .map((model) => model.apiIdentifier);89 const syncSince = new Date(Date.now() - 24 * HourInMs);1011 await globalShopifySync({12 apiKeys: connections.shopify.apiKeys,13 syncSince,14 models: syncOnlyModels,15 });16};
1import { globalShopifySync } from "gadget-server";2const HourInMs = 60 * 60 * 1000;34export const run: ActionRun = async ({ params, logger, api, connections }) => {5 const syncOnlyModels = connections.shopify.enabledModels6 .filter((model) => model.syncOnly)7 .map((model) => model.apiIdentifier);89 const syncSince = new Date(Date.now() - 24 * HourInMs);1011 await globalShopifySync({12 apiKeys: connections.shopify.apiKeys,13 syncSince,14 models: syncOnlyModels,15 });16};
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:
1import { globalShopifySync } from "gadget-server";2const HourInMs = 60 * 60 * 1000;34export const run: ActionRun = async ({ params, logger, api, connections }) => {5 const syncOnlyModels = connections.shopify.enabledModels6 .filter((model) => model.syncOnly)7 .map((model) => model.apiIdentifier);89 const syncSince = new Date(Date.now() - 24 * HourInMs);1011 await globalShopifySync({12 apiKeys: connections.shopify.apiKeys,13 syncSince,1415 // Pass the list of models to sync as an array of strings16 models: [...syncOnlyModels, "shopifyRefund"],17 });18};
1import { globalShopifySync } from "gadget-server";2const HourInMs = 60 * 60 * 1000;34export const run: ActionRun = async ({ params, logger, api, connections }) => {5 const syncOnlyModels = connections.shopify.enabledModels6 .filter((model) => model.syncOnly)7 .map((model) => model.apiIdentifier);89 const syncSince = new Date(Date.now() - 24 * HourInMs);1011 await globalShopifySync({12 apiKeys: connections.shopify.apiKeys,13 syncSince,1415 // Pass the list of models to sync as an array of strings16 models: [...syncOnlyModels, "shopifyRefund"],17 });18};
To remove a sync-only model from being included in the scheduled sync, head back to the Shopify connection's settings page to edit your API scopes and simply unselect the model.
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.
Applications on frameworks prior to v0.3.1
If your application is on a framework version before v0.3.1 the global action added to your app upon connecting to Shopify is known as the globalShopifySync
which operates very similarly to scheduledShopifySync
.
If users upgrade their applications framework version to v0.3.1 they will receive the scheduledShopifySync
global action but you will also have your globalShopifySync
action as well, but feel free to remove it or move any existing custom code to the scheduledShopifySync
action as it is now redundant in use after upgrading.
Sync-only models
Several Shopify models are sync-only. This means they are not created or updated through webhooks and are updated exclusively during a sync operation that is scheduled or manually triggered.
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.
For more information on models that are currently sync only refer to our available Shopify models for details here
Sync-only fields
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.
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:
1export const onSuccess: ActionOnSuccess = async ({2 api,3 record,4 params,5 logger,6}) => {7 if (record.changed("someField")) {8 // This code will run when someField is updated during a sync9 logger.info(`someField was updated to: ${record.someField}`);10 }11};
1export const onSuccess: ActionOnSuccess = async ({2 api,3 record,4 params,5 logger,6}) => {7 if (record.changed("someField")) {8 // This code will run when someField is updated during a sync9 logger.info(`someField was updated to: ${record.someField}`);10 }11};
The current list of sync only fields are:
Model Name | Fields |
---|---|
Shopify Customer | Data Sale Opt Out Locale Mergeable Statistics |
Shopify Draft Order | Discount Codes Accept Automatic Discounts Allow Discount Codes In Checkout Warnings Subtotal Price Set Total Price Set Total Tax Set Purchasing Entity |
Shopify Fulfillment Service | Service Name Type |
Shopify Inventory Item | Measurement Inventory History Url Duplicate Sku Count Legacy Resource Id Tracked Editable Unit Cost |
Shopify Inventory Level | Can Deactivate Deactivation Alert |
Shopify Order | Total Cash Rounding Adjustment Status Page URL Business Entity Retail Location Fulfillments Count Transactions Count Risk Cancellation Shopify Protect Additional Fees Purchasing Entity |
Shopify Order Transaction | Multicapturable |
Shopify Product | Has Variants That Requires Components Product Category Compare At Price Range |
Shopify Product Variant | Selected Options |
Shopify Shop | Customer Accounts V2 |
Shopify Subscription Contract | Last Billing Attempt Error Type App Admin URL Customer Payment Method Delivery Method Delivery Price Last Payment Status Next Billing Date Note Origin Order Shopify Created At Shopify Updated At Status |
Shopify Business Entity | Company Name Display Name Primary Address |
Shopify Payments Account | Balance Country Default Currency Onboardable Payout Statement Descriptor |
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.
1import { ActionOptions } from "gadget-server";2import Twilio from "twilio";34// Environment variables set in Gadget5const twilio = Twilio(6 process.env["TWILIO_ACCOUNT_SID"],7 process.env["TWILIO_ACCOUNT_AUTH_TOKEN"]8);910export const run: ActionRun = async ({ params, logger }) => {11 const errors = params.shopifySync?.errorDetails?.split("\n") ?? [];1213 // send the errors individually to the logger14 for (const error of errors) {15 logger.error({ error }, "an error occurred syncing");16 }1718 // send an SMS notification19 await twilio.messages.create({20 to: "+11112223333",21 from: "+11112223333",22 body: `Shopify sync failed with message: ${params.shopifySync?.errorMessage}`,23 });24};2526export const options: ActionOptions = {27 actionType: "update",28};
1import { ActionOptions } from "gadget-server";2import Twilio from "twilio";34// Environment variables set in Gadget5const twilio = Twilio(6 process.env["TWILIO_ACCOUNT_SID"],7 process.env["TWILIO_ACCOUNT_AUTH_TOKEN"]8);910export const run: ActionRun = async ({ params, logger }) => {11 const errors = params.shopifySync?.errorDetails?.split("\n") ?? [];1213 // send the errors individually to the logger14 for (const error of errors) {15 logger.error({ error }, "an error occurred syncing");16 }1718 // send an SMS notification19 await twilio.messages.create({20 to: "+11112223333",21 from: "+11112223333",22 body: `Shopify sync failed with message: ${params.shopifySync?.errorMessage}`,23 });24};2526export const options: ActionOptions = {27 actionType: "update",28};
id
is the Shopify Sync id that was createderrorMessage
contains a generate description of the error or errorserrorDetails
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):
{app_logs_service=~".+", environment_id="{environmentID}"} | json | connectionSyncId = "{syncID}"
Abort a running sync
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 abortThen paste the following code within the
abort
action code file
1import {2 transitionState,3 applyParams,4 save,5 ActionOptions,6 ShopifySyncState,7} from "gadget-server";8import { preventCrossShopDataAccess, abortSync } from "gadget-server/shopify";910export const run: ActionRun = async ({11 params,12 record,13 logger,14 api,15 connections,16}) => {17 transitionState(record, {18 from: ShopifySyncState.Running,19 to: ShopifySyncState.Errored,20 });21 applyParams(params, record);22 await preventCrossShopDataAccess(params, record);23 await abortSync(params, record);24 await save(record);25};2627export const onSuccess: ActionOnSuccess = async ({28 params,29 record,30 logger,31 api,32 connections,33}) => {34 // Your logic goes here35};3637export const options: ActionOptions = {38 actionType: "update",39};
1import {2 transitionState,3 applyParams,4 save,5 ActionOptions,6 ShopifySyncState,7} from "gadget-server";8import { preventCrossShopDataAccess, abortSync } from "gadget-server/shopify";910export const run: ActionRun = async ({11 params,12 record,13 logger,14 api,15 connections,16}) => {17 transitionState(record, {18 from: ShopifySyncState.Running,19 to: ShopifySyncState.Errored,20 });21 applyParams(params, record);22 await preventCrossShopDataAccess(params, record);23 await abortSync(params, record);24 await save(record);25};2627export const onSuccess: ActionOnSuccess = async ({28 params,29 record,30 logger,31 api,32 connections,33}) => {34 // Your logic goes here35};3637export const options: ActionOptions = {38 actionType: "update",39};
- Now that you've successfully added the action you can abort a running sync and can follow the steps above to do so.