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.
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.
To get the most out of this tutorial, you will need:
- A Shopify Partners account
- A development store
- At least one product in your store that has a product description
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.
- Go to the Shopify Partner dashboard
- 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 the Create App button
- Click the Create app manually button and enter a name for your Shopify app
- Click on Settings in the side nav bar
- Click on Plugins in the modal that opens
- Select Shopify from the list of plugins and connections
- Copy the Client ID and Client secret from your newly created Shopify app and paste the values into the Gadget Connections page
- 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.
- 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 that we want to import into Gadget
Now we want to connect our Gadget app to our custom app in the Partner dashboard.
- 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
- Copy the App URL and Allowed redirection URL from the Gadget Connections page and paste them into your custom Shopify App
Now we need to install our Shopify app on a store.
- Go back to the Shopify Partner dashboard
- Click on Apps to go to the Apps page again
- Click on your custom app
- Click on Select store
- Click on the store we want to use to develop our app
- You may be prompted about Store transfer being disabled. This is okay, click Install anyway
- Click Install app to install your Gadget app on your Shopify store
If you are getting a permissions denied error when installing your app, try logging in to the Shopify store Admin!
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.jsx
.
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!
- Click + next to the
api/models
folder in the nav to add a model, and call itallowedTag
- Click + in the FIELDS section of the
api/models/allowedTag/schema
page to add a field, and name itkeyword
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!
- Click on
api/models/allowedTag/actions/create.js
to open theallowedTag
model'screate
action - 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 GraphQL mutation is pre-populated already, all we need to do is update the keyword
value to the keyword we want to store.
- Add the following to the Variables section of the API Playground:
copy-paste into the Variables section of the API Playgroundjson{"allowedTag": {"keyword": "sweater"}}
- Click the Execute query button to run the mutation
We can run the same mutation again with a different keyword value to store additional keywords.
We can also check to make sure our tag keywords have been saved.
- Go back to the main Gadget editor
- Click on
api/models/allowedTag/data
in the left nav to go to this model's data page
We can see our added allowedTag
records!
Step 3: Build your tagging script
Gadget keeps your app and store in sync by generating a CRUD (Create, Read, Update, Delete) API around each of your cloned models and wiring up each of the API actions to their corresponding Shopify webhook. What makes Actions special is that they can be completely customized. You can change what happens when the action runs by adding custom code to the run
and onSuccess
functions. Read more about Gadget actions.
Now that we have keywords to check against, we can write our tagging script. Because we want this script to run every time a product record is created or updated, we'll add an Effect to the create
and update
actions on shopifyProduct
:
- Click on
api/models/shopifyProduct/actions/create.js
in the file explorer - Replace the contents of the
api/models/shopifyProduct/actions/create.js
code file with the following snippet:
api/models/shopifyProduct/actions/create.jsJavaScript1import {2 applyParams,3 preventCrossShopDataAccess,4 save,5 ActionOptions,6 CreateShopifyProductActionContext,7} from "gadget-server";89/**10 * @param { CreateShopifyProductActionContext } context11 */12export async function run({ params, record, logger, api }) {13 applyParams(params, record);14 await preventCrossShopDataAccess(params, record);15 await save(record);16}1718/**19 * @param { CreateShopifyProductActionContext } context20 */21export async function onSuccess({ params, record, logger, api, connections }) {22 if (record.body && record.changed("body")) {23 // get a unique list of words used in the record's description24 let newTags = [...new Set(record.body.match(/\w+(?:'\w+)*/g))];2526 // filter down to only those words which are allowed27 const allowedTags = (await api.allowedTag.findMany()).map((tag) => tag.keyword);28 // merge with any existing tags and use Set to remove duplicates29 const finalTags = [30 ...new Set(31 newTags.filter((tag) => allowedTags.includes(tag)).concat(record.tags)32 ),33 ];34 logger.info(35 { newTags, allowedTags, finalTags },36 `applying final tags to product ${record.id}`37 );3839 // write tags back to Shopify40 const shopify = await connections.shopify.current;41 if (shopify) {42 await shopify.product.update(parseInt(record.id), {43 tags: finalTags.join(","),44 });45 }46 }47}4849/** @type { ActionOptions } */50export const options = {51 actionType: "create",52};
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
We also use Gadget's changed helper on our record
to avoid entering an infinite loop. This looping can occur when a Shopify webhook triggers code that updates our Shopify store. Because we have added this change detection, we can use the same code for both the create
and update
actions.
- Instead of duplicating the code in both places, we can create a new
api/models/shopifyProduct/utils.js
file to hold our shared code:
api/models/shopifyProduct/utils.jsJavaScript1export async function applyTags({ record, logger, api, connections }) {2 if (record.body && record.changed("body")) {3 // get a unique list of words used in the record's description4 let newTags = [...new Set(record.body.match(/\w+(?:'\w+)*/g))];56 // filter down to only those words which are allowed7 const allowedTags = (await api.allowedTag.findMany()).map((tag) => tag.keyword);89 // merge with any existing tags and use Set to remove duplicates10 const finalTags = [11 ...new Set(12 newTags.filter((tag) => allowedTags.includes(tag)).concat(record.tags)13 ),14 ];15 logger.info(16 { newTags, allowedTags, finalTags },17 `applying final tags to product ${record.id}`18 );1920 // write tags back to Shopify21 const shopify = await connections.shopify.current;22 if (shopify) {23 await shopify.product.update(parseInt(record.id), {24 tags: finalTags.join(","),25 });26 }27 }28}
- Import the
applyTags
function intoapi/models/shopifyProduct/actions/create.js
and callapplyTags
from theonSuccess
function:
api/models/shopifyProduct/actions/create.jsJavaScript1import {2 applyParams,3 preventCrossShopDataAccess,4 save,5 ActionOptions,6 CreateShopifyProductActionContext,7} from "gadget-server";8import { applyTags } from "../utils";910/**11 * @param { CreateShopifyProductActionContext } context12 */13export async function run({ params, record, logger, api }) {14 applyParams(params, record);15 await preventCrossShopDataAccess(params, record);16 await save(record);17}1819/**20 * @param { CreateShopifyProductActionContext } context21 */22export async function onSuccess({ params, record, logger, api, connections }) {23 await applyTags({ record, logger, api, connections });24}2526/** @type { ActionOptions } */27export const options = {28 actionType: "create",29};
- Do the same in
api/models/shopifyProduct/actions/update.js
, importapplyTags
and call the function inonSuccess
:
api/models/shopifyProduct/actions/update.jsJavaScript1import {2 applyParams,3 preventCrossShopDataAccess,4 save,5 ActionOptions,6 UpdateShopifyProductActionContext,7} from "gadget-server";8import { applyTags } from "../utils";910/**11 * @param { UpdateShopifyProductActionContext } context12 */13export async function run({ params, record, logger, api }) {14 applyParams(params, record);15 await preventCrossShopDataAccess(params, record);16 await save(record);17}1819/**20 * @param { UpdateShopifyProductActionContext } context21 */22export async function onSuccess({ params, record, logger, api, connections }) {23 await applyTags({ record, logger, api, connections });24}2526/** @type { ActionOptions } */27export const options = {28 actionType: "update",29};
Now this code will be run every time a product is created or updated in a Shopify store.
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.product.update(...)
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 custom code.
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: Add shop tenancy and permissions to allowedTag
model
Right now the allowedTag
model only has a single field, keyword
. If you're building a public Shopify app, you also need to associate keywords with individual stores so that all the shops that install your app don't share keyword
records.
It's also important to grant your embedded app permission to call the create and delete actions on the allowedTag
model. Access to custom model APIs are always disabled by default for embedded app users. If you encounter a GGT_PERMISSION_DENIED error when building an embedded app, you probably need to go into accessControl/permissions
and grant embedded app users access to your Gadget app API.
- Add a new field named
shop
to theallowedTag
model - Make
shop
a belongs to relationship field and selectshopifyShop
as the related model - Select the has many option when defining the inverse of the relationship so that
shopifyShop has many allowedTags
With this added Shop relationship, you will be able to track what keywords are used for individual shops. To automatically filter by the current shop when reading from your allowedTag
model, you can add a Gelly snippet to enforce shop tenancy automatically.
- Open the
accessControl/permissions
page - Enable the read, create, and delete actions for your
allowedTag
model on theshopify-app-users
role
- Click + Filter next to the read action, type
tenancy
into the input, and hit Enter on your keyboard to create a new Gelly file - Go to the file by clicking on the File icon
- Add the following Gelly fragment
accessControl/filters/allowedTag/tenancy.gellygellyfilter ($session: Session) on AllowedTag [where shopId == $session.shopId]
This snippet selects all allowedTag
records when the related shopId
is equal to the shop id of the current session
. The current session
is managed for you when you connect to Shopify, and session records including the current session can be viewed on the session
Data page in Gadget.
This handles shop tenancy for your read action, and will also be applied for update, delete, or custom model actions. For custom actions, it's advisable to leverage Gadget's preventCrossShopDataAccess
helper which prevents the modification of the shop relationship on all model actions to which it is applied.
- Go to your
api/models/allowedTag/actions/create.js
file and paste the following code:
api/models/allowedTag/actions/create.jsjs1import { applyParams, save, ActionOptions, CreateAllowedTagActionContext, preventCrossShopDataAccess } from "gadget-server";23/**4 * @param { CreateAllowedTagActionContext } context5 */6export async function run({ params, record, logger, api, connections }) {7 applyParams(params, record);8 await preventCrossShopDataAccess(params, record);9 await save(record);10}1112/**13 * @param { CreateAllowedTagActionContext } context14 */15export async function onSuccess({ params, record, logger, api, connections }) {16 // Your logic goes here17}1819/** @type { ActionOptions } */20export const options = {21 actionType: "create",22};
The relationship to the current shop is automatically set up by the preventCrossShopDataAccess
helper in the run
function. By using this helper, the shopId
cannot be spoofed by malicious users.
The preventCrossShopDataAccess
helper is also useful for other model actions, such as delete
, to enforce data tenancy in public apps.
- Go to your
api/models/allowedTag/actions/delete.js
file and paste the following code:
api/modelsallowedTag/actions/delete.jsjs1import { deleteRecord, ActionOptions, DeleteAllowedTagActionContext, preventCrossShopDataAccess } from "gadget-server";23/**4 * @param { DeleteAllowedTagActionContext } context5 */6export async function run({ params, record, logger, api, connections }) {7 // only allow deletion if the request comes from the same shop that relates to the record8 await preventCrossShopDataAccess(params, record);9 await deleteRecord(record);10}1112/**13 * @param { DeleteAllowedTagActionContext } context14 */15export async function onSuccess({ params, record, logger, api }) {16 // Your logic goes here17}1819/** @type { ActionOptions } */20export const options = {21 actionType: "delete",22};
For delete
actions (or update
, or other custom actions), using preventCrossShopDataAccess
in the run
function verifies that the shopId associated with the current record matches the shopId for the current session. If these values do not match, an error is triggered, resulting in the failure of the action.
You also need to update how you read your allowedTag
data in the shopifyProduct
update and create actions. You must filter by the currentShopId to ensure that you are only reading the allowedTag records for the current shop, as Gelly tenancy is not applied when making API requests in your backend actions.
- Paste the following code into
api/models/shopifyProduct/utils.js
:
api/models/shopifyProduct/utils.jsjs1export async function applyTags({ record, logger, api, connections }) {2 if (record.body && record.changed("body")) {3 // get a unique list of words used in the record's description4 let newTags = [...new Set(record.body.match(/\w+(?:'\w+)*/g))];56 // filter down to only those words which are allowed7 // a filter condition is used on the api.allowedTag.findMany() request that checks the shop id8 const allowedTags = (await api.allowedTag.findMany({ filter: { shop: { equals: connections.shopify.currentShopId } } })).map(9 (tag) => tag.keyword10 );1112 // merge with any existing tags and use Set to remove duplicates13 const finalTags = [...new Set(newTags.filter((tag) => allowedTags.includes(tag)).concat(record.tags))];14 logger.info({ newTags, allowedTags, finalTags }, `applying final tags to product ${record.id}`);1516 // write tags back to Shopify17 const shopify = await connections.shopify.current;18 if (shopify) {19 await shopify.product.update(parseInt(record.id), {20 tags: finalTags.join(","),21 });22 }23 }24}
Now you have a multi-tenant backend. The final step is building an embedded frontend.
Step 5: Build a Shopify admin frontend
New Gadget apps include a web
folder. When you set up a Shopify connection, Gadget automatically makes changes to this web
folder by:
- initializing your Gadget API client in
web/api.js
- setting up a default React app in
web/main.jsx
- adding a routing example in
web/App.jsx
- has two examples of rendered pages and navigation with
web/routes/index.jsx
andweb/routes/about.jsx
Additional packages have also been added to your package.json
upon connecting to Shopify, including @gadgetinc/react
which allows for the use of Gadget's handy React hooks for fetching data and calling your Gadget project's API, and @shopify/polaris
which allows you to use Shopify's Polaris components out of the box when building embedded Shopify apps.
Start building!
The entire tagger frontend code snippet is below. Additional details on some of Gadget's provided tooling are below the snippet.
- Paste the following code into
web/routes/index.jsx
web/routes/index.jsxjsx1import { useCallback } from "react";2import { useFindMany, useAction, useActionForm, Controller } from "@gadgetinc/react";3import { TitleBar } from "@shopify/app-bridge-react";4import {5 Banner,6 Button,7 Form,8 FormLayout,9 Layout,10 Page,11 Spinner,12 Tag,13 TextField,14 Card,15 BlockStack,16 Text,17 InlineStack,18} from "@shopify/polaris";19import { api } from "../api";2021// component used to display error messages22const ErrorBanner = ({ title, error }) => {23 return (24 <Banner tone="critical" title={title}>25 <code>{error.toString()}</code>26 </Banner>27 );28};2930export default function () {31 // a useFindMany hook to fetch allowedTag data32 const [{ data, fetching, error }] = useFindMany(api.allowedTag);3334 // useActionForm used to manage form state and submission for creating new tags35 const { submit, control, reset, error: createError, formState } = useActionForm(api.allowedTag.create);36 // the useAction hook is used for deleting existing tags37 const [{ error: deleteTagError }, deleteTag] = useAction(api.allowedTag.delete);3839 const removeTag = useCallback(40 async (id) => {41 // call the deleteTag function defined with the useAction hook with the id of the tag to delete42 await deleteTag({ id });43 },44 [deleteTag]45 );4647 // render the page, using data, fetching, and error from the useFindMany, useAction, and useActionForm hooks to display different widgets48 return (49 <Page title="Keyword manager">50 <Layout>51 <Layout.Section>52 <TitleBar title="Manage keywords" />53 <Form54 onSubmit={async () => {55 // submit the form and save the keyword as a new allowedTag record56 await submit();57 // reset the form input to empty58 reset();59 }}60 >61 <FormLayout>62 {createError && <ErrorBanner title="Error adding keyword" error={createError} />}63 <Controller64 name="keyword"65 control={control}66 required67 render={({ field }) => {68 // Functional components like the Polaris TextField do not allow for 'ref's to be passed in69 // Remove it from the props passed to the TextField70 const { ref, ...fieldProps } = field;71 // Pass the field props down to the TextField to set the value value and add onChange handlers72 return (73 <TextField74 label="Tag"75 type="text"76 autoComplete="tag"77 helpText={<span>Add a keyword</span>}78 disabled={formState.isSubmitting}79 connectedRight={80 <Button variant="primary" submit disabled={formState.isSubmitting}>81 Add keyword82 </Button>83 }84 {...fieldProps}85 />86 );87 }}88 ></Controller>89 </FormLayout>90 </Form>91 </Layout.Section>92 <Layout.Section>93 <Card>94 <BlockStack gap="200">95 <Text variant="headingSm" as="h5">96 Existing keywords97 </Text>98 {fetching && <Spinner />}99 {error && <ErrorBanner title="Error reading tags" error={error} />}100 {deleteTagError && <ErrorBanner title="Error removing keyword" error={deleteTagError} />}101 <InlineStack gap="100">102 {data?.map((allowedTag, i) => (103 <Tag key={i} onRemove={() => removeTag(allowedTag.id)}>104 {allowedTag.keyword}105 </Tag>106 ))}107 </InlineStack>108 {data?.length === 0 && <p>No keywords added</p>}109 </BlockStack>110 </Card>111 </Layout.Section>112 </Layout>113 </Page>114 );115}
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, along with the related shop ID, in the allowedTag
Data page in Gadget.
How the frontend reads and writes data
The above snippet has everything you need to build a frontend for your app. Let's take a closer look at how you read and write data using Gadget's React tooling.
Using the API and hooks to fetch data
Your Gadget API client is already set up for you in web/api.js
! You can use this API client to fetch data from our models using the product tagger application's auto-generated API. You can also make use of some Gadget-provided React hooks that help to read (and write) using your app's API.
The useFindMany
hook will run api.allowedTag.findMany()
and return a response that has data
, error
, and fetching
as properties. You can use fetching
to display a Polaris Spinner while the data
is being fetched, and the error
property to display and handle any request errors.
Here is the code snippets from your tagger frontend that use the useFindMany
hook:
reading allowedTag data in web/routes/index.jsxjsx1// import the required dependencies2import { useFindMany } from "@gadgetinc/react";3import { api } from "../api";45// ...67export default function () {8 // fetch allowedTag data9 const [{ data, fetching, error }] = useFindMany(api.allowedTag);1011 // ...1213 // a Spinner is displayed while fetching is true14 // an error Banner is displayed if there is a read error15 // a list of keywords is displayed if data exists16 // a message is displayed if data is empty17 return (18 {/* other components */}19 <BlockStack gap="200">20 <Text variant="headingSm" as="h5">Existing keywords</Text>21 {fetching && <Spinner />}22 {error && <ErrorBanner title="Error reading tags" error={error} />}23 <InlineStack gap="100">24 {data?.map((allowedTag, i) => (25 <Tag key={i} onRemove={() => removeTag(allowedTag.id)}>26 {allowedTag.keyword}27 </Tag>28 ))}29 </InlineStack>30 {data?.length === 0 && <p>No keywords added</p>}31 </BlockStack>32 {/* other components */}33 );34};
Using the API and hooks to write data
Now you need to send a request to your Gadget app's backend API to create entered keywords in your app's database. The @gadgetinc/react
package also has a useActionForm
and useAction
hooks that will assist with making requests to any of your model actions.
Similar to useFindMany
, the useAction
hook returns an object with data
, fetching
, and error
properties. Additionally, useAction
returns a function that needs to be called to run the action.
For the product tagger, you are using the useAction
hook to call api.allowedTag.delete
:
calling the allowedTag create action in web/routes/index.jsxjsx1import { useAction } from "@gadgetinc/react";2import { api } from "../api";34// ...56export default function () {7 // ...89 // useAction hook to call the delete action10 const [{ error: deleteTagError }, deleteTag] = useAction(api.allowedTag.delete);1112 // ...1314 // a removeTag callback is created, and the deleteTag function is called15 const removeTag = useCallback(16 async (id) => {17 // call the deleteTag function defined with the useAction hook with the id of the tag to delete18 await deleteTag({ id });19 },20 [deleteTag]21 );2223 // ...2425 return (26 {/* other components */}27 <InlineStack gap="100">28 {data?.map((allowedTag, i) => (29 <Tag key={i} onRemove={() => removeTag(allowedTag.id)}>30 {allowedTag.keyword}31 </Tag>32 ))}33 </InlineStack>34 {/* other components */}35 );36};
The useActionForm
hook is used to manage the state of the form and submit the entered keyword to the api.allowedTag.create
action. A Controller
is also used to interact with controlled components, like those from the Polaris library.
useActionForm
wraps react-form-hooks
, more information can be found in the Gadget reference docs.
using useActionFrom to create new allowedTag records in web/routes/index.jsxjsx1import { useActionForm, Controller } from "@gadgetinc/react";2import { api } from "../api";34export default function () {56 // ...78 // useActionForm used to manage form state and submission for creating new tags9 const { submit, control, reset, error: createError, formState } = useActionForm(api.allowedTag.create);1011 // ...1213 // render the page, using data, fetching, and error from the useFindMany, useAction, and useActionForm hooks to display different widgets14 return (15 {/* other components */}16 <Form17 onSubmit={async () => {18 // submit the form and save the keyword as a new allowedTag record19 await submit();20 // reset the form input to empty21 reset();22 }}23 >24 <FormLayout>25 {createError && <ErrorBanner title="Error adding keyword" error={createError} />}26 <Controller27 name="keyword"28 control={control}29 required30 render={({ field }) => {31 // Functional components like the Polaris TextField do not allow for 'ref's to be passed in32 // Remove it from the props passed to the TextField33 const { ref, ...fieldProps } = field;34 // Pass the field props down to the TextField to set the value value and add onChange handlers35 return (36 <TextField37 label="Tag"38 type="text"39 autoComplete="tag"40 helpText={<span>Add a keyword</span>}41 disabled={formState.isSubmitting}42 connectedRight={43 <Button variant="primary" submit disabled={formState.isSubmitting}>44 Add keyword45 </Button>46 }47 {...fieldProps}48 />49 );50 }}51 ></Controller>52 </FormLayout>53 </Form>54 {/* other components */}55 );56};
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
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.
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.
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.
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: