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.

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:

1// in api/models/shopifyShop/actions/install.ts
2
3export 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 success
4 errors {
5 message
6 ... on InvalidRecordError {
7 validationErrors {
8 apiIdentifier
9 message
10 }
11 record
12 model {
13 apiIdentifier
14 }
15 }
16 }
17 shopifySync {
18 __typename
19 id
20 state
21 createdAt
22 domain
23 errorDetails
24 errorMessage
25 models
26 syncSince
27 updatedAt
28 }
29 }
30}
Variables
json
{ "shopifySync": { "shop": { "_link": "SHOPID" }, "domain": "SHOPDOMAIN" } }
1// in api/models/shopifyShop/actions/install.ts
2
3export 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 file
2export 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 parameter
11 models: ["shopifyProduct"],
12 },
13 });
14};
15
16export const params = {
17 shopId: { type: "string" },
18};
1mutation ($shopifySync: RunShopifySyncInput) {
2 runShopifySync(shopifySync: $shopifySync) {
3 success
4 errors {
5 message
6 ... on InvalidRecordError {
7 validationErrors {
8 apiIdentifier
9 message
10 }
11 record
12 model {
13 apiIdentifier
14 }
15 }
16 }
17 shopifySync {
18 __typename
19 id
20 state
21 createdAt
22 domain
23 errorDetails
24 errorMessage
25 models
26 syncSince
27 updatedAt
28 }
29 }
30}
Variables
json
1{
2 "shopifySync": {
3 "shop": { "_link": "SHOPID" },
4 "domain": "SHOPDOMAIN",
5 "models": ["shopifyProduct"]
6 }
7}
1// in an action file
2export 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 parameter
11 models: ["shopifyProduct"],
12 },
13 });
14};
15
16export 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 file
2const millisecondsPerDay = 1000 * 60 * 60 * 24;
3
4export const run: ActionRun = async ({ api, params }) => {
5 const shop = await api.shopifyShop.findOne("123");
6 const syncSince = new Date();
7 // sync from 5 days ago
8 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);
9 await api.shopifySync.run({
10 shopifySync: {
11 domain: shop.domain,
12 // optional parameter
13 syncSince,
14 shop: { _link: params.shopId },
15 },
16 });
17};
18
19export const params = {
20 shopId: { type: "string" },
21};
1mutation ($shopifySync: RunShopifySyncInput) {
2 runShopifySync(shopifySync: $shopifySync) {
3 success
4 errors {
5 message
6 ... on InvalidRecordError {
7 validationErrors {
8 apiIdentifier
9 message
10 }
11 record
12 model {
13 apiIdentifier
14 }
15 }
16 }
17 shopifySync {
18 __typename
19 id
20 state
21 createdAt
22 domain
23 errorDetails
24 errorMessage
25 models
26 syncSince
27 updatedAt
28 }
29 }
30}
Variables
json
1{
2 "shopifySync": {
3 "shop": { "_link": "SHOPID" },
4 "domain": "SHOPDOMAIN",
5 "syncSince": "ISOSTRING"
6 }
7}
1// in an action file
2const millisecondsPerDay = 1000 * 60 * 60 * 24;
3
4export const run: ActionRun = async ({ api, params }) => {
5 const shop = await api.shopifyShop.findOne("123");
6 const syncSince = new Date();
7 // sync from 5 days ago
8 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);
9 await api.shopifySync.run({
10 shopifySync: {
11 domain: shop.domain,
12 // optional parameter
13 syncSince,
14 shop: { _link: params.shopId },
15 },
16 });
17};
18
19export 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 mutation
json
{
"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 file
2const millisecondsPerDay = 1000 * 60 * 60 * 24;
3
4export const run: ActionRun = async ({ api, params }) => {
5 const shop = await api.shopifyShop.findOne("123");
6 const syncSince = new Date();
7 // sync from 5 days ago
8 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);
9 await api.shopifySync.run({
10 shopifySync: {
11 domain: shop.domain,
12 // optional parameter
13 syncSince,
14 shop: {
15 _link: shop.id,
16 },
17 force: true,
18 },
19 });
20};
21
22export const params = {
23 shopId: { type: "string" },
24};
1mutation ($shopifySync: RunShopifySyncInput) {
2 runShopifySync(shopifySync: $shopifySync) {
3 success
4 errors {
5 message
6 ... on InvalidRecordError {
7 validationErrors {
8 apiIdentifier
9 message
10 }
11 record
12 model {
13 apiIdentifier
14 }
15 }
16 }
17 shopifySync {
18 __typename
19 id
20 state
21 createdAt
22 domain
23 errorDetails
24 errorMessage
25 models
26 syncSince
27 updatedAt
28 }
29 }
30}
Variables
json
1{
2 "shopifySync": {
3 "shop": { "_link": "SHOPID" },
4 "domain": "SHOPDOMAIN",
5 "syncSince": "ISOSTRING",
6 "force": true
7 }
8}
1// in an action file
2const millisecondsPerDay = 1000 * 60 * 60 * 24;
3
4export const run: ActionRun = async ({ api, params }) => {
5 const shop = await api.shopifyShop.findOne("123");
6 const syncSince = new Date();
7 // sync from 5 days ago
8 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);
9 await api.shopifySync.run({
10 shopifySync: {
11 domain: shop.domain,
12 // optional parameter
13 syncSince,
14 shop: {
15 _link: shop.id,
16 },
17 force: true,
18 },
19 });
20};
21
22export 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.

