# Writing actions  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. ## Action basics  Through [What's in an action](https://docs.gadget.dev/guides/actions/code), you have learned the types of exports produced by an action. This section will provide more details on the basics of writing an action, as well as the actions your gadget models come pre-equipped with. ### 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. #### `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 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 }) => { applyParams(params, record); record.startDate ??= new Date(); await save(record); }; ``` #### `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("123", { 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 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 await api.gizmo.upsert({ name: "XZ-77", code: "112233", user: { _link: "1", }, on: ["code", "user"], }); ``` ### 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-scoped 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-scoped 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 // example: 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(), }); }; ``` For more information on the api's use in actions, see [actions and api](https://docs.gadget.dev/guides/actions/actions-and-api). #### `trigger`  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. ```typescript export const run: ActionRun = async ({ api, record, trigger, logger }) => { // will log an object like { "type": "api", "rootModel": "someModel", "rootAction": "someAction", etc ... } logger.info(trigger); }; ``` See the [Triggers](https://docs.gadget.dev/guides/actions/triggers) guide for more information on the different types of triggers. #### `connections`  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 `best-sneakers.myshopify.com` 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: ```typescript export const run: ActionRun = async ({ api, record, connections }) => { const shopifyClient = await connections.shopify.forShopDomain( "best-sneakers.myshopify.com" ); await shopifyClient.graphql( `mutation ($input: ProductInput!) { productUpdate(input: $input) { product { id } userErrors { message } } }`, { input: { id: `gid://shopify/Product/${productId}`, ...productParams, }, } ); }; ``` Visit the [Connections](https://docs.gadget.dev/guides/plugins) 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. ```typescript export const onSuccess: ActionOnSuccess = async ({ api, record, connections }) => { // only do the remote update if the body field has changed if (record.changed("body")) { const shopifyClient = await connections.shopify.forShopDomain( "best-sneakers.myshopify.com" ); await shopifyClient.graphql( `mutation ($input: ProductInput!) { productUpdate(input: $input) { product { id } userErrors { message } } }`, { input: { id: `gid://shopify/Product/${productId}`, ...productParams, }, } ); } }; ``` #### `request`  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. ```typescript export const run: ActionRun = async ({ api, record, trigger, request, logger }) => { // will log the client making this API requests's IP logger.info(request.id); // will log the incoming Authorization request header logger.info(request.headers["authorization"]); }; ``` See all the available properties on the `RequestData` type in the [Reference](https://docs.gadget.dev/reference/gadget-server#requestdata). #### `emails`  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. ```typescript import { ActionOptions } from "gadget-server"; export const run: ActionRun = async ({ logger, emails }) => { await emails.sendMail({ to: "user@gmail.com", subject: "Welcome", text: "Welcome to our application!", }); }; export const options: ActionOptions = { actionType: "custom", }; ``` For more examples of using the `emails` object, see [building with email/password authentication](https://docs.gadget.dev/guides/plugins/authentication/email-pass). #### `signal`  Each action is passed a `signal` object, [which is an instance of `AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). `signal.aborted` will become true when underlying request becomes closed or aborted. **Why/How?** Async requests made to your app's API or an external API can be aborted. Common reasons for a request to be aborted include a timeout or an expected error on the request. An AbortSignal is a read-only representation of a request's status and allows you to handle these aborted requests gracefully. ```typescript export const run: ActionRun = async ({ api, record, request, signal }) => { for (const dog of await api.internal.dog.findMany()) { if (dog.age < 2) { await api.internal.dog.update({ id: dog.id, description: `${dog.name} is a young dog`, }); } // Use throwIfAborted() to rollback transactions that have not yet been committed signal.throwIfAborted(); } // 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), fetch will immediately cancel const cats = fetch("someAPI/cats", { signal }); // it is okay if we don't create cats, so we return and have the action succeed if (signal.aborted) return; await api.internal.cats.bulkCreate(cats); }; ``` ### 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) { logger.error({ error }, " there was a problem"); } // 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"); }; ``` #### 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-scoped action `run` functions, and off by default for globally-scoped 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. ##### 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  You can further customize your actions with some advanced features, such as transactionality, nestled actions, and converge actions. ### 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 accept 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(); } }); }; ``` #### `GadgetTransaction` callback params  The following params are available in 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 }; ``` ### Nested actions and execution order  Every action supports calling other 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. If all the `run` functions are successful, then all the actions' `onSuccess` will be executed. 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. As a rule of thumb, 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 simplifies the process of creating, updating, and deleting records in a database to match an incoming specified list. Instead of manually tracking changes, `_converge` intelligently determines the required operations to transform the current database state into the desired new list. This is particularly useful when updating a parent object alongside a list of child objects. For example, in a form where users edit a blog post and its associated images, `_converge` allows developers to pass the updated list of images directly without worrying about which images were added, removed, or modified. #### How `_converge` Works  When executed, `_converge` processes records using the following default API actions: * **Create new records** using `create`. * **Update existing records** using `update`. * **Delete records not present in the new list** using `delete`. These operations ensure that all modifications happen within a single transaction, maintaining data integrity. **Example Usage** To run a `_converge` call, specify `_converge` as the action name in a nested action input and pass the new list of records in the `values` property: ```json { "_converge": { "values": [ { "id": "1", "name": "first" }, // Updates existing record with ID 1 { "name": "second" } // Creates a new record // Any records missing from this list will be deleted ] } } ``` **Use Case: Editing a Blog Post with Images** If a user is editing a blog post that includes multiple images, `_converge` can automatically handle additions, updates, and deletions of images based on the new list provided. Without `_converge`, developers would need to manually track changes and construct the corresponding API calls. Instead, `_converge` allows us to synchronize the list of images seamlessly: ```typescript await api.post.update("123", { _converge: { values: [ { id: "10", caption: "Oceans" }, // Updates an existing image { caption: "Mountains" }, // Creates a new image ], }, }); ``` ```graphql mutation UpdatePost($id: GadgetID!, $post: CreatePostInput) { updatePost(id: $id, post: $post) { success errors { message } post { id } } } ``` ```json { "post": { "_converge": { "values": [{ "id": "10", "caption": "Oceans" }, { "caption": "Mountains" }] } } } ``` In this case: * A new image with the caption **"Mountains"** is created using `Image.create`. * The existing image with ID **"10"** is updated to **"Oceans"** using `Image.update`. * Any images **not present** in the new list are deleted using `Image.delete`. **Invoking Custom Actions in `_converge`** By default, `_converge` uses `create`, `update`, and `delete` actions, but developers can override these with custom actions using the `actions` property: ```json { "_converge": { "values": [ // List of records to create, update, or delete ], "actions": { "create": "publicCreate", // Uses a custom 'publicCreate' action for new records "update": "specialUpdate" // Uses 'specialUpdate' instead of 'update' } } } ``` This override mechanism allows flexibility in defining how records are processed, making `_converge` adaptable to different business logic requirements. ## Optimizing actions  So far you've learned how actions work and how to write them. However, there are some tips you should keep in mind to optimize action performance. ### Handling infinite loops in actions  Infinite loops in actions can cause performance issues and excessive API calls, making it crucial to prevent them. These loops often occur when an action unintentionally triggers itself or another action recursively. The most common causes include triggering an action inside itself, circular dependencies between actions, and event-based triggers that fire continuously. **Preventing infinite loops with conditional checks** Use conditional logic to ensure you're not updating a record unnecessarily. This is especially important in actions that are generated on its own (`update`, `create`) or within a custom action's run or `onSuccess` hook. ```typescript const user = await api.user.findById("1"); // Prevent unnecessary updates if (user.status !== "active") { await api.user.update(user.id, { status: "active", }); } ``` 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** Add a custom field or metadata (like processed) to track whether logic has already been executed. This can be used in `onSuccess`, or custom actions to prevent duplicate behavior. ```typescript const orderRecord = await api.order.findById("1"); if (!orderRecord.processed) { await api.order.update(orderRecord.id, { processed: true, }); await api.order.archiveOrder(orderRecord.id); } ``` Here, the processed flag ensures that this block runs only once per record. **Using time-based conditions** In serverless environment, delaying execution using setTimeout() is not possible. Instead, you can use timestamps to control the timing of when actions should be triggered. This is useful for rate-limiting or ensuring that updates happen only after a certain amount of time. ```typescript const now = new Date(); if ( !order.lastProcessedAt || Date.now() - new Date(order.lastProcessedAt).getTime() > 1000 ) { await api.order.update(order.id, { lastProcessedAt: now.toISOString(), status: "pending", }); } ``` In this example, the order is only updated if it hasn’t been processed within the last second. This avoids repeated triggers within a short time frame. **Debugging with logging** Logging inside your `run` or `onSuccess` function helps identify if an action is being repeatedly triggered. ```typescript logger.info("Order processing started:", new Date()); await api.order.update("789", { status: "completed", }); logger.info("Order processing finished:", new Date()); ``` If you see logs appearing too frequently for the same action, that indicates an unintended loop or excess triggers. **Limiting event-based triggers** For event-based triggers like `update`, always add checks to ensure an update won’t recursively trigger another update. These checks ensure that actions won’t cause infinite loops due to their event-handling mechanism. ```typescript api.order.update(async ({ record }) => { if (record.status !== "confirmed") { await api.order.update(record.id, { status: "confirmed", }); } }); ``` This ensures that an update doesn’t occur recursively if the status is already set to the desired value. By combining these strategies—**conditional checks, flags, rate limits, logging, and event handling**—you can effectively prevent infinite loops in Gadget actions, ensuring smooth performance and reliable data handling.