Use Gadget to build an automated product tagging app for a Shopify store 

Topics covered: Shopify connections, Building models, Actions, React frontends
Time to build: ~20 minutes

A Shopify merchant needs an automated way to tag products being added to their Shopify store inventory. They source hundreds of products weekly from various dropshippers and upload the unstructured data to Shopify programmatically. Because the data is unstructured, Shopify is unable to power the merchant's storefront search. While the merchant can add tags inside the Shopify Admin, the experience of doing this on hundreds of products weekly is time-consuming.

To solve this, the merchant wants to build a custom Shopify app on Gadget that will run every new product description through an automated tagging script.

Screenshot of the completed product tagger app embedded in a Shopify store admin

In this example, we'll build a custom product tagging app that listens to the product/create and product/update webhooks from Shopify, runs product descriptions through a tagging script, and sets tags back in Shopify.

Requirements

To get the most out of this tutorial, you will need:

You can fork this Gadget project and try it out yourself.

Fork on Gadget

Step 1: Create a Gadget app and connect to Shopify 

Our first step will be to set up a Gadget project and connect our backend to a Shopify store via the Shopify connection.

Create a new Gadget application at gadget.new and select the Shopify app template.

Because we are adding an embedded frontend, we are going to build an app using the Partners connection.

Now we will set up a custom Shopify application in the Partners dashboard.

  1. Go to the Shopify Partner dashboard
  2. Click on the link to the Apps page

Both the Shopify store Admin and the Shopify Partner Dashboard have an Apps section. Ensure that you are on the Shopify Partner Dashboard before continuing.

Click on Apps link in Shopify Partners Dashboard
  1. Click the Create App button
Click on Create app button
  1. Click the Create app manually button and enter a name for your Shopify app
Shopify's app creation landing page in the Partners Dashboard
  1. Click on Settings in the side nav bar
  2. Click on Plugins in the modal that opens
  3. Select Shopify from the list of plugins and connections
The Gadget homescreen, with the Connections link highlighted
  1. Copy the Client ID and Client secret from your newly created Shopify app and paste the values into the Gadget Connections page
Screenshot of the Partners card selected on the Connections page
  1. Click Connect on the Gadget Connections page to move to scope and model selection

Now we get to select what Shopify scopes we give our application access to, while also picking what Shopify data models we want to import into our Gadget app.

  1. Select the scopes and models listed below and click Confirm to connect to the custom Shopify app
  • Enable the read and write scopes for the Shopify Products API, and select the underlying Product model
Select Product API scope + model

Now we want to connect our Gadget app to our custom app in the Partner dashboard.

  1. In your Shopify app in the Partner dashboard, click on Configuration in the side nav bar so you can edit the App URL and Allowed redirection URL(s) fields
  2. Copy the App URL and Allowed redirection URL from the Gadget Connections page and paste them into your custom Shopify App
Screenshot of the connected app, with the App URL and Allowed redirection URL(s) fields

Now we need to install our Shopify app on a store.

  1. Go back to the Shopify Partner dashboard
  2. Click on Apps to go to the Apps page again
  3. Click on your custom app
  4. Click on Select store
Click on the Select store button
  1. Click on the store we want to use to develop our app
  2. You may be prompted about Store transfer being disabled. This is okay, click Install anyway
  3. Click Install app to install your Gadget app on your Shopify store
Having an issue installing?

If you are getting a permissions denied error when installing your app, try logging in to the Shopify store Admin!

Click Install app to authorize our Gadget app with our store

You will be redirected to an embedded admin app that has been generated for you. The code for this app template can be found in web/routes/index.tsx.

Step 2: Add new model for tag keywords 

The next step is to create a model that will store our list of vetted keywords that we can use to power our tagging script. These keywords can be different types of products or brands. Make sure to add keywords that will be found in your products' descriptions!

  1. Click + next to the api/models folder in the nav to add a model, and call it allowedTag
  2. Click + in the FIELDS section of the api/models/allowedTag/schema page to add a field, and name it keyword
The allowedTag model with keyword field

