Actions
Useful apps do more than just store data. They accomplish helpful things for us, like validating inputs for errors, deriving and tracking new data like audit logs, updating other 3rd party systems via API, or anything else useful to us humans. This behavior of an application is defined in Gadget by adding Actions.
Actions are the units of work your application can perform. When a user or other system interacts with your application, they do so by calling Actions. Each Action is exposed as a unique mutation in your application's GraphQL API and can also be called by other Triggers like webhooks from Shopify or on a cron-like schedule. You can add as many Actions to your app as you need.
Model Actions vs Global Actions
There are two types of Actions in Gadget:
- a specific model's Actions, which are API endpoints that run in the context of specific records
- Global Actions, which run outside the context of any particular record
Picking which type of Action to use is best done by asking the question, "does this Action operate on a specific record of a specific model?" For example, if you wanted to implement logic that sent an email to your boss every time a new user signed up to your app, you would likely add an Action to a User model. This is best represented as a model Action because it concerns a particular record (the user who just signed up), and Gadget can help with loading and saving that particular record as it is changed. Conversely, a weekly email that gets sent to your whole company notifying you of all the week's signups might be a Global Action. This Action doesn't read or write any data to or from a particular central record, so it's most natural to model it as a Global Action outside any one record's lifecycle.
Model actions can be found by clicking on an Action in the list of actions on the Model page, and Global Actions can be found under the Global Actions header in the navigation sidebar.
Anatomy of an Action
Every Action and Global Action in Gadget has the same structure and is executed the same way. A Trigger starts an Action, then permissions are checked, and then the list of Effects is run to do the actual work.
This is the specific sequence that Gadget runs:
- A trigger starts the Action. Triggers are often the GraphQL API mutation being run, but they can also be things like webhooks from 3rd parties or a daily scheduled Action. Think of triggers as the when of running an Action.
- Gadget then verifies that the caller has permission to run the Action. The current user or current API key's permissions determine whether or not the Action can be run. Think of permissions as the answer to who can run an Action. For more information on roles and permissions, see the Access Control guide.
- Model Actions load the record they are being run on from the database. You don't have to manually fetch the record that the Action is running on.
- The Action Run Effects are executed one at a time, in order, within a database transaction. Effects manipulate the record, make API calls, log data, or do anything else that needs to happen. Think of effects as the answer to what happens once an Action has decided to run. Gadget has built-in effects to manipulate the database, and you can also write your own effects in JavaScript.
- If all the Run Effects succeed and the database transaction commits, the Action's Success Effects are run. The Success Effects are a second effect list that allows only running certain business logic if the data was reliably saved, which is useful for the common case of reaching out to third-party systems like Shopify.
Global Actions share all these same properties, but since they don't operate in the context of a record, they don't load any data automatically.
Triggers
Actions are kicked off by triggers. Triggers parse some event from the outside world, like an incoming webhook or API call, and send it along to the rest of the Action. Most Actions already have default triggers set up, like the GraphQL API trigger that mounts the Action in your app's API, or the Shopify Webhook triggers which run your Actions in response to webhooks from Shopify. There are also other trigger types, like:
- Daily syncs for many Shopify models.
- Incoming webhooks for many Shopify models.
- The Scheduler trigger, which can repeatedly execute a Global Action on a regular schedule.
GraphQL API Trigger
This trigger allows actions to be called from the GraphQL API that Gadget generates for your app. Each Action with a GraphQL API trigger will have one corresponding mutation. When this mutation is called, the Action will be run, and the resulting record or errors will be returned by the GraphQL API. These mutations are also easily callable from the JS client for your application.
For example, if we add a publish
action to a Post model in the Gadget editor, the GraphQL API will get a new publishPost
mutation:
GraphQL1mutation PublishPost($id: GadgetID!, $post: PublishPostParams!) {2 publishPost(id: $id, post: $post) {3 id4 title5 publishedAt6 }7}
You can also call the publishPost
mutation with the generated JS client:
JavaScriptimport { Client } from "@gadget-client/blog-post-example";const client = new Client();await client.post.publish(123, { post: { publishedAt: new Date() } });
Read more about the generated GraphQL API for Gadget applications in the GraphQL reference.
Shopify models in the GraphQL API
Since Shopify is the source of truth for Shopify data, Gadget doesn't add API triggers allowing mutation of Shopify data to your app by default. Shopify data is instead updated through Actions triggered by the Shopify Webhook and Shopify Sync triggers. You can still add an API trigger to Shopify model Actions. Generally, this is only necessary for Shopify models where you've added your own fields to the model and need to update these fields with a normal API call.
The create, update, and delete Actions on Shopify models in Gadget's GraphQL API will not persist changes back to Shopify. For example, if you add a GraphQL trigger to the update Action on the Shopify Product model, using your GraphQL API to update the product will not update that product in Shopify unless you add an effect to make an update call to the Shopify API.
If you decide to do that, care must be taken to avoid an infinite loop.
Scheduler trigger
You can use the scheduler trigger if you want to run your Action on a regular schedule, similar to scheduling a cron job. For example, perhaps you would like to fetch some data from another service every hour or summarize your Gadget data every Thursday at midnight. Every Global Action is allowed to have one scheduler trigger, which you can add by clicking the plus icon next to the triggers panel:

