Global and model-scoped actions 

Gadget supports two different kinds of actions: model-scoped actions and globally-scoped actions.

Model-scoped actions 

Model-scoped actions are actions that run in the context of specific records within a model. Model-scoped actions are invoked on a particular record object from their model, and are passed that record each time they are run.

Actions that center around creating, updating, or deleting a specific record in the database are best expressed as model-scoped actions.

For example, in a CRM application, we might have a model representing a contact. If a salesperson wants to remind themselves to contact that person later, a developer might implement a model-scoped action called scheduleFollowUp on the contact model.

Because scheduleFollowUp concerns following up with a specific contact, this action should be a model-scoped action.

Model-scoped action ids 

Because model-scoped actions operate on specific records, they often take an id parameter in the auto-generated API. This id parameter is the id of the record that the action is being invoked on. Gadget will load the record with this id, and pass it as the record object in the action's context.

Create actions don't accept an id, as they are creating a new record. Instead, they get passed an unsaved record populated with the default values for each field.

Globally-scoped actions 

Globally-scoped actions are actions that do not operate in the context of a specific record or model. Globally-scoped actions aren't passed any record objects and don't accept any id parameters by default.

Actions that touch many records from many models, or that mainly interface with other systems are often best expressed as globally-scoped actions. If there isn't a clear central record, globally-scoped actions are generally recommended.

For example, in a CRM system, a developer might need to implement an action to send a weekly report to all the executives at a company. This report doesn't concern one particular user or one particular contact and instead relates to many different records. This globally-scoped action can still read data from other records, but because it doesn't concern one central one, it's easiest to implement as a globally-scoped action.

Globally-scoped actions vs model-scoped actions 

Globally-scoped actions are similar to model-scoped actions in that they operate using the action framework.

However, model-scoped actions are tied to a specific model and are used when you want to operate on a specific instance of that model. For instance, if you have a User model and you want to create an action that changes the user's password, you would use a model-scoped action. This action would be tied to a specific user and would operate on that user's data.

On the other hand, globally-scoped actions are not tied to any specific model, and are used when you want to perform an operation that doesn't necessarily relate to a specific instance of a model. For example, if you wanted to create an action that sends a newsletter email to all users, you would use a globally-scoped action. This action isn't tied to a specific user but rather operates on all users.

Consider the table below to summarize the key differences between model and global scoped actions.

FeaturesModel-scoped actionsGlobally-scoped actions
actionTypeCan be create, update, delete, or customNo action type
default returnTypeDefaults to falseDefaults to true
default paramsparams, record, logger, api, connectionsparams, logger, api, connections
SchedulingCannot be scheduledCan be scheduled
Sync triggerExecutes model-scoped actions on Shopify modelsCannot be used
Record in contextRecord is passed as a parameterRecord is not passed
Strict type support

The choice between a model-scoped action and a globally-scoped action depends on the specific requirements of the operation you want to perform. The suggestions above are a recommended convention. That being said, globally-scoped and model-scoped actions can mostly do the same things, so there is no restriction to follow this convention. However, it can be useful to follow these conventions, since some functions are easier to perform with a record or without.

To further illustrate this, consider this globally-scoped function that can update a model called file:

A global action to update a user record
JavaScript
import { applyParams, save, ActionOptions } from "gadget-server"; export const params = { recordId: { type: "string" }, fields: { type: "object", properties: { fileName: { type: "string" }, }, additionalProperties: true, }, }; export const run: ActionRun = async ({ params, logger, api, connections }) => { const record = await api.file.findOne(params.recordId || ""); if (params.fields) { Object.entries(params.fields).forEach(([key, value]) => { record[key] = value; }); } await save(record); };
import { applyParams, save, ActionOptions } from "gadget-server"; export const params = { recordId: { type: "string" }, fields: { type: "object", properties: { fileName: { type: "string" }, }, additionalProperties: true, }, }; export const run: ActionRun = async ({ params, logger, api, connections }) => { const record = await api.file.findOne(params.recordId || ""); if (params.fields) { Object.entries(params.fields).forEach(([key, value]) => { record[key] = value; }); } await save(record); };

This action should clearly be a model-scoped action, as it requires an id to function. In fact, the update action generated by the file model requires much less code as a result of this.

A model action to update a user record
JavaScript
import { applyParams, save, ActionOptions } from "gadget-server"; import { preventCrossUserDataAccess } from "gadget-server/auth"; export const run: ActionRun = async ({ params, record, logger, api, connections, }) => { applyParams(params, record); await preventCrossUserDataAccess(params, record); await save(record); }; export const options: ActionOptions = { actionType: "update", };
import { applyParams, save, ActionOptions } from "gadget-server"; import { preventCrossUserDataAccess } from "gadget-server/auth"; export const run: ActionRun = async ({ params, record, logger, api, connections, }) => { applyParams(params, record); await preventCrossUserDataAccess(params, record); await save(record); }; export const options: ActionOptions = { actionType: "update", };

Similarly, a model-scoped action could be used in place of a globally-scoped one, but it would be more expensive. For example, we could create a model-scoped action to send newsletters to all active subscribers. This would involve setting up a global action to trigger the model action, and the model action would need to run on every single user instance to determine if they are active before sending the newsletter. This is much more expensive than a globally-scoped action, which would filter for active users and then send the email.

Actions vs HTTP routes 

In addition to Actions, you can define HTTP Routes in your application. Both can execute backend logic and interact with your app’s API, but they serve different purposes.

FeatureActionsHTTP Routes
Database TransactionsSupports transactionsNo transaction support
SchedulingGlobal actions be scheduledCannot be scheduled
GraphQL APIIncluded automaticallyNot included
Response TypeJSON onlyAny content (JSON, HTML, images, files, etc.)
Error HandlingBuilt-inMust be handled manually
AuthenticationHandled automaticallyMust be handled manually
Frontend UsageCalled with useAction or useGlobalActionCalled with useFetch
Auto-documentedProvided for all your actionsNot provided for HTTP Routes

Gadget recommends using globally-scoped actions when possible because of the added benefits of being included in the Gadget app's GraphQL API.

Actions working together 

To better illustrate when to use model-scoped actions, globally-scoped actions, and HTTP routes, consider the following scenario:

You are building a website to inform users of new products from various online retailers and to provide reviews. The schema for this application includes the model ProductReview, which stores reviews for records from Product. Users are also able to sign up for the website and view reviews.

If you create a review for one of these products, a create model-scoped action could be used.

Now suppose that users could sign up to be emailed when a new product is available. To email all subscribed users, you would need to use a globally-scoped action to access multiple user records at once.

Finally, suppose you would like to implement a way for users to cancel subscriptions through text. To do this, you could implement an HTTP route, which listens for a Twilio webhook. The incoming message and user's phone number can be extracted from the webhook, and a model action can be called to cancel the user's subscription.

Was this page helpful?