Action code 

Most of the logic of your application will end up living as code in action files. action code can change the database, change the requester's session, make outgoing API calls, and really, do anything!

An example action file
1import { applyParams, save } from "gadget-server";
3export default async function run({ api, record, params }) {
4 applyParams({ record, params });
5 await save({ record });
8export const options = {
9 actionType: "update",

Action file exports 

An action file can export four pieces: a run function, an onSuccess function, an options object, and a params object.

  • The run function executes the main body of the action
  • The optional onSuccess function executes when the whole action group's run functions complete
  • The optional options object configures the action's details, like action type, transactionality and timeouts.
  • The optional params object configures the schema of any extra parameters the action accepts.

run function 

run functions are for the main body of logic that an action should run. Calls to write data to the database generally belong in a run function.

1import { save } from "gadget-server";
3export async function run({ record }) {
4 record.title ??= "New Record Title";
5 await save(record);

The run function is passed an action context object describing everything Gadget knows about this action execution, like the incoming parameters, the record the action is running on, the current connections, a logger, and more.

For example, if we have a model named post, we can apply the incoming parameters from an API call to the record instance when a new post is created:

1import { save, applyParams } from "gadget-server";
3export async function run({ record, params }) {
4 applyParams(record, params);
5 await save(record);

For more on the context object passed to actions, see the gadget-server reference.

onSuccess function 

The onSuccess function runs once the run function has executed successfully. onSuccess is not run if your action's run function throws, or if one of the other actions being executed at the same time throws.

import { save, applyParams } from "gadget-server";
export async function onSuccess({ record, params, logger }) {{ record, params }, "post successfully created!");

onSuccess functions are often useful for sending data to other systems, like an e-commerce platform, a hosted search service, or any other third party. Similarly, sending emails or push notifications is generally done in an onSuccess function so you can be those notifications are talking about events that actually saved to the database.

1import twilio from "some-twilio-client";
2export async function run() {
3 // ...
6export async function onSuccess({ record }) {
7 await twilio.sendSMS({
8 to: record.phoneNumber,
9 from: "+11112223333",
10 body: "Notification from a Gadget app!",
11 });

Why not just do everything in run? 

It's generally important to ensure that the changes you are making to your local Gadget database have been committed successfully before informing those other services that changes have been made. If third-party service API calls are made in the run function, there is still a chance that the database transaction will be aborted and rolled back if something else fails. 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 function, and changes to a third-party system belong in onSuccess.

Errors in actions 

Errors thrown during the run or onSuccess function will cause the action to stop processing and will appear in your logs. Errors descriptions will be sent back from your application's autogenerated API as well. When using the api object, they'll be thrown as JavaScript Error objects that you can catch. Throwing an error will also stop any sibling actions, and will prevent any nested actions from starting.

If you are running code that you expect might error in your action, you can wrap it in a try/catch statement:

1import { save, applyParams } from "gadget-server";
3export async function run({ record, params, logger }) {
4 try {
5 await callUnreliableAPI();
6 } catch (error) {
7 logger.error({ error }, "error encountered");
8 }
9 // continue processing ...

Transaction rollback from errors 

If your action is transactional and an error is thrown in the run function, the database transaction will be rolled back, and any changes made to the database won't take effect.

If your action is not transactional, or if errors are thrown in the onSuccess function, any changes made so far in the action will have been committed.

For debugging, errors are also logged when they occur. These logs are visible in the Gadget Log Viewer.

Action transactions 

By default, the run function of your action is wrapped in a database transaction. This means that either all of the changes made to any record in the run function will commit to the database, or none will if an error is encountered.

To control if transactionality is on or off, export the transactional option:

1export async function run({ record, params, logger }) {
2 // ...
5export const options = {
6 transactional: false, // disable transactions for this action

Transactionality is on by default for run functions. To ensure performance, Gadget will time out any transaction after 5 seconds. If an Action exceeds the transaction timeout, a GGT_TRANSACTION_TIMEOUT error will be thrown, and the transaction will be aborted and rolled back.

To avoid hitting transaction timeouts, you can disable transactionality for an action that takes longer than 5 seconds, or split it up into several smaller actions, each of which is transactional.

Action timeouts 

Gadget actions have a default maximum execution duration of 3 minutes. Time spent running run and onSuccess functions must total under three minutes, or Gadget will abort the action's execution by default. Actions are aborted by throwing the GGT_ACTION_TIMEOUT error 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.

Action timeouts can be increased by exporting the timeoutMS option:

1import { save, applyParams } from "gadget-server";
3export async function run({ record, params, logger }) {
4 // fast stuff within 5s transaction timeout
7export async function onSuccess({ record, params, logger }) {
8 // slow stuff
11export const options = {
12 timeoutMS: 600000, // 10 minutes (600,000 milliseconds) for a really long action

Action timeouts can be a maximum of 15 minutes (900,000 milliseconds).

If your action is set to be transactional (which is the default), the run function must finish within 5 seconds. This limit can't be changed. If you want to use longer timeouts, put your logic in onSuccess, or export the transactional: false option.

Default model actions (CRUD) 

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 code within their file.

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 applyParams function
  • Saves the record instance to the database with the save function.

You can add more code to extend the functionality to the create action 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. In that case, you can add some code to the run function and update the record instance before the save utility runs:

export default async function ({ record }) {
record.startDate ??= new Date();
// ... rest of the action

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 applyParams utility
  • Saves the changed record instance fields to the database

You can add more code to the update action's code 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 deleteRecord utility.

If you want to log more details or update other records as a result of the delete, you can add more code to the delete action.

Action context 

Each time an action runs, it gets passed one argument describing the current execution. Most Gadget users use JavaScript destructuring to pull out just the context keys that they require in their action function.

1export async function run({ params, logger, api }) {
2 // ...
5// in models/actions/post/update.js
6export async function run({ record, params, logger, connections }) {
7 // ...

The context object passed into each function has the following keys:

  • api: A connected, authorized instance of the generated API client for the current Gadget application. See the API Reference for more details on this object's interface.
  • params: The incoming data from the API call invoking this action.
  • record: The root record this action is operating on (only available in model actions).
  • session: A record representing the current user's session, if there is one.
  • config: An object of all the environment variables created in Gadget's Environment Variables editor.
  • connections: An object containing client objects for all connections. Read the connections guide to see what each connection provides.
  • logger: A logger object suitable for emitting log entries viewable in Gadget's Log Viewer.
  • model: An object describing the metadata for the model currently being operated on, like the fields and validations this model applies.
  • request: An object describing the incoming HTTP request that triggered this action, if it was an HTTP request that triggered it.
  • currentAppUrl: The current URL for the environment. e.g.
  • trigger: An object containing what event caused this action to run.

Global actions will be passed everything in the context object except a record or a model.

If you have a computed field associated with your model, it is not included within the context, and must be manually loaded into the action. For more information on how to do so, see our computed fields guide.


For model actions, Gadget automatically loads the record that the action is being taken on from the database at the start of the action. The record is stored in the record key on the action context, so it is there if you'd like to access it.

export async function run({ record }) {
console.log(; // will log the record's id
console.log(record.firstName); // will log the value of the firstName field loaded from the database

You can change properties of the record like normal JavaScript objects:

export async function run({ record }) {
record.firstName = "New name";

Note that the record is not automatically saved. To save the record, use the save function from gadget-server:

import { save } from "gadget-server";
export async function run({ record }) {

The save function will validate the record and persist it into your Gadget app's database.

The record passed to an action may not be persisted yet. In the case of an actionType: "create" action, there will be a record in the context, but it won't have an id assigned yet. To persist the record and assign it an id, call save(record).


The api object that gets passed into an action is an instance of the generated JavaScript client for your Gadget application. It works the same way in an action as it does elsewhere and has all the same functions documented in the API Reference. You can use the api object to fetch other data in an action:

1// example: send a notification to the user who owns the record if they've opted in
2const twilio = require("../../twilio-client");
3module.exports = async ({ api, record }) => {
4 const creator = await api.users.findOne(record.creator._link);
5 if (creator.wantsNotifications) {
6 await twilio.sendSMS({
7 to: creator.phoneNumber,
8 from: "+11112223333",
9 body: "Notification: " + record.title + "was updated",
10 });
11 }

Or, you can use the api object to change other data in your application as a result of the current action:

1// example: save an audit log record when this record changes
2module.exports = async ({ api, record, model }) => {
3 const changes = record.changes();
5 // run a nested \`create\` action on the \`auditLog\` model
6 await api.auditLogs.create({
7 action: "Update",
8 model: model.apiIdentifier,
9 record: { _link: },
10 changes: changes.toJSON(),
11 });

Using the Public API vs the Internal API 

Your application has two different levels of API: the Public API and the Internal API. The Public API runs the high-level actions you've defined in your app, and the Internal API runs low-level persistence operations that just change the database.

Depending on what you're trying to accomplish with your action, it is sometimes appropriate to use the Public API of your application via api.someModel.create, and sometimes it's appropriate to use the Internal API via api.internal.someModel.create.

Generally, the Public API should be preferred. The Public API for a Gadget application invokes model actions and global action, which means your business logic runs. This means that if you call api.someModel.create() within an action, you are invoking another action from within the currently invoking one. This is supported by Gadget and is often the right thing to do if your business logic needs to trigger other high-level logic.

Sometimes, though, you may want to skip executing all the business logic, and just persist data. The Internal API does just this. The Internal API can be used by running api.internal.someModel.<action name>. You can think of the Internal API like raw SQL statements in other frameworks where you might run explicit INSERT or UPDATE statements that skip over the other bits of the framework.

The Internal API is significantly faster and is often used for building high-volume scripts that need to import data or touch many records quickly. However, because the Internal API skips important logic for actions, it is generally not recommended for use unless you have an explicit reason.

Action infinite loops 

Be careful when invoking actions within other actions, especially if invoking the same action from within itself.

Invoking actions within actions can cause infinite loops, so care must be taken to avoid this. If you invoke the currently underway action from within itself, your run function will run again, potentially invoking the action again in a chain. Or, if you invoke an action in model B from model A, but model B invokes an action in model A, you can create a chain of action invocations that never ends. Action chains like this can show up in the logs as timeouts or errors that happen at an arbitrary point in an action.

To avoid infinite loops, you must break the loop somehow. There are a couple of options for this:

  • Use record.changed('someField') to only run a nested action some of the time when data you care about has changed
  • Use the Internal API instead of the Public API (api.internal.someModel.update instead of api.someModel.update) to make changes, skipping the action that retriggers the loop

Connection Action infinite loops 

Be careful when invoking remote APIs within actions if those remote APIs might fire webhooks that call back to your application, re-invoking the same action.

Calling third-party APIs from your actions is common, but it's important to be aware that third-party APIs may trigger subsequent actions in your Gadget app.

For example, if you have a Gadget app connected to a Shopify store with a connected shopifyProduct model, every time Shopify sends the app a product/updated webhook, Gadget will run the update action. If the business logic inside the update action makes API calls back to Shopify to update the product again (say to update the tags of a product in response to a change in the other fields), the API call back to Shopify will trigger a second webhook sent back to Gadget. Without care, this can cause an infinite loop of actions triggering webhooks that retrigger the actions.

See Action infinite loops for strategies to break this infinite loop.


Each action is passed a logger object conforming to the logger API for logging to Gadget's built-in Log Viewer.

1export async function run({ api, record, logger }) {
2 if (!record.someField) {
3 logger.error({ record }, "record is missing required field for processing");
4 } else {
5{ recordID: }, "processing record");
6 doImportantThingWithRecord(record);
7 }

Read more about the logger object in the Logging guide.


Each action is passed a trigger object that contains information related to what event triggered the action to run. The trigger object is different for each type of trigger that can call an action and has a type object describing which type of trigger fired.

export async function run({ api, record, trigger }) {
console.log(trigger); // will log an object like { "type": "api", "rootModel": "someModel", "rootAction": "someAction", etc ... }

See the Triggers guide for more information on the different types of triggers.


The connections object in an action context contains prebuilt client objects for making remote API calls. This is useful for easily reading or writing data in supported Connection systems like Shopify without having to manage API clients or credentials.

For example, suppose you have a Shopify connection to your store and you want to update a product. You could write an action that creates a Shopify client for this shop, and makes an API call to Shopify like so:

1export async function run({ api, record, connections }) {
2 const shopifyClient = await connections.shopify.forShopDomain(
3 ""
4 );
5 await shopifyClient.product.update(productId, productParams);

Visit the Connections guide to learn more about each of the specific clients.

It's important to keep in mind that changing your remote data could result in an update webhook being sent back to Gadget. Care will need to be taken to ensure you don't get into an infinite feedback loop with the remote system. One good way to ensure this doesn't happen is to make remote updates in the onSuccess function. This guarantees the record has been committed to the database, so you can check the incoming webhook to see if a relevant field has changed before making the remote call.

1export async function onSuccess({ api, record, connections }) {
2 // only do the remote update if the body field has changed
3 if (record.changed("body")) {
4 const shopifyClient = await connections.shopify.forShopDomain(
5 ""
6 );
7 await shopifyClient.product.update(productId, productParams);
8 }


Each action is passed a request object describing the HTTP request which triggered this action. Not all actions are triggered by HTTP requests (like scheduled actions), but for those that are, this will be present. request has the requester's ip (also known as the x-forwarded-for header), the userAgent of the requester, and all the other request headers sent in the request.

export async function run({ api, record, trigger, request }) {
console.log(; // will log the client making this API requests's IP
console.log(request.headers["authorization"]); // will log the incoming Authorization request header

See all the available properties on the RequestData type in the Reference.


Each action is passed an emails object, which is an instance of the GadgetMailer (a Gadget wrapper for NodeMailer). emails is primarily used to handle and facilitate the sending of emails.


Each action is passed a signal object, which is an instance of AbortSignal. signal is primarly used to identify when the underlying request object becomes closed or aborted.


To reiterate, when we discuss signals aborting we are talking about the idea that a request would abort (in an async context) and an AbortSignal is a readonly representation of that. As a result, whenever you make a request (i.e. a call to your api) it could abort and it is encouraged to check for this. Also, when making an external api call it could also abort.

In practice the reasons for aborting could be timeout (i.e. calling the external api could take longer than your timeout) or any expected error on the request.

Example: aborting an action call early
1export async function run({ api, record, request, signal }) {
2 await (dog) => {
3 if (dog.age < 2) {
4 await{
5 id:,
6 description: `${} is a young dog`,
7 });
9 // A signal could potentially be classified as aborted from a gadget side operation
10 if (signal.aborted) return;
11 }
12 });
14 // This is a feature of fetch which allows you to pass a signal. If we timeout on the gadget server (the call took too long) then fetch will immediately cancel.
15 const cats = fetch("someAPI/cats", { signal });
17 // a signal could be aborted from an external api call
18 if (signal.aborted) return;
20 await api.internal.cats.bulkCreate(cats);

Action options 


The actionType option set for each action determines how a model action is exposed in the GraphQL API.

export const options = {
// mark this action as a record creator
actionType: "create",

The actionType option can be one of the following:

  • create: The action is exposed as a create mutation in the GraphQL API. creates don't accept an id parameter, but do accept parameters for each model's field. If any model fields have a Required validation, those fields must be passed to the create mutation. creates return the newly created record.
  • update: The action is exposed as an update mutation in the GraphQL API. updatess require being passed an id parameter, and also accept optional parameters for each model's field. updates return the updated record.
  • delete: The action is exposed as a delete mutation in the GraphQL API. deletess require being passed an id parameter, and don't accept or also accept optional parameters for each model's field. Deletes don't return any data.
  • custom The action is exposed in the GraphQL API, with the rest of the params up to you. customs require being passed an id parameter, and take no other params by default. customs return the record they were invoked on.

Global Actions don't have an actionType option, since they aren't exposed in the GraphQL API.


Allows specifying the maximum amount of time (in milliseconds) an action can run before being terminated. This is useful for preventing runaway actions from consuming too many resources. The default timeout is 3 minutes seconds.

export const options = {
timeoutMS: 1000, // allow this action to take 1s before throwing a GGT_TIMEOUT_ERROR


Specifies if the action's run function should execute within a database transaction or not. Is true by default.

export const options = {
transactional: false, // disable transactions for this action


Specifies whether the action should return the result of the run function. Defaults to false for model actions and true for global actions.

export const options = {
returnType: true, // enable the ability to return a value from the `run` function of the action

Let's take a look at a simple example of how to return a value from a run function when setting returnType to true.

1export async function run({ params, record, logger, api, session }) {
2 applyParams(params, record);
3 await save(record);
4 return {
5 // returning a string
6 result: "successfully returned",
7 };
10export async function onSuccess({ params, record, logger, api, emails }) {}
12export const options = {
13 actionType: "custom",
14 returnType: true, // Setting returnType to `true` as it is false by default on a model action
15 triggers: {
16 sendResetPassword: true,
17 },

Accepting more parameters 

You can add new parameters to your actions beyond the autogenerated defaults that Gadget adds to your action by exporting the params object from your action file:

1export const params = {
2 sendNotifications: { type: "boolean" },
5export async function run({ params }) {
6 if (params.sendNotifications) {
7 // ...
8 }

This is useful for actions that require parameters to change their behavior, but that you don't want to store on the model.

Gadget expects the exported params object to be a subset of a JSON schema specification. For example, if you want to expose a boolean flag and name object in your action or global action:

1export const params = {
2 fullName: {
3 type: "object",
4 properties: {
5 first: { type: "string" },
6 last: { type: "string" },
7 },
8 },
9 emailMarketing: { type: "boolean" },
12export async function run({ record, params }) {
13 // params.emailMarketing will be a boolean
14 // params.fullName will be an object like { first: "Jane", last: "Dough" }
15 if (params.emailMarketing) {
16 // ...
17 }

With that in place in your action code, you can now make the following api call:

await api.customer.create({
emailMarketing: true,
fullName: { first: "Jane", last: "Dough" },

Gadget currently supports the following types from JSON schema:

  • object
  • string
  • integer
  • number
  • boolean
  • array

Only these primitive types are currently supported by Gadget. No other features of JSON schema are currently available, so don't use validations, required and/or schema composition primitives like allOf. If you want to validate parameter values, you can do so in the code of your action.

Nested Actions 

Every action supports calling actions on associated models in its input. This is useful, for example, to create a model and some associated models without having to make API calls for each individual record.

For example, suppose you're modeling a blog where each post has many comments. If we wanted to create a blog post with some comments, it would look something like this with nested actions.

2 title: "My First Blog Post",
3 author: {
4 _link: "1",
5 },
6 body: "some interesting content",
8 {
9 create: {
10 body: "first comment!",
11 author: {
12 _link: "2",
13 },
14 },
15 },
16 {
17 create: {
18 body: "another comment",
19 author: {
20 _link: "3",
21 },
22 },
23 },
24 ],
1mutation CreatePost($post: CreatePostInput) {
2 createPost(post: $post) {
3 success
4 errors {
5 message
6 }
7 post {
8 id
9 }
10 }
2 "post": {
3 "title": "My First Blog Post",
4 "author": { "_link": "1" },
5 "body": "some interesting content",
6 "comments": [
7 { "create": { "body": "first comment!", "author": { "_link": "2" } } },
8 { "create": { "body": "another comment", "author": { "_link": "3" } } }
9 ]
10 }

Note that there's no need for a { _link: "…" } parameter to link the blog post and comments.

How are nested actions executed? 

When you invoke an action with an input that has nested actions, Gadget will execute the actions in the right order to feed the right values for relationship fields of subsequent inner actions. In the above example, Gadget would create the blog post first and get its ID value to then send into the comment create actions.

When a group of nested actions is executed, all the constituent actions' run functions are executed together within one database transaction, and then the actions' onSuccess are executed second if all the run functions successfully execute. This means that Gadget runs all the run functions for the root action and nested actions first, then commits the transaction, then executes onSuccess functions.

If any actions throw an error during the run function, like say if a record is invalid or a code snippet throws an error, all the actions will be aborted, and no onSuccess function for any action will run.

The rule of thumb is, a run function that fails on any action rolls back changes from all other actions.

Converge actions 

Gadget supports a special nested action called _converge that creates, updates, or deletes records in the database as needed to match an incoming specified list. Often when updating a parent object and list of child objects at the same time, it is cumbersome to exactly specify the individual operations you want, and instead, it's easier to just specify the new list. _converge figures out the individual operations that take you from the current state in the database to the new specified list. This is most frequently used on forms that edit a parent and child records at the same time so you can just send the new list of child records specified in the form, instead of tracking which things have been added, removed, and changed.

The _converge action will process creates, updates, and deletes using actions with the api identifiers create, update, and delete by default, so any effects added to those actions will run as Gadget converges the record list. You can override which actions Gadget uses to implement the converge using the actions property, see Invoking custom actions to converge for more details.

Run a converge call by specifying _converge as the action name in a nested action input, and passing a new list of records in the values property like so:

2 "_converge": {
3 "values": [
4 // will update record with id 1
5 { "id": "1", "name": "first" },
6 // will create a new record
7 { "name": "second" }
8 // other records not present in this list will be deleted
9 ]
10 }

For example, suppose you're editing a blog post where each post has many images. If you wanted to build a client side form where users could remove some of the images that exist already, and then add a couple new ones, you can use _converge to create, update, and delete the images as needed to get to the new list. Without _converge, you'd have to manually track the image ids that have been deleted or updated to build the nested actions data format yourself.

Instead, we can converge the list of child images for the post to the newly specified list.

1await"123", {
2 title: "Updated Blog Post",
3 body: "some interesting content",
4 images: [
5 {
6 _converge: {
7 values: [
8 {
9 caption: "Mountains",
10 url: "",
11 },
12 {
13 id: "10",
14 caption: "Oceans",
15 url: "",
16 },
17 ],
18 },
19 },
20 ],
1mutation UpdatePost($id: GadgetID!, $post: CreatePostInput) {
2 updatePost(id: $id, post: $post) {
3 success
4 errors {
5 message
6 }
7 post {
8 id
9 }
10 }
2 "post": {
3 "title": "Updated Blog Post",
4 "body": "some interesting content",
5 "images": [
6 {
7 "_converge": {
8 "values": [
9 { "caption": "Mountains", "url": "" },
10 {
11 "id": "10",
12 "caption": "Oceans",
13 "url": ""
14 }
15 ]
16 }
17 }
18 ]
19 }

The _converge call above will create a new image with the caption "Mountains" using the Image.create action, and update an existing image with id "10" to have the caption "Oceans" using the Image.update action. It will also delete any images in the database which aren't specified in this list using Image.delete, so if there was previously an image with id "9" labelled "Skies", it'd be deleted.

Invoking custom actions to converge 

The _converge action will process creates, updates, and deletes using actions with the api identifiers create, update, and delete by default. You can override which actions Gadget uses to implement the converge using the actions property of the passed object like so:

2 "_converge": {
3 "values": [
4 // ...
5 ],
6 "actions": {
7 // will create any new records using the publicCreate action of the child model
8 "create": "publicCreate",
10 // will update any existing records using the specialUpdate action of the child model
11 "update": "specialUpdate"
12 }
13 }

The actions override list is optional and partial, so you can just specify the actions you want to override, or just omit the actions property entirely.