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

Daily syncs 

The Shopify connection will automatically fetch all recent data in the background daily. This ensures we have all of the shop's data in the case of a missing webhook or for resources that don't have webhooks (such as the Online Store content). This sync only fetches data from the past day, so 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.

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:

1export async function onSuccess({ api, record, params, logger, connections }) {
2 await api.shopifySync.run({
3 shopifySync: {
4 domain: record.domain,
5 shop: {
6 _link: record.id,
7 },
8 },
9 });
10}
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" } }

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.

1export async function onSuccess({ api, record, params, logger, connections }) {
2 if (record.shopId) {
3 const shop = await api.shopifyShop.findOne(record.shopId);
4 await api.shopifySync.run({
5 shopifySync: {
6 domain: shop.domain,
7 shop: {
8 _link: record.shopId,
9 },
10 // optional parameter
11 models: ["shopifyProduct"],
12 },
13 });
14 }
15}
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}

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.

1export async function onSuccess({ api, record, params, logger, connections }) {
2 if (record.shopId) {
3 const shop = await api.shopifyShop.findOne(record.shopId);
4 const syncSince = new Date();
5 const millisecondsPerDay = 1000 * 60 * 60 * 24;
6 // sync from 5 days ago
7 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);
8 await api.shopifySync.run({
9 shopifySync: {
10 domain: shop.domain,
11 // optional parameter
12 syncSince,
13 shop: {
14 _link: record.shopId,
15 },
16 },
17 });
18 }
19}
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}

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.

After connecting to Shopify, a globalShopifySync global action has been added to your Gadget app. You can use this global action to run 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 API Key/Client Id of your connected Shopify app. You can also include syncSince and models variables to limit the scope of the sync.

Variables for the globalShopifySync mutation
json
{
"apiKeys": ["<your Shopify app's API key>"],
"force": true
}

You can also run a forced sync on an individual store in JS or GraphQL by writing a custom action:

1export async function run({ api, record, params, logger, connections }) {
2 if (record.shopId) {
3 const shop = await api.shopifyShop.findOne(record.shopId);
4 const syncSince = new Date();
5 const millisecondsPerDay = 1000 * 60 * 60 * 24;
6 // sync from 5 days ago
7 syncSince.setTime(syncSince.getTime() - 5 * millisecondsPerDay);
8 await api.shopifySync.run({
9 shopifySync: {
10 domain: shop.domain,
11 // optional parameter
12 syncSince,
13 shop: {
14 _link: record.shopId,
15 },
16 force: true,
17 },
18 });
19 }
20}
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}

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.

Sync only fields 

A small number of fields on Shopify models are 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.

JavaScript
1export async function onSuccess({
2 api,
3 record,
4 params,
5 logger,
6 connections,
7 trigger,
8}) {
9 if (trigger.type == "shopify_sync") {
10 // do something with a sync only field
11 }
12}

The current list of sync only fields are:

Model NameFields
Shopify CustomerMergeableStatistics
Shopify Draft OrderPurchasing Entity
Shopify OrderAdditional FeesPurchasing Entity
Shopify ProductProduct Category

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.

JavaScript
1const Twilio = require("twilio");
2// Environment variables set in Gadget
3const twilio = new Twilio(
4 process.env["TWILIO_ACCOUNT_SID"],
5 process.env["TWILIO_ACCOUNT_AUTH_TOKEN"]
6);
7
8export async function run({ api, record, params, logger, connections }) {
9 /*
10 params: {
11 id: string;
12 shopifySync: {
13 errorMessage: string;
14 errorDetails: string;
15 }
16 }
17 */
18
19 const errors = params.shopifySync.errorDetails.split("\n");
20
21 // send the errors individually to the logger
22 for (const error of errors) {
23 logger.error({ error }, "an error occurred syncing");
24 }
25
26 // send an SMS notification
27 await twilio.sendSMS({
28 to: "+11112223333",
29 from: "+11112223333",
30 body: `Shopify sync failed with message: ${params.shopifySync.errorMessage}`,
31 });
32}
  • 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}"