When you select the scheduler trigger card, you will be able to change the schedule or add more entries to the schedule. For example, you will need five entries - one for each day - to have a schedule that runs at 4 PM every weekday.
The scheduler trigger does not prevent duplicate calls from overlapping schedule items. For example, if you have a schedule with one entry that runs every hour, on the hour, and another entry for every day at 5:00 PM, your action will run twice every day at 5:00 PM.
Since the scheduler trigger executes a Global Action like the API, all logs emitted from the Action can be found in the log viewer.
Any execution of a scheduled Global Action that overlaps a previously scheduled execution that is still running will be ignored. For example, suppose you have a schedule that runs a Global Action every minute. If an execution at 10:50 takes 3.5 minutes to run, the executions that were scheduled for 10:51, 10:52, and 10:53 will be ignored, and the next scheduled execution of the Global Action will be at 10:54. Any execution that fails will be retried 4 times, so a maximum of 5 attempts. Similar to overlapping executions, retries will also cause overlapping scheduled executions to be ignored.
Webhooks Trigger
Gadget only receives webhook triggers from Shopify once you've set up a Shopify connection. Whenever Shopify sends Gadget a webhook, Gadget verifies its authenticity and triggers the relevant Action. If the Action fails for any reason, Gadget will retry up to 10 times with an increasing delay between each attempt. The same traceId
is used for each retry. This traceId
can be used to filter a Gadget app's Logs to see details about each attempt, for example, {level=~"warn|error", environment_id="123"} | json | trace_id="21a0324a67729a47ded4b33046afcffe"
.
If the Action fails on the last attempt, the webhook is "lost," but your next daily sync should ensure that the affected records in Gadget are brought back in sync with Shopify.
Shopify Sync Trigger
Webhooks alone are not enough to ensure data is kept up to sync with Shopify. Most Shopify models also receive a sync trigger to reconcile any missed webhooks, as per Shopify's best practices. Syncs run once a day and ensure any data from missed webhooks in the past day is kept up to date. It also ensures any models that don't receive webhooks are updated.
Actions that fail during sync are not retried. You can visit the Shopify Connection page to find the failed sync and view the logs for that sync.
Action Effects
Actions have Effects that do useful things. Effects can change the database, change the requester's session, make outgoing API calls, and really do whatever they want! An Action's Effects are broken into two separate lists: Run Effects and Success Effects. Each Effect list is run one Effect at a time in order from top to bottom. If any Effect throws an error, the subsequent Effects in the list are skipped, and the error is returned to the caller.
There's a variety of different types of Effects in Gadget. Gadget has a built-in library of effects for working with the database and other Gadget systems, as well as a Run Code effect that is used for executing whatever JavaScript code you like.
Run Effects
Run Effects are for the main body of business logic that an Action should run. Business logic that changes data in your app's models generally belongs in a Run Effect.
For example, we could write a Run Effect to set a default value on the current record during Action execution:
JavaScriptmodule.exports = async ({ record }) => {record.title ??= "New Record Title";};
To set a field in a Run Effect, you need to update the record
in a Code Snippet before the Create/Update Record Effect. This is required because Create/Update Record are the effects responsible for writing record
to the database. Otherwise, you will need to make a call to the api
to update the field.