Gadget instantly creates a new table and column in the underlying database and generates a GraphQL CRUD API for this model. Test it out the API in the API Playground!

  1. Click on api/models/allowedTag/actions/create.ts to open the allowedTag model's create action
  2. Click the Run action button in the TRIGGERS panel on the right of the editor to open up the create action in the API Playground

Using the API Playground, we can make a create call to our allowedTag model to store a new keyword. The JS action is already set up to take a keyword as a parameter.

  1. Enter a keyword to store in your database and run the action

We can also check to make sure our tag keywords have been saved.

  1. Navigate to api/models/allowedTag/data to go to this model's data page

We can see our added allowedTag record!

The allowedTag Data page with our added keyword

Step 3: Build your tagging script 

Gadget auto-generates a CRUD (create, read, update, delete) API for each of your models. For Shopify models, these create, update, and delete actions are triggered by Shopify webhooks.

Actions are customizable, and you can add code to the run and onSuccess functions. Read more about these functions in the Gadget actions docs.

Next, as add some code to the create action for the shopifyProduct model to tag products based on the keywords we have stored in the allowedTag model.

  1. Navigate to api/models/shopifyProduct/actions/create.ts
  2. Paste the following code into the file:
api/models/shopifyProduct/actions/create.ts
TypeScript
1import {
2 applyParams,
3 preventCrossShopDataAccess,
4 save,
5 ActionOptions,
6} from "gadget-server";
7
8export const run: ActionRun = async ({ params, record }) => {
9 applyParams(params, record);
10 await preventCrossShopDataAccess(params, record);
11 await save(record);
12};
13
14export const onSuccess: ActionOnSuccess = async ({
15 record,
16 logger,
17 api,
18 connections,
19}) => {
20 if (record.body && record.changed("body")) {
21 // get a unique list of words used in the record's description
22 let newTags = [...new Set(record.body.match(/\w+(?:'\w+)*/g))];
23
24 // filter down to only those words which are allowed
25 const allowedTags = (await api.allowedTag.findMany()).map((tag) => tag.keyword);
26 // merge with any existing tags and use Set to remove duplicates
27 const finalTags = [
28 ...new Set(
29 newTags
30 .filter((tag) => allowedTags.includes(tag))
31 .concat(record.tags as string[])
32 ),
33 ];
34
35 logger.info(
36 { newTags, allowedTags, finalTags },
37 `applying final tags to product ${record.id}`
38 );
39
40 // write tags back to Shopify
41 const shopify = connections.shopify.current;
42 if (shopify) {
43 await shopify.graphql(
44 `mutation ($input: ProductInput!) {
45 productUpdate(input: $input) {
46 product {
47 tags
48 }
49 userErrors {
50 message
51 }
52 }
53 }`,
54 {
55 input: {
56 id: `gid://shopify/Product/${record.id}`,
57 tags: finalTags,
58 },
59 }
60 );
61 }
62 }
63};
64
65export const options: ActionOptions = {
66 actionType: "create",
67};
1import {
2 applyParams,
3 preventCrossShopDataAccess,
4 save,
5 ActionOptions,
6} from "gadget-server";
7
8export const run: ActionRun = async ({ params, record }) => {
9 applyParams(params, record);
10 await preventCrossShopDataAccess(params, record);
11 await save(record);
12};
13
14export const onSuccess: ActionOnSuccess = async ({
15 record,
16 logger,
17 api,
18 connections,
19}) => {
20 if (record.body && record.changed("body")) {
21 // get a unique list of words used in the record's description
22 let newTags = [...new Set(record.body.match(/\w+(?:'\w+)*/g))];
23
24 // filter down to only those words which are allowed
25 const allowedTags = (await api.allowedTag.findMany()).map((tag) => tag.keyword);
26 // merge with any existing tags and use Set to remove duplicates
27 const finalTags = [
28 ...new Set(
29 newTags
30 .filter((tag) => allowedTags.includes(tag))
31 .concat(record.tags as string[])
32 ),
33 ];
34
35 logger.info(
36 { newTags, allowedTags, finalTags },
37 `applying final tags to product ${record.id}`
38 );
39
40 // write tags back to Shopify
41 const shopify = connections.shopify.current;
42 if (shopify) {
43 await shopify.graphql(
44 `mutation ($input: ProductInput!) {
45 productUpdate(input: $input) {
46 product {
47 tags
48 }
49 userErrors {
50 message
51 }
52 }
53 }`,
54 {
55 input: {
56 id: `gid://shopify/Product/${record.id}`,
57 tags: finalTags,
58 },
59 }
60 );
61 }
62 }
63};
64
65export const options: ActionOptions = {
66 actionType: "create",
67};

That's not a lot of code!

This snippet will run on every incoming products/create webhook that is sent by Shopify, and determines if tags need to be added by cross-referencing the body of the incoming payload against the stored keyword records by making an internal API request to Gadget. Should any words match, they're sent back to Shopify as new tags for the product.

Gadget gives us a connections object as an argument to our effect function, which has an authenticated Shopify API client ready to go. We use this object to make API calls back to Shopify to update the tags and complete the process.

Sharing code between actions 

If we want to run this same code on the update action, we can create a shared utility function to avoid duplicating code.

  1. Create a new api/models/shopifyProduct/utils.ts file:
api/models/shopifyProduct/utils.ts
TypeScript
1import { api, logger, connections } from "gadget-server";
2
3export const applyTags = async (record: {
4 body: string;
5 tags: string[];
6 id: string;
7}) => {
8 // get a unique list of words used in the record's description
9 let newTags = [...new Set(record.body.match(/\w+(?:'\w+)*/g))];
10
11 // filter down to only those words which are allowed
12 const allowedTags = (await api.allowedTag.findMany()).map((tag) => tag.keyword);
13
14 // merge with any existing tags and use Set to remove duplicates
15 const finalTags = [
16 ...new Set(
17 newTags.filter((tag) => allowedTags.includes(tag)).concat(record.tags)
18 ),
19 ];
20
21 logger.info(
22 { newTags, allowedTags, finalTags },
23 `applying final tags to product ${record.id}`
24 );
25
26 // write tags back to Shopify
27 const shopify = connections.shopify.current;
28 if (shopify) {
29 await shopify.graphql(
30 `mutation ($input: ProductInput!) {
31 productUpdate(input: $input) {
32 product {
33 tags
34 }
35 userErrors {
36 message
37 }
38 }
39 }`,
40 {
41 input: {
42 id: `gid://shopify/Product/${record.id}`,
43 tags: finalTags,
44 },
45 }
46 );
47 }
48};
1import { api, logger, connections } from "gadget-server";
2
3export const applyTags = async (record: {
4 body: string;
5 tags: string[];
6 id: string;
7}) => {
8 // get a unique list of words used in the record's description
9 let newTags = [...new Set(record.body.match(/\w+(?:'\w+)*/g))];
10
11 // filter down to only those words which are allowed
12 const allowedTags = (await api.allowedTag.findMany()).map((tag) => tag.keyword);
13
14 // merge with any existing tags and use Set to remove duplicates
15 const finalTags = [
16 ...new Set(
17 newTags.filter((tag) => allowedTags.includes(tag)).concat(record.tags)
18 ),
19 ];
20
21 logger.info(
22 { newTags, allowedTags, finalTags },
23 `applying final tags to product ${record.id}`
24 );
25
26 // write tags back to Shopify
27 const shopify = connections.shopify.current;
28 if (shopify) {
29 await shopify.graphql(
30 `mutation ($input: ProductInput!) {
31 productUpdate(input: $input) {
32 product {
33 tags
34 }
35 userErrors {
36 message
37 }
38 }
39 }`,
40 {
41 input: {
42 id: `gid://shopify/Product/${record.id}`,
43 tags: finalTags,
44 },
45 }
46 );
47 }
48};
  1. Import the applyTags function into api/models/shopifyProduct/actions/create.ts and call applyTags from the onSuccess function:
api/models/shopifyProduct/actions/create.ts
TypeScript
1import {
2 applyParams,
3 preventCrossShopDataAccess,
4 save,
5 ActionOptions,
6} from "gadget-server";
7import { applyTags } from "../../utils";
8
9export const run: ActionRun = async ({ params, record }) => {
10 applyParams(params, record);
11 await preventCrossShopDataAccess(params, record);
12 await save(record);
13};
14
15export const onSuccess: ActionOnSuccess = async ({
16 record,
17 logger,
18 api,
19 connections,
20}) => {
21 if (record.body) {
22 await applyTags({
23 body: record.body,
24 tags: record.tags as string[],
25 id: record.id,
26 });
27 }
28};
29
30export const options: ActionOptions = {
31 actionType: "create",
32};
1import {
2 applyParams,
3 preventCrossShopDataAccess,
4 save,
5 ActionOptions,
6} from "gadget-server";
7import { applyTags } from "../../utils";
8
9export const run: ActionRun = async ({ params, record }) => {
10 applyParams(params, record);
11 await preventCrossShopDataAccess(params, record);
12 await save(record);
13};
14
15export const onSuccess: ActionOnSuccess = async ({
16 record,
17 logger,
18 api,
19 connections,
20}) => {
21 if (record.body) {
22 await applyTags({
23 body: record.body,
24 tags: record.tags as string[],
25 id: record.id,
26 });
27 }
28};
29
30export const options: ActionOptions = {
31 actionType: "create",
32};
  1. Do the same in api/models/shopifyProduct/actions/update.ts, import applyTags and call the function in onSuccess:
api/models/shopifyProduct/actions/update.ts
TypeScript
1import { applyParams, save, ActionOptions } from "gadget-server";
2import { preventCrossShopDataAccess } from "gadget-server/shopify";
3import { applyTags } from "../../utils";
4
5export const run: ActionRun = async ({ params, record }) => {
6 applyParams(params, record);
7 await preventCrossShopDataAccess(params, record);
8 await save(record);
9};
10
11export const onSuccess: ActionOnSuccess = async ({
12 record,
13 logger,
14 api,
15 connections,
16}) => {
17 if (record.body && record.changed("body")) {
18 await applyTags({
19 body: record.body,
20 tags: record.tags as string[],
21 id: record.id,
22 });
23 }
24};
25
26export const options: ActionOptions = { actionType: "update" };
1import { applyParams, save, ActionOptions } from "gadget-server";
2import { preventCrossShopDataAccess } from "gadget-server/shopify";
3import { applyTags } from "../../utils";
4
5export const run: ActionRun = async ({ params, record }) => {
6 applyParams(params, record);
7 await preventCrossShopDataAccess(params, record);
8 await save(record);
9};
10
11export const onSuccess: ActionOnSuccess = async ({
12 record,
13 logger,
14 api,
15 connections,
16}) => {
17 if (record.body && record.changed("body")) {
18 await applyTags({
19 body: record.body,
20 tags: record.tags as string[],
21 id: record.id,
22 });
23 }
24};
25
26export const options: ActionOptions = { actionType: "update" };

Now this code will be run every time a product is created or updated in a Shopify store.

Avoid webhook loops

The record.changed helper is a special field that Gadget has included to help prevent an infinite loop when updating Shopify records.

When we call shopify.graphql(...) with the productUpdate mutation, the product in our Shopify store will be updated. This update action will fire Shopify's products/update webhook. If we are using this webhook as a trigger for running custom code that updates a product, we will be stuck in an endless loop of updating our products and running our action.

We can use record.changed to determine if changes have been made to the key on this record and only run our code if changes have occurred.

For more info on change tracking in Gadget, refer to the documentation.

Step 4: Access control 

To ensure that only the right people can access your app, you can set up access control rules in Gadget. This will allow you to restrict access to certain parts of your app based on the user's role.

By default, merchants will not have access to your custom model APIs, such as allowedTag. You can grant permissions to the shopify-app-users role to allow merchants to access these APIs.

  1. Navigate to the accessControl/permissions page
  2. Grant the shopify-app-users role access to the allowedTag/ model's read, create, and delete actions

Now merchants will be able to manage allowedTag records from the embedded frontend in their Shopify store admin.

Step 5: Build a Shopify admin frontend 

Gadget apps include a web folder for your frontend. This folder contains the following:

  • your Gadget API client in web/api.ts
  • a default React app in web/main.tsx
  • router setup in web/App.tsx
  • two pages at web/routes/index.tsx and web/routes/about.tsx

Additional packages have also been added to your package.json upon connecting to Shopify:

  • @shopify/polaris: Shopify's design system components
  • @gadgetinc/react: provides React hooks for fetching data and calling your API
    • @gadgetinc/react/auto: provides autocomponents, pre-built forms and tables connected to your APIs

The entire tagger frontend code snippet is below. Additional details on some of Gadget's provided tooling are below the snippet.

  1. Paste the following code into web/routes/index.tsx
web/routes/index.jsx
React (TypeScript)
1import { Layout, Page, Card, Text } from "@shopify/polaris";
2import { AutoForm, AutoTable } from "@gadgetinc/react/auto/polaris";
3import { api } from "../api";
4
5export default function () {
6 // use autocomponents to automatically create a form and table to manage allowedTag records
7 return (
8 <Page title="Keyword manager">
9 <Layout>
10 <Layout.Section>
11 <Card>
12 {/** AutoForm automatically calls allowedTag.create on form submission */}
13 <AutoForm action={api.allowedTag.create} title="Add keywords" />
14 </Card>
15 </Layout.Section>
16 <Layout.Section>
17 <Card>
18 <Text as="h2" variant="headingLg">
19 Manage keywords
20 </Text>
21 {/** AutoTable allows you to delete allowedTag records (in bulk!) */}
22 <AutoTable model={api.allowedTag} columns={["keyword"]} />
23 </Card>
24 </Layout.Section>
25 </Layout>
26 </Page>
27 );
28}
1import { Layout, Page, Card, Text } from "@shopify/polaris";
2import { AutoForm, AutoTable } from "@gadgetinc/react/auto/polaris";
3import { api } from "../api";
4
5export default function () {
6 // use autocomponents to automatically create a form and table to manage allowedTag records
7 return (
8 <Page title="Keyword manager">
9 <Layout>
10 <Layout.Section>
11 <Card>
12 {/** AutoForm automatically calls allowedTag.create on form submission */}
13 <AutoForm action={api.allowedTag.create} title="Add keywords" />
14 </Card>
15 </Layout.Section>
16 <Layout.Section>
17 <Card>
18 <Text as="h2" variant="headingLg">
19 Manage keywords
20 </Text>
21 {/** AutoTable allows you to delete allowedTag records (in bulk!) */}
22 <AutoTable model={api.allowedTag} columns={["keyword"]} />
23 </Card>
24 </Layout.Section>
25 </Layout>
26 </Page>
27 );
28}
Autocomponents

The @gadgetinc/react/auto library provides autocomponents for your frontend. Autocomponents are pre-built configurable forms and tables that are wired up to your model actions.

Read the autocomponent guide for more information on autocomponent customization.

You don't need to use autocomponents in your frontends. Check out the Shopify frontends guide to learn how to manually read and write data.

If you go to your development app, you should now be able to test it out! Go back to the embedded frontend in your store admin and start adding custom tags. You'll be able to see the created tags in the allowedTag Data page in Gadget.

A screenshot of the completed embedded tagger app

Congrats! You have built a full-stack and fully functional embedded product tagger application! Now you can test it out.

Step 6: Test it out 

  1. First, add some keywords to your product tagger. You want to make sure to add words that are in your product descriptions. If using Shopify's default store data, SUPER and DUPER both appear in the product description of The Complete Snowboard.
  2. Go back to the Connections page in Gadget and click Shop Installs and then Sync on the connected store if you set up a custom app through the Partners dashboard, or just click Sync if you used the store Admin to set up your app.
The Installs page for the connection, displaying the store name, and the Sync button

Gadget will fetch each of the records in Shopify and run them through your actions. Not only will this populate your Gadget backend with the store's inventory, but it will also run the effects we added, updating the tags for each synced product. Our tagging application will also run on products when they are added to the store, so any new products will also be tagged for us automatically.

Display the tag added on one of the demo products

Congratulations! In about 20 minutes you were able to build a custom app that updates tags in Shopify each time there is a match against the list of allowed tags.

Next steps 

Now that you can add keywords using an admin UI, you may want to try adding a global action to run through all existing products that have been synced to Gadget to apply tags!

Want to build apps that use Shopify extensions? Check out our pre-purchase checkout UI extension tutorial:

Was this page helpful?