The connections index page having performed a single sync successfully

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) 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.
  • 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
api/actions/scheduledShopifySync.js
JavaScript
1import { globalShopifySync } from "gadget-server";
2const HourInMs = 60 * 60 * 1000;
3
4export const run: ActionRun = async ({ params, logger, api, connections }) => {
5 const syncOnlyModels = connections.shopify.enabledModels
6 .filter((model) => model.syncOnly)
7 .map((model) => model.apiIdentifier);
8
9 const syncSince = new Date(Date.now() - 24 * HourInMs);
10
11 await globalShopifySync({
12 apiKeys: connections.shopify.apiKeys,
13 syncSince,
14 models: syncOnlyModels,
15 });
16};
1import { globalShopifySync } from "gadget-server";
2const HourInMs = 60 * 60 * 1000;
3
4export const run: ActionRun = async ({ params, logger, api, connections }) => {
5 const syncOnlyModels = connections.shopify.enabledModels
6 .filter((model) => model.syncOnly)
7 .map((model) => model.apiIdentifier);
8
9 const syncSince = new Date(Date.now() - 24 * HourInMs);
10
11 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:

Adjusting the scheduler trigger to run the Scheduled Shopify sync on different times

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
1import { globalShopifySync } from "gadget-server";
2const HourInMs = 60 * 60 * 1000;
3
4export const run: ActionRun = async ({ params, logger, api, connections }) => {
5 const syncOnlyModels = connections.shopify.enabledModels
6 .filter((model) => model.syncOnly)
7 .map((model) => model.apiIdentifier);
8
9 const syncSince = new Date(Date.now() - 24 * HourInMs);
10
11 await globalShopifySync({
12 apiKeys: connections.shopify.apiKeys,
13 syncSince,
14
15 // Pass the list of models to sync as an array of strings
16 models: [...syncOnlyModels, "shopifyRefund"],
17 });
18};
1import { globalShopifySync } from "gadget-server";
2const HourInMs = 60 * 60 * 1000;
3
4export const run: ActionRun = async ({ params, logger, api, connections }) => {
5 const syncOnlyModels = connections.shopify.enabledModels
6 .filter((model) => model.syncOnly)
7 .map((model) => model.apiIdentifier);
8
9 const syncSince = new Date(Date.now() - 24 * HourInMs);
10
11 await globalShopifySync({
12 apiKeys: connections.shopify.apiKeys,
13 syncSince,
14
15 // Pass the list of models to sync as an array of strings
16 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.

Remove sync only model from scopes

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.

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.

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
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 sync
9 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 sync
9 logger.info(`someField was updated to: ${record.someField}`);
10 }
11};

The current list of sync only fields are:

Model NameFields
Shopify CustomerData Sale Opt OutLocaleMergeableStatistics
Shopify Draft OrderDiscount CodesAccept Automatic DiscountsAllow Discount Codes In CheckoutWarningsSubtotal Price SetTotal Price SetTotal Tax SetPurchasing Entity
Shopify Fulfillment ServiceService NameType
Shopify Inventory ItemMeasurementInventory History UrlDuplicate Sku CountLegacy Resource IdTracked EditableUnit Cost
Shopify Inventory LevelCan DeactivateDeactivation Alert
Shopify OrderTotal Cash Rounding AdjustmentStatus Page URLBusiness EntityRetail LocationFulfillments CountTransactions CountRiskCancellationShopify ProtectAdditional FeesPurchasing Entity
Shopify Order TransactionMulticapturable
Shopify ProductHas Variants That Requires ComponentsProduct CategoryCompare At Price Range
Shopify Product VariantSelected Options
Shopify ShopCustomer Accounts V2
Shopify Subscription ContractLast Billing Attempt Error TypeApp Admin URLCustomer Payment MethodDelivery MethodDelivery PriceLast Payment StatusNext Billing DateNoteOrigin OrderShopify Created AtShopify Updated AtStatus
Shopify Business EntityCompany NameDisplay NamePrimaryAddress
Shopify Payments AccountBalanceCountryDefault CurrencyOnboardablePayout 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.

api/models/shopifySync/actions/error.js
JavaScript
1import { ActionOptions } from "gadget-server";
2import Twilio from "twilio";
3
4// Environment variables set in Gadget
5const twilio = Twilio(
6 process.env["TWILIO_ACCOUNT_SID"],
7 process.env["TWILIO_ACCOUNT_AUTH_TOKEN"]
8);
9
10export const run: ActionRun = async ({ params, logger }) => {
11 const errors = params.shopifySync?.errorDetails?.split("\n") ?? [];
12
13 // send the errors individually to the logger
14 for (const error of errors) {
15 logger.error({ error }, "an error occurred syncing");
16 }
17
18 // send an SMS notification
19 await twilio.messages.create({
20 to: "+11112223333",
21 from: "+11112223333",
22 body: `Shopify sync failed with message: ${params.shopifySync?.errorMessage}`,
23 });
24};
25
26export const options: ActionOptions = {
27 actionType: "update",
28};
1import { ActionOptions } from "gadget-server";
2import Twilio from "twilio";
3
4// Environment variables set in Gadget
5const twilio = Twilio(
6 process.env["TWILIO_ACCOUNT_SID"],
7 process.env["TWILIO_ACCOUNT_AUTH_TOKEN"]
8);
9
10export const run: ActionRun = async ({ params, logger }) => {
11 const errors = params.shopifySync?.errorDetails?.split("\n") ?? [];
12
13 // send the errors individually to the logger
14 for (const error of errors) {
15 logger.error({ error }, "an error occurred syncing");
16 }
17
18 // send an SMS notification
19 await twilio.messages.create({
20 to: "+11112223333",
21 from: "+11112223333",
22 body: `Shopify sync failed with message: ${params.shopifySync?.errorMessage}`,
23 });
24};
25
26export const options: ActionOptions = {
27 actionType: "update",
28};
  • 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):

{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.

  1. During an ongoing sync, head over to your shopifySync model.

  2. Select the abort action and then click Run Action

view of the abort action in shopify sync model
  1. Then invoke your action within the GraphQL playground, and once ran, your sync is then successfully aborted
Execute the abort action in the gql playground

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

  1. To add the abort action, head to your shopifySync model and create a new action called abort

  2. Then paste the following code within the abort action code file

api/models/shopifySync/actions/abort.js
JavaScript
1import {
2 transitionState,
3 applyParams,
4 save,
5 ActionOptions,
6 ShopifySyncState,
7} from "gadget-server";
8import { preventCrossShopDataAccess, abortSync } from "gadget-server/shopify";
9
10export 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};
26
27export const onSuccess: ActionOnSuccess = async ({
28 params,
29 record,
30 logger,
31 api,
32 connections,
33}) => {
34 // Your logic goes here
35};
36
37export 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";
9
10export 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};
26
27export const onSuccess: ActionOnSuccess = async ({
28 params,
29 record,
30 logger,
31 api,
32 connections,
33}) => {
34 // Your logic goes here
35};
36
37export const options: ActionOptions = {
38 actionType: "update",
39};
  1. Now that you've successfully added the action you can abort a running sync and can follow the steps above to do so.

Was this page helpful?