Or we could write a Run Effect to create a new record for a different model in our app:
JavaScript1module.exports = async ({ api, record }) => {2 await api.auditLogs.create({3 auditLog: {4 message: `A new record was created`,5 record: record.toJSON(),6 },7 });8};
For more information on authoring Effects with JavaScript, see the Extending with Code guide.
Success Effects
Gadget supports a secondary effect list that runs after the Run Effects called the Success Effects. Success Effects run when the Run Effects all execute without throwing any errors and are not run if any Run Effect does throw an error.
Success Effects are often useful for sending data to other systems, like a hosted search service, payments API, or any other third party. Another frequent use case for Success Effects is sending emails or push notifications because you generally only want to show those external signs of success if an action really did succeed.
It's generally important to ensure that the changes you are making to your local Gadget database have been committed successfully and saved to the database before informing those other services that changes have been made. If third-party service API calls are made in the Run Effects, there is still a chance that a different Run Effect might fail and cause the transaction to be rolled back. If this happens, the third-party service may have been sent data that doesn't exist in the database, which can be a major bug. A good rule of thumb is: changes to your Gadget database belong in Run Effects, and changes to a third-party system belong in Success Effects. See Action Transactions for more details.
For example, we could write a Success Effect to log out the record that was just written to the database:
JavaScriptmodule.exports = async ({ record, logger }) => {logger.info({ record }, "new record successfully committed to the database");};
Or we could write a Success Effect that POSTs the newly changed record details to a 3rd party API:
JavaScript1const axios = require("axios");23module.exports = async ({ record, logger }) => {4 await axios({5 method: "post",6 url: "https://some-third-party-api.net/api/v1/records",7 data: { record: record.toJSON() },8 });9};
If a Success Effect throws an uncaught error, the Action's execution is aborted, and the error is returned to the API caller with
success: false
. Despite this, the transaction around the Run Effects (which is on by default) will have already been committed, and any
changes made persisted to the database.
If you want to suppress reporting errors in your Success Effects, wrap your code in a try {} catch {}
statement to catch and log errors instead of letting them interrupt Action execution.
JavaScript1// some success effect snippet2module.exports = async ({ api, logger }) => {3 try {4 await makeSomeApiCall();5 } catch (error) {6 logger.error({ error }, "error running success effect API call");7 // don't rethrow the error so the API caller gets `success: true`.8 }9};
Handling errors in the client
Errors thrown during Run Effects or Success Effects are sent back from your application's API as errors
in the Action Result format. If you're making GraphQL requests with the Gadget API client, they'll be thrown as JavaScript Error
objects that you can catch. If you're making GraphQL requests with a different client, you will need to inspect the result from your GraphQL mutation to see if there are any errors
in the result JSON. For more information, consult the API Reference.
For debugging, errors are also logged when they occur. These logs are visible in the Gadget Log Viewer.
Action Transactions
Effect lists in Gadget, both Run Effects and Success Effects, can be wrapped in a database transaction. This means that all the changes made to the database by all the Effects in the list will either save together or abort together. This is a convenient way to program -- if some Effect fails, that Effect does not need to worry about reversing any changes from prior Effects. Instead, the Gadget database can undo anything that was underway, and restore a consistent state. Without this, only part of your changes might take effect (which creates lots of bugs) or your code must try to revert some of the changes manually (which is error-prone and unpleasant to code).
By default, Run Effects are transactional, and Success Effects are non-transactional. You can change these defaults by hitting the transaction button for a relevant effect stack.

These act as good defaults, but a transaction should only be open for a short time. Gadget will time out any transaction after 5 seconds. If an Action hits the transaction timeout, a GGT_TRANSACTION_TIMEOUT
error will be thrown, and the transaction will be aborted. You may want to disable transactions on an Effect stack if more than 5 seconds are needed to complete the Effects or refactor to use multiple Action executions where each is transactional.
Action Timeouts
Gadget Actions have a maximum execution duration of 3 minutes. Time spent running Run Effects and Success Effects must total under three minutes, or Gadget will abort the Action's execution. Actions are aborted by throwing the GGT_ACTION_TIMEOUT
error, interrupting Effect execution, and returning an error to the API caller.
This timeout applies to nested actions as well, such that all nested actions executed simultaneously must complete within the 3-minute timeout.
If any of the Effect stacks run during the action are transactional (see Action Transactions), Action timeouts will
also apply and are much shorter, at only 5 seconds. If you are receiving GGT_TRANSACTION_TIMEOUT
errors after 5 seconds, you must either
refactor your effects to be faster and complete within the 5-second transaction timeout or disable transactions with the transaction
toggle.
Default CRUD Actions
Each model starts with three base Actions: a create
, an update
, and a delete
Action. These Actions allow you to immediately add, change, and remove records from the database without customization. You can leave these Actions in place, remove them, or customize their behavior by adding and removing Effects.
A model with these base Actions will generate three mutations in your application's GraphQL API. For example, a model named Post
will generate the three createPost
, updatePost
, and deletePost
mutations, which will each trigger their respective Action when called.
create
Action
The default create
Action for your model does a few things:
- Creates a new
record
instance with the default values for any fields set - Applies incoming parameters from the GraphQL mutation to the
record
instance with theApply Params
Effect - Saves the
record
instance to the database with theCreate Record
Effect.
You can add more Effects to the create
Action's Effects to manipulate the record
or do other things. For example, supposed you want to add logic to default a certain field to a dynamically generated value. In that case, you can add a Run Code Effect to the Run Effects and update the record
instance before the Create Record
effect runs:
JavaScriptmodule.exports = async ({ record }) => {record.startDate ??= new Date();};
update
Action
The default update
Action for your model does this:
- Finds the given
record
instance based on the passed-inid
parameter - Applies incoming parameters from the GraphQL mutation to the
record
instance with theApply Params
Effect - Saves the changed
record
instance fields to the database with theUpdate Record
Effect
You can add more Effects to the update
action's Effects to manipulate the record
or do other things.
delete
Action
The default delete
Action for your model does this:
- Finds the given
record
instance based on the passed-inid
parameter - Permanently deletes the
record
from the database with theDelete Record
Effect.
If you want to log more details or update other records as a result of the delete, you can add more Effects to the delete
Action.
Beyond CRUD
Each model in Gadget can be customized to solve your specific problem. It's not needed, but if you want, you can change just about everything by adding and removing Actions and Effects!
You can add new Actions to an existing model by clicking the Add Action button. Your new Action will need an API Identifier that controls the name of the GraphQL mutation that will trigger it. New Actions start with a default set of Effects that can be changed or removed to create the specific behavior you need.
For example, let's say we're building the backend of a blog, and we have a Post model storing fields like title
and body
. Let's say we want a publishing workflow where posts can be worked on in the background for a while before being published and aren't visible to the audience until an author hits a publish button in an admin somewhere. You can do that in Gadget by adding new Actions to the Post model. We can add two Actions: one representing the Publish
Action and one representing the Unpublish
Action, which might correspond to buttons in our blogging frontend.
Other Actions like publish
action work just like create
, update
, or any other: you can run them via the API, you can grant or deny permissions on them, and you can make them run other business logic with Effects.
Effect library
Action Effects come in different forms. Gadget supports a handful of different Effect types:
- Create Record: instructs Gadget to create a new record for the current model
- Update Record: instructs Gadget to update a passed-in record for the current model
- Delete Record: instructs Gadget to delete a passed-in record for the current model
- Run Code Snippet: runs a JavaScript function to do anything else. To learn more about authoring Run Code Effects, refer to Extending with Code.
Default database Effects
Gadget has built-in database Effects that make it easy to persist a model. By default, Gadget installs a Create Record Effect to each model's Create Action, an Update Record Effect to the Update Action, and a Delete Record Effect to the Delete Action. You can remove default Effects if you like, though that may mean your Actions no longer accomplish what their names suggest they do. You can re-add these Actions without any special configuration by clicking the Add Effect button for the Action you'd like to add an Effect to.
Gadget's database effects are implemented using the Internal API. The Internal API is the low-level primitive that powers the higher-level Actions that each model starts with, so it makes sense that they use the low-level functionality to avoid accidental infinite recursion.
Run Code Effects
Gadget supports running arbitrary JavaScript code in Run and Success Effects. Add a Run Code Snippet Effect to an Action and your JavaScript function will be executed whenever the Action is executed. For more information on custom code Effects, refer to Extending with Code.
Global Actions
Global Actions run operations that are not tied to a model or particular record. For example, sending a weekly newsletter to all your users simultaneously is best modeled as a Global Action because it's not manipulating any single record -- it's a Global Action across many records. Global Actions can still fetch and manipulate record data, but Gadget doesn't automatically load a record for the Global Action to work on.
Global Actions are edited within the Global Action configuration page.
Global Actions support Run Effects and Success Effects the same way Model Actions do, and share the same transactional semantics. Global Actions won't have a model
or record
object in the contexts for their Action executions but will still have the api
, logger
, session
, connections
, and other key Effect context elements for use in your code.
Global actions vs HTTP routes
Gadget gives you the ability to add HTTP routes to your application. Global Actions are similar to HTTP routes in that they are not defined on a model and can use the Gadget project's api
to fetch data from one or more models before running some code.
There are a couple of key differences between Global Actions and routes to be aware of:
- Global Actions can be run as transactions. You can break your code into multiple code files and roll back any changes made to your Gadget database if a failure is encountered during the transaction.
- Global Actions can be run on a schedule.
- Global Actions are included in the Gadget app's generated GraphQL API. This means that they are also included in the generated JavaScript client, which makes authenticating requests and, in the case of Shopify apps, dealing with shop tenancy easier.
- Global Actions cannot serve frontend code (HTML, CSS, etc). Frontend hosting is only available on HTTP routes.