Actions

Useful apps do more than just store data. They accomplish useful 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 helpful for 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 and Preconditions are checked, and then the list of Effects is run to do the actual work.

This is the specific sequence that Gadget runs:

  1. A trigger starts the action. Triggers are often the GraphQL API mutation being run, but can also be things like webhooks from 3rd parties or a daily scheduled Action. Think of triggers as the when of running an action.
  2. 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.
  3. 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.
  4. Any Action Preconditions are checked. Preconditions determine the specific, customizable circumstances when an action can be run. This allows changing business logic to allow or prevent action execution before it really starts. For example, to prevent a blog from being able to publish posts on weekends, you would create a precondition on the Publish action that checks the current date to ensure that it is not a weekend. Preconditions are written as JavaScript functions.
  5. 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.
  6. 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.

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:

Configuring a scheduler trigger

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.

Action Preconditions

Action preconditions are individual chunks of code that answer the question: "can this action be run right now?". Preconditions can inspect fields on the record the action is attempting to run on, or pull other things from the context, like the currently logged-in user.

An action's preconditions are checked immediately upon an API call. If any of the action's preconditions are not met, the API will return an error instead of executing the action. Each precondition is written as a JavaScript function that runs on Gadget's platform. To learn more about coding preconditions, please refer to Extending with Code.

Preconditions are separate from Effects so that your API can answer the question "which actions can be run right now", without having to actually try to run the action. This is useful if you want to enable or disable buttons in a UI depending on if the action is currently available, or if you want to hide functionality from users who don't have permission to access them.

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 next 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:

JavaScript
module.exports = async ({ record }) => {
record.title ??= "New Record Title";
};

Or we could write a Run Effect to create a new record for a different model in our app:

JavaScript
1module.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 most 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, then 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:

JavaScript
module.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:

JavaScript
1const axios = require("axios");
2
3module.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.

JavaScript
1// some success effect snippet
2module.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 just 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

Toggling transactionality for an effects stack

These act as good defaults, but a transaction should not be open for a long 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), those timeouts will also apply and are a lot shorter, at 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 the Apply Params effect
  • Saves the record instance to the database with the Create Record effect.

You can add more effects to the create action's Effects to manipulate the record or do other things. For example, if you want to add logic to default a certain field to a dynamically generated value, you can add a Run Code Effect to the Run Effects and update the record instance before the Create Record effect runs:

JavaScript
module.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-in id parameter
  • Applies incoming parameters from the GraphQL mutation to the record instance with the Apply Params effect
  • Saves the changed record instance fields to the database with the Update 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-in id parameter
  • Permanently deletes the record from the database with the Delete 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. You don't need to, but if you want to, 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 actually 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 at the same time 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 that 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.