# Action code  Most of the application's backend logic resides in action files. Action code can modify the database, manage the requester's session, make API calls, and perform various other operations. ```typescript import { applyParams, save, ActionOptions } from "gadget-server"; export const run: ActionRun = async ({ api, record, params }) => { await applyParams(record, params); await save(record); }; export const options: ActionOptions = { actionType: "update", }; ``` ## Default actions  By default, any models you create come pre-packaged with three default actions, `create`, `update`, and `delete`. These actions will interact with your application's GraphQL API to handle record mutations. The default `create`, `update`, and `delete` actions in Gadget handle basic record operations like creating, updating, and deleting records using utilities like `applyParams`, `save`, and `deleteRecord`. These actions can be extended with custom logic to manipulate records or perform additional tasks before saving or deleting. For example, you can add default values in the `create` action or log changes in the `update` action. In addition to these default actions, if both `create` and `update` actions exist on a model, Gadget will also automatically include an `upsert` action. This action can be used to `create` or `update` records based on their unique fields. For more advanced usage, you can explore the [`upsert` meta action](https://docs.gadget.dev/guides/actions/code#upsert-meta-action), which offers more control over the upsert process. ## Action file structure and exports  An action file can export four pieces: a `run` function, an `onSuccess` function, an `options` object, and a `params` object. Only the `run` function is required — `onSuccess`, `options`, and `params` are optional. * The executes the main body of the action * The executes when the action's `run` function completes successfully * The configures the schema of any extra parameters the action accepts * The configures the action's details, like action type, transactionality and timeouts ### `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. In model actions the `run` function is transactional by default and requires that it completes within a 5 second timeout. ```typescript import { save } from "gadget-server"; export const run: ActionRun = async ({ record }) => { // Explicitly setting the title field of the record record.title = "New Record Title"; await save(record); }; ``` A transaction is a group of database operations that are executed as a single unit. If any of the operations fail, the entire transaction is rolled back. If all of the operations succeed, the transaction is committed. This means that if an error occurs during your `run` function, all changes made to the database, for example `save(record)` or writes using `api`, will be rolled back. The `run` function is passed an 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: ```typescript // Importing necessary functions from the "gadget-server" library import { save, applyParams, ActionRun, ActionOptions } from "gadget-server"; // The 'run' function is executed when the action is triggered export const run: ActionRun = async ({ record, params }) => { // Apply the incoming parameters to the record applyParams(record, params); // Save the updated record to the database await save(record); }; ``` For more on the context object passed to actions, see the [`gadget-server` reference](https://docs.gadget.dev/reference/gadget-server#action-contexts) or [writing actions](https://docs.gadget.dev/guides/actions/writing-actions#action-context). #### Before and after save  In all `run` functions, you may notice that there is a line with `await save(record)`. ```typescript import { applyParams, save, ActionOptions } from "gadget-server"; export const run: ActionRun = async ({ params, record, logger, api, connections, }) => { applyParams(params, record); await save(record); }; ``` This function does exactly what it sounds like - it saves any changes made to a record. However, it is important to differentiate what is done before and after `save`. Generally, any modification you want to apply to the record should be done before save. This includes applyParams, or any actions or modifications that should be made. For example: ```typescript record.status = "pending"; record.updatedBy = { _link: currentUser.id }; await record.save(); ``` If you make any modifications after save, you will need to call the function again to preserve these new changes, which can lead to poorer performance. Additionally, any checks on the ability to run an action should be performed before save. For instance, you may want to check that you are within a billing plan's limit before saving anything that may cause you to exceed the plan and pay more. However, if you are creating a new record, and want to modify it immediately afterwards, this modification will need to be done after the save, otherwise you would not have access to the new record's id to perform updates. ### `onSuccess` function  The `onSuccess` function runs once the `run` function has executed successfully. It will not run if: * The `run` function throws an error. * Another action being executed as part of the same transaction throws an error. * It is not included and exported in the action file. Errors in the `onSuccess` function will not rollback the transaction completed in the `run` function. `onSuccess` functions are often useful for running business logic, like sending emails or charging a card, after your action's database work has completed successfully. Doing so ensures that important business logic is not prematurely triggered (e.g you avoid alerting your subscribers to a new blog post when the blog post record failed to save in the database). For example, if an action triggers multiple related actions and one of them throws an uncaught error (i.e., not handled with `try/catch` or a .catch() on a promise), none of their `onSuccess` functions will execute. However, if errors are caught and handled by the user, the `onSuccess` functions can still run. Lets view some examples to further demonstrate the `onSuccess` function's capabilities. ### Custom cascading delete  In this example, a blog post has multiple images (a hasMany relationship). If either the blog post or any of its images fails to delete, the onSuccess function will not run. ```typescript import { deleteRecord, ActionOptions } from "gadget-server"; // Define the 'run' function which handles the deletion of the blog post and its associated images export const run: ActionRun = async ({ record, logger }) => { /** * Delete all images associated with the blog post by using the blogPostId (using the internal API). * If this api call fails, the run function will throw an error, and stop the onSuccess function from running. */ await api.internal.blogPostImage.deleteMany({ filter: { blogPostId: record.id }, }); // Now delete the blog post itself await deleteRecord(record); }; // Define the 'onSuccess' function to run after the 'run' function completes successfully export const onSuccess: ActionOnSuccess = async ({ record, logger }) => { // Log a success message after the blog post is deleted logger.info("Blog deleted"); }; export const options = { actionType: "delete", }; ``` The `run` function deletes all images associated with the blog post first and then deletes the blog post itself. If either deletion fails (e.g., due to a database error or constraint violation), `run` throws an error, and `onSuccess` will not execute. #### Why not just do everything in `run`?  While you can place all logic inside the `run` function, doing so can lead to longer wait times for users, especially when dealing with complex interactions or third-party API calls. By default, the `run` function in model actions is **transactional** and must complete within a **5-second timeout**. This transaction timeout cannot be increased. Global actions are non-transactional by default and don't have this 5-second limit. If you need longer-running logic in a model action, you can: * Move time-consuming work to `onSuccess`. * Set . Note: database changes won't be atomic when `transactional: false` is set. By separating out logic that doesn't need to run immediately into `onSuccess`, you: * Improve performance by offloading non-critical tasks. * Ensure that external services or APIs are only notified after the database transaction has successfully completed, avoiding bugs where external systems might act on inconsistent data. In summary, keep database work in `run` and business logic, such as sending emails or charging a card, in `onSuccess` to prevent premature business logic triggers. | Method | Usage Scenario | | --- | --- | | `run` | Use when you want to execute an action immediately or work on data in the database. Best for processing user input or event-based triggers. | | `onSuccess` | Use when you need to execute a follow-up task only if the action completes successfully. Ideal for handling post-processing or making third-party API calls. | ### Adding parameters to actions  You can enhance your model-scoped or globally-scoped actions by adding new parameters. To do this, export a `params` object from your action file. ```typescript export const params = { sendNotifications: { type: "boolean" }, }; export const run: ActionRun = async ({ params }) => { if (params.sendNotifications) { // ... logic to send notifications } }; ``` In this example, we are defining a new parameter called `sendNotifications` of type `boolean`. This parameter can be accessed within the `run` function of your action, which is used to handle immediate tasks, like database interactions. If the task requires additional steps to be performed after the action has successfully completed, such as sending notifications, you can use the `onSuccess` function. By structuring your logic this way, you ensure that time-consuming or non-essential tasks (such as sending notifications) are handled in the `onSuccess` function, improving performance and keeping your `run` function focused on critical operations. Gadget expects the exported params `object` to adhere to a subset of the JSON schema specification. Currently, the JSON schema types supported for both model-scoped and globally-scoped actions are `object`, `string`, `integer`, `number`, `boolean`, and `array`. Only these primitive types are currently supported by Gadget. Other JSON schema features like validations, required, and schema composition primitives (allOf) are not yet available. If you need to validate parameter values, you can implement this logic within your action's code. For more information on JSON schema, visit the [JSON Schema website](https://json-schema.org/docs). #### Adding parameters to globally-scoped actions  By default, globally-scoped actions do not have any parameters. You'll often need to define your own params object to accept input. Let's imagine you're building a tool that processes widgets. You might want a globally-scoped action to handle this, allowing users to provide a name (`foo`) and a quantity (`bar`). Here's how you'd define the parameters: ```typescript // Define parameters for this action in the module scope export const params = { foo: { type: "string" }, bar: { type: "number" }, }; export const run: ActionRun = async ({ params, logger, api, connections }) => { // Access the 'foo' and 'bar' parameters here logger.info({ foo: params.foo, bar: params.bar }, "This is foobar"); // ... your action logic }; ``` This globally-scoped action, named `processWidgets` in this example, can be called with the defined parameters like this: ```typescript // Calling the globally-scoped action with parameters await api.processWidgets({ foo: "hello", bar: 10 }); ``` #### Adding parameters to model-scoped actions  Model-scoped actions automatically include a set of parameters based on the action type and the model's fields. You can add additional `params` for custom behavior or when you need parameters that are not directly stored on the model. This is particularly useful for `custom` model-scoped actions. For instance, let's say you have a `Student` model and you want to create a custom action to suspend a student for a specific duration. You can add a `suspensionLength` parameter: ```typescript import { applyParams, save, ActionOptions } from "gadget-server"; import { getUpdatedDate } from "../utils"; // Define a new parameter for the suspend action export const params = { suspensionLength: { type: "number" }, }; export const run: ActionRun = async ({ params, record, logger, api, connections, }) => { applyParams(params, record); // Mark the student as suspended record.isSuspended = true; // Schedule a background action to un-suspend the student await api.enqueue( api.student.update, { id: record.id, isSuspended: false, }, { startAt: getUpdatedDate(params.suspensionLength), } ); }; export const options: ActionOptions = { actionType: "custom", }; ``` This custom `suspend` action for a `Student` record can be called with the `suspensionLength` parameter: ```typescript // Calling the model-scoped action with the custom parameter await api.student.suspend("123", { suspensionLength: 3 }); ``` #### Adding `Object` parameters  For more complex scenarios, you can define parameters of type `object`. This allows you to accept structured data as input to your actions. The `object` param type uses the `properties` key to define the expected fields within the object and their respective types. Here's an example where we define a fullName object, which contains first and last name strings: ```typescript // define a fullName object parameter export const params = { fullName: { type: "object", properties: { first: { type: "string" }, last: { type: "string" }, }, }, }; export const run: ActionRun = async ({ record, params, logger }) => { // Access the properties of the fullName object logger.info({ firstName: params.fullName.first, lastName: params.fullName.last }); }; ``` With this definition, you can make API calls with the `fullName` object parameter: ```typescript // Calling the action with an object parameter await api.myAction({ fullName: { first: "Jane", last: "Doe" } }); ``` Using the `properties` key is the preferred method for defining object parameters, as it allows for your api to remain strongly typed. However, if you want to add a parameter that allows for an arbitrary object shape, you can use the `additionalProperties` key. ```typescript // define a data object param that allows for any object shape export const params = { data: { type: "object", additionalProperties: true }, }; export const run: ActionRun = async ({ params, logger }) => { // params.data will be an object with type Record logger.info({ data: params.data }); }; ``` #### Adding array parameters  Arrays can be added as parameters to actions, allowing for more complex inputs. The `array` param type requires an `items` key with a mapping of the items in the array to their types. Each item in the array can be a primitive type or an `object`. For example, if you want to allow a list of `customerIds` to be passed into an action: ```typescript export const params = { customerIds: { type: "array", items: { type: "string" } }, }; export const run: ActionRun = async ({ record, params }) => { // params.customerIds will be an array of strings for (const customerId of params.customerIds) { // ... do something with each customer id } }; ``` With that in place in your action code, you can now make the following API call: ```typescript await api.myAction({ customerIds: ["123", "456"] }); ``` ### `options` object  Action options provide configurations for how your action is executed. Some of these options, such as triggers, can be set in the Gadget GUI. The rest may be set within the action code. Generally, those that can be set within action code are used less frequently. Action options will be found at the bottom of the default action file and provide configuration settings for how your action is executed. #### `actionType`  The `actionType` option set for each action determines how a model-scoped action is exposed in the GraphQL API. ```typescript import { ActionOptions } from "gadget-server"; export const options: ActionOptions = { // 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. `create`s do not 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. `create`s return the newly created record. * `update`: The action is exposed as an `update` mutation in the GraphQL API. `updates`s require being passed an `id` parameter, and also accept optional parameters for each model's field. `update`s return the updated record. * `delete`: The action is exposed as a `delete` mutation in the GraphQL API. `deletes`s require being passed an `id` parameter, and do not accept any parameters. Deletes do not return any data. * `custom` The action is exposed in the GraphQL API, with the rest of the params up to you. `custom`s require being passed an `id` parameter, and take no other params by default. `custom`s return the record they were invoked on. Globally-scoped actions do not have an `actionType` option because they do not need to specify whether an `id` is required (as is the case with model actions that typically deal with record mutations like `create`, `update`, or `delete`). This distinction is unnecessary for globally-scoped actions, which are more generalized and are not tied to a specific record context. However, both globally-scoped and model-scoped actions are still exposed in the GraphQL API. #### `timeoutMS`  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 for apps using earlier versions. Starting with the 1.4 release, the default timeout will change to 15 seconds. However, the timeout may vary depending on which version of the app is in use. ```typescript import { ActionOptions } from "gadget-server"; export const options: ActionOptions = { // allow this action to take 1s before throwing a GGT_TIMEOUT_ERROR timeoutMS: 1000, }; ``` #### `transactional`  The `transactional` option specifies whether the action's `run` function should execute within a database transaction. For model-scoped actions, transactionality is set to `true` by default, meaning all operations in the `run` function are wrapped in a transaction. If any operation fails, the entire transaction is rolled back to maintain consistency. You should keep the run function transactional when you want all operations to be atomic to ensure data integrity. However, you can use `transactional: false` when the operations are independent, and a failure in one should not affect the others (e.g., for non-database operations). ```typescript import { ActionOptions } from "gadget-server"; export const options: ActionOptions = { // Disable transaction for this action transactional: false, }; ``` #### `returnType`  Specifies whether the action should return a custom result from the `run` function. Defaults to `false` for model-scoped actions and `true` for globally-scoped actions. ```typescript import { ActionOptions } from "gadget-server"; export const options: ActionOptions = { // enable the ability to return a value from the `run` function of the action returnType: true, }; ``` When you set `returnType` to `true`, it allows the `run` function to return a value, which can be useful when you need to send feedback or results back to the caller immediately after the action has been executed. This is particularly helpful for scenarios where you want to confirm the success of an operation, return computed results, or provide data for further use, without relying on external systems or triggers. Let's take a look at a simple example of how to return a value from a `run` function when setting `returnType` to `true`. ```typescript import { applyParams, save, ActionOptions } from "gadget-server"; export const run: ActionRun = async ({ params, record, logger, api }) => { applyParams(params, record); await save(record); return { // returning an object result: "successfully returned", }; }; export const onSuccess: ActionOnSuccess = async ({ params, record, logger, api, }) => { // cannot return from onSuccess! }; export const options: ActionOptions = { actionType: "custom", // Setting returnType to `true` as it is false by default on a model-scoped action returnType: true, }; ``` #### `triggers`  Specifies which [triggers](https://docs.gadget.dev/guides/actions/triggers) cause the action to execute. Triggers can be added via code or via [Gadget UI](https://docs.gadget.dev/guides/actions/triggers/#adding-triggers-to-actions-via-the-gadget-ui) ##### API endpoint trigger  The API endpoint trigger is not shown within options but by default is `true` in every model-scoped action. This applies to all model-scoped actions, except for the default Shopify ones. However, if you create a custom model-scoped action on a Shopify-related model, it will include an API endpoint trigger. ```typescript import { ActionOptions } from "gadget-server"; export const options: ActionOptions = { actionType: "update", // API trigger will not be shown by default on newly created actions triggers: {}, }; ``` If you choose to remove the trigger you'll need to explicitly set the API endpoint trigger to `false` ```typescript import { ActionOptions } from "gadget-server"; export const options: ActionOptions = { actionType: "update", triggers: { // Pass in `api` and set it to false api: false, }, }; ``` ## Writing action code  In Gadget, actions are used to modify data, perform business logic, and handle API requests. Actions can be defined on models and executed using transactions to ensure data consistency. ### Default actions  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](https://docs.gadget.dev/reference/gadget-server#applyparams) * Saves the `record` instance to the database with the [`save` function](https://docs.gadget.dev/reference/gadget-server#save). 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: ```typescript export const run: ActionRun = async ({ 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. #### `upsert` meta Action  Models that have both a `create` and `update` action will also have a meta `upsert` action. The create and update actions must exist for the upsert action to be available on the model. The upsert action doesn't need to be created by you, nor will it be found in the actions folder. When run, `upsert` will do one of the following: * Call the create action if the given `record` does not exist in the database. * Call the update action if the `record` does exist in the database. ```typescript await api.gizmo.upsert({ id: "123", name: "XZ-77", uniqueCode: "112233", }); ``` The `upsert` action uses a record's `id` field to determine if the record exists in the database. A custom upsert field can be provided with the `on: ["customField"]` option. ```typescript // This will upsert the record based on the 'uniqueCode' field await api.gizmo.upsert({ name: "XZ-77", uniqueCode: "112233", on: ["uniqueCode"], }); ``` If a combination of two fields on a model is used to determine a record's uniqueness, you can provide an array of fields to the `on` option. ```typescript // This will upsert the record based on the 'uniqueCode' field and the 'user' relationship await api.gizmo.upsert({ name: "XZ-77", code: "112233", user: { _link: "1", }, on: ["code", "user"], }); ``` ### 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: ```typescript import { ActionOptions } from "gadget-server"; export const run: ActionRun = async ({ record, params, logger }) => { // ... }; export const options: ActionOptions = { // disable transactions for this action transactional: false, }; ``` Transactionality is on by default for model action `run` functions, and off by default for global action `run` functions. Transactions currently only group API calls made with the Internal API, so invoking other actions within a transactional action `run` function will start a new, different transaction. If you need nested action transactions, please contact us on Discord. #### Using `api.transaction()`  Sometimes, you may want to add a transaction to a function that doesn't already have it, such as in `onSuccess` or HTTP routes. You can do this by wrapping the body of these functions in the `api.transaction` function. Transactions are defined as such: `transaction: (callback: (transaction: GadgetTransaction) => Promise) => Promise` Breaking this down, we can see that transaction accepts an argument `callback`. This is used to call different, predefined, Gadget functions which may be useful during a transaction. For example, the following code makes use of a `rollback`, which can undo any changes made to a database in case of action failure. ```typescript export const onSuccess: ActionOnSuccess = async ({ record, params, logger, api, }) => { await api.transaction(async ({ rollback }) => { // Example of a rollback use case try { await api.internal.someModel.create({ name: "new record" }); // Error occurs in call to external service await fetch("https://example.com/api", { method: "POST", body: JSON.stringify({ name: "new record" }), }); } catch (error) { logger.error({ error }, "error encountered"); // Rollback the transaction await rollback(); } }); }; ``` For transactionality and rollbacks to take effect, api calls must be made using the [Internal API](https://docs.gadget.dev/guides/actions/code#using-the-public-api-vs-the-internal-api). ##### `GadgetTransaction` callback params  The following are parameters of the `callback` function: | Parameter | Type | Description | | --- | --- | --- | | `client` | Client | Information about the underlying urql client. This is not the same as the API parameter in action context. | | `start` | `Promise` | A function that starts the transaction and returns a void promise. | | `commit` | `Promise` | A function that commits the transaction. Returns a void promise. | | `rollback` | `Promise` | A function that explicitly rolls back the transaction. It prevents any changes from the transaction from being committed. Uncaught errors will automatically rollback the transaction. Returns a void promise. | | `close` | () => void | A function that shuts down the transaction, closing the connection to the backend. Void return. | | `open` | boolean | Returns a boolean. | | `subscriptionClient` | SubscriptionClient | The instance of the underlying transport object. This is the transaction websocket manager. | 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 by setting `transactional: false` in the action options * split up a single transactional action into several smaller actions, each of which is transactional' Example of splitting an action into multiple actions: ```typescript export const onSuccess: ActionOnSuccess = async ({ record, api }) => { // ... do some work await api.enqueue(api.someModel.someAction, { id: record.id }); // ... do some more work }; ``` ### Handling errors and Rollbacks  Errors thrown during the `run` or `onSuccess` functions will cause the action to stop processing and will be logged. These error descriptions will also be sent back from your application's auto-generated API. When using the `api` object, errors are thrown as JavaScript Error objects that you can catch. Throwing an error will: * Stop any sibling actions from continuing. * Prevent nested actions from starting. #### Handling Expected Errors  If you anticipate that a certain part of your action might fail, wrap it in a `try/catch` block to handle it gracefully. ```typescript import { save, applyParams } from "gadget-server"; export const run: ActionRun = async ({ record, params, logger }) => { try { await callUnreliableAPI(); } catch (error) {} // continue processing ... }; ``` #### onFailure  While Gadget does not have its own `onFailure` function, you can create one to handle any errors encountered by your actions. However, unlike `onSuccess`, you will need to call this function in the `run` function as needed. For example, this function could be called in the `catch` block of a `try/catch`. ```typescript import { save, applyParams } from "gadget-server"; export const run: ActionRun = async ({ record, params, logger }) => { try { await callUnreliableAPI(); } catch (error) { await onFailure({ error, logger }); } }; export const onFailure = async ({ error, logger }) => { logger.error({ error }, "Failed to update record"); }; ``` #### Transaction rollback from errors  If your action is transactional and an error occurs in the `run` function: * The database transaction will be rolled back automatically. * Any changes made to the database won't take effect. However, if your action is not transactional, or if errors occur in the `onSuccess` function: * Any changes made so far will be committed and won't be rolled back. For debugging, errors are also logged when they occur. These logs are visible in the Gadget [Log Viewer](https://docs.gadget.dev/guides/development-tools/logger). ## Advanced action code  ### Nested actions and execution order  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. ```typescript await api.post.create({ title: "My First Blog Post", author: { _link: "1", }, body: "some interesting content", comments: [ { create: { body: "first comment!", author: { _link: "2", }, }, }, { create: { body: "another comment", author: { _link: "3", }, }, }, ], }); ``` ```graphql mutation CreatePost($post: CreatePostInput) { createPost(post: $post) { success errors { message } post { id } } } ``` ```json { "post": { "title": "My First Blog Post", "author": { "_link": "1" }, "body": "some interesting content", "comments": [ { "create": { "body": "first comment!", "author": { "_link": "2" } } }, { "create": { "body": "another comment", "author": { "_link": "3" } } } ] } } ``` 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 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: ```json { "_converge": { "values": [ // will update record with id 1 { "id": "1", "name": "first" }, // will create a new record { "name": "second" } // other records not present in this list will be deleted ] } } ``` 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. ```typescript await api.post.update("123", { title: "Updated Blog Post", body: "some interesting content", images: [ { _converge: { values: [ { caption: "Mountains", url: "https://example.com/mountains.jpg", }, { id: "10", caption: "Oceans", url: "https://example.com/oceans.jpg", }, ], }, }, ], }); ``` ```graphql mutation UpdatePost($id: GadgetID!, $post: CreatePostInput) { updatePost(id: $id, post: $post) { success errors { message } post { id } } } ``` ```json { "id": "123", "post": { "title": "Updated Blog Post", "body": "some interesting content", "images": [ { "_converge": { "values": [ { "caption": "Mountains", "url": "https://example.com/mountains.jpg" }, { "id": "10", "caption": "Oceans", "url": "https://example.com/oceans.jpg" } ] } } ] } } ``` 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: ```json { "_converge": { "values": [ // ... ], "actions": { // will create any new records using the publicCreate action of the child model "create": "publicCreate", // will update any existing records using the specialUpdate action of the child model "update": "specialUpdate" } } } ``` 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. ### Action timeouts  Gadget actions have a default maximum execution duration of 3 minutes . Time spent executing the `run` and `onSuccess` functions must take less than 3 minutes by default, or Gadget will abort the action's execution. Actions that run for longer than the timeout will throw the `GGT_ACTION_TIMEOUT` error and return an error to the API caller. Note that a timeout error does not guarantee that your code will stop running. If you want to make sure that your code will terminate gracefully, take advantage of the [signal](https://docs.gadget.dev/guides/actions/code#signal) property from the action context. This timeout applies to nested actions as well, such that all nested actions executed simultaneously must complete within the 3 minutes timeout. Action timeouts can be changed using the `timeoutMS` action option: ```typescript import { save, applyParams } from "gadget-server"; export const run: ActionRun = async ({ record, params, logger }) => { // fast stuff within 5s transaction timeout }; export const onSuccess: ActionOnSuccess = async ({ record, params, logger }) => { // slow stuff }; export const options: ActionOptions = { // 10 minutes (600,000 milliseconds) for a really long action timeoutMS: 600000, }; ``` Action timeouts can be a maximum of 15 minutes (900,000 milliseconds). If your action is transactional the `run` function must finish within a 5-second transaction timeout. This limit can't be changed. Model actions are transactional by default. Global actions are non-transactional by default and don't have this limit. If you need longer-running logic, put it in `onSuccess`, or set `transactional: false` in the action options. ## 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. ```typescript export const run: ActionRun = async ({ api, params, logger, ...others }) => { // ... }; ``` 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](https://docs.gadget.dev/api/example-app) 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](https://docs.gadget.dev/guides/plugins) to see what each connection provides. * `logger`: A [logger](https://docs.gadget.dev/guides/development-tools/logger#logger-api) 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. `https://my-app.gadget.app` * `trigger`: An object containing what event caused this action to run. Globally-scoped 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](https://docs.gadget.dev/guides/data-access/computed-fields) guide. ### `record`  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. ```typescript export const run: ActionRun = async ({ record, logger }) => { // will log the record's id logger.info(record.id); // will log the value of the firstName field loaded from the database logger.info(record.firstName); }; ``` You can change properties of the record like normal JavaScript objects: ```typescript export const run: ActionRun = async ({ record }) => { record.firstName = "New name"; }; ``` Note that the record is **not** automatically saved. To save the record, use the `save` function from `gadget-server`: ```typescript import { save } from "gadget-server"; export const run: ActionRun = async ({ record }) => { await save(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)`. ### `api`  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](https://docs.gadget.dev/api/example-app/development). You can use the `api` object to fetch other data in an action: ```typescript // send a notification to the user who owns the record if they've opted in import twilio from "../../twilio-client"; export const run: ActionRun = async ({ api, record }) => { const creator = await api.user.findOne(record.creatorId); if (creator.wantsNotifications) { await twilio.sendSMS({ to: creator.phoneNumber, from: "+11112223333", body: "Notification: " + record.title + "was updated", }); } }; ``` Or, you can use the `api` object to change other data in your application as a result of the current action: ```typescript // example: save an audit log record when this record changes export const run: ActionRun = async ({ api, record, model }) => { const changes = record.changes(); // run a nested `create` action on the `auditLog` model await api.auditLogs.create({ action: "Update", model: model.apiIdentifier, record: { _link: record.id }, changes: changes.toJSON(), }); }; ``` ### Public API vs internal API  Your actions define your app's **public API**. As you make changes to your data models, Gadget also generates and updates an **internal CRUD (create, read, update, delete) API**. The internal API can be called using `api.internal..`: ```typescript await api.internal.student.create({ firstName: "Carl", lastName: "Weathers", }); ``` The internal API does not run any action code or custom business logic, and also bypasses validation and data consistency promises that are enforced when using the public API. It is typically used for low-level data manipulation and performance optimization. It is powerful, but should be used with caution. [Read more about when to use the internal API](https://docs.gadget.dev/guides/data-access/api#public-api-vs-internal-api). ### `api.actAsSession`  By default, inside action code, the `api` object will have system admin privileges. This means inside your actions, the `api` object can use any api endpoints without any restrictions. This is very helpful for implementing the internal logic of your app. However, there are times where you may want the `api` object to act with the same permissions as the session making the request. In this case you can use `api.actAsSession`, here is an example: ```typescript export const run: ActionRun = async ({ api }) => { // assume that this action is called from an unauthenticated session // by default api has admin privileges, so this will be all posts const allPosts = await api.post.findMany(); // assume that there is a filter on the unauthenticated role's read access to post const unauthenticatedAccessiblePosts = await api.actAsSession.post.findMany(); }; ``` ### `api.actAsAdmin`  The `api` object also supports reversing any current session permissions by calling `api.actAsAdmin`. This is useful if you need to escape session permissions in a context where the `api` client may have been created with session permissions, like if the client has had `.actAsSession` called on it. ```typescript export const run: ActionRun = async ({ api }) => { // get a session api client const sessionApi = await api.actAsSession; // do some stuff with the session's privileges // ... // later, go back to the admin api const backToAdminApi = await sessionApi.actAsAdmin; // do some stuff with sysadmin privileges // ... }; ``` This ensures the update runs only if the user's status is not already "active," preventing an infinite loop. **Using Flags or Metadata to Prevent Recursion** Tracking whether an action has already been processed using a flag prevents duplicate execution: ```typescript await api.transaction(async (transaction) => { const order = await api.order.findById(456); if (!order.processed) { await api.internal.order.update(order.id, { processed: true, // Flag to prevent re-execution }); // Perform action only once await triggerAnotherAction(order); } }); ``` Here, the `processed` flag ensures that the logic runs only once. **Using Rate Limits and Timeouts** Slowing down execution with a timeout prevents actions from running too frequently: ```typescript setTimeout(async () => { await api.internal.order.update("789", { status: "pending", }); }, 1000); // Wait 1 second before retrying ``` **Debugging with Logging** Logging the execution times of an action helps detect infinite loops: ```typescript logger.info({ date: new Date() }, "Order processing started"); await api.internal.order.update("789", { status: "completed", }); logger.info({ date: new Date() }, "Order processing finished"); ``` If excessive logs appear for the same action, it indicates a loop. ## Model-scoped vs globally-scoped actions  In Gadget, there are 2 kinds of actions: model-scoped and globally-scoped. Model-scoped actions are defined on a specific model, while globally-scoped actions are not tied to any model. Model-scoped actions are typically used to perform operations tied to a specific record, while globally-scoped actions are more generalized application logic.