# Actions

Source: https://developers.swell.is/apps/actions

Actions are buttons and menu items that an app adds to the Swell dashboard. An action runs an app function or workflow when an admin user clicks it, or opens a link. A function action can open a dialog first, to confirm the action or collect input, and the dashboard shows the result when the function finishes.

### Where actions appear

Actions are declared in the app's [settings](https://developers.swell.is/apps/settings) and [content](https://developers.swell.is/apps/content) files. Where an action is declared determines where it appears in the dashboard:

| Declared in | Appears as | $action.source |
| --- | --- | --- |
| actions in a settings file | An item at the top of the Actions menu on the app's page | settings |
| actions in a content view | A button in the view's header | list or record |
| extra_actions in a content view | An item in the view's Actions menu | list or record |
| bulk_actions in a list view | A button in the bulk bar, shown when records are selected | bulk |
| A field with type action | A button among a record's fields or the app's settings | field |

On the record pages of standard collections such as products, orders, and customers, the function actions in an app's record views are added to the page's Actions menu, next to its own actions. On standard lists, the function actions in `actions` and `extra_actions` of the app's `list` view are added to the list's Actions menu, and its `bulk_actions` to the bulk bar.

### Running a function

A function runs as an action when its config sets `action: true`. Like `route`, `model`, and `cron`, `action` is a trigger, and a function can only have one trigger.

**functions/send-product.ts**

```typescript
export const config: SwellConfig = {
  description: 'Send a product to the warehouse',
  action: true,
};

export default async function (req: SwellRequest) {
  const { record_id } = req.data.$action;

  if (!/^[0-9a-f]{24}$/i.test(record_id)) {
    throw new Error('Invalid product id');
  }

  const product = await req.swell.get(`/products/${record_id}`);
  if (!product) {
    throw new Error('Product not found');
  }

  // Send the product to the warehouse

  return { message: `Sent ${product.name} to the warehouse` };
}
```

To add the action to product pages, name the function in an action of the app's `products` content file. The value of `function` is the function's file name without its extension:

**content/products.json**

```json
{
  "collection": "products",
  "views": [
    {
      "id": "edit",
      "type": "record",
      "actions": [
        {
          "id": "send_to_warehouse",
          "label": "Send to warehouse",
          "function": "send-product",
          "hint": "Sends this product's details to the warehouse"
        }
      ]
    }
  ]
}
```

The function receives the action context in `req.data.$action`, and the values of any dialog fields at the top level of `req.data`. Its response decides what the admin user sees:

- Return an object with a `message` to show that message. Without one, the dashboard shows that the action finished. The page then reloads the record or list, so changes the function made appear right away.
- Throw an error to report a failure. The dashboard shows the error's message.
- If the function doesn't respond within its timeout, the dashboard reports that the action may still be running.

The admin user waits while the function runs, so it must respond within the [function timeout](https://developers.swell.is/apps/functions), 10 seconds by default. For longer work, have the action start a workflow instead.

### Action properties

Actions in settings files and content views support these properties:

| Property | Description |
| --- | --- |
| id | Identifier of the action, sent to the function as $action.id. Required for function actions. Must be unique among a settings file's actions, among a view's actions and extra_actions combined, and among its bulk_actions. |
| label | Text on the button or menu item. Defaults to the id, formatted as words. |
| function | Name of the app function or workflow to run, such as send-product for functions/send-product.ts. Cannot be combined with link. |
| link | URL to open instead of running a function. Use frontend:// to open a page of the app's frontend. In content views, {field} placeholders are replaced with values from the current record. |
| target | Where a link opens, usually blank or self. |
| hint | Tooltip shown after hovering over the action for 2 seconds. |
| loading_label | Text shown while the function runs, or while a workflow run starts. Defaults to the label followed by an ellipsis. |
| modal | Dialog shown before the function runs. See Dialogs. |
| conditions | Shows the action only while the expression matches the current record, or the app's saved settings for settings actions. Ignored for bulk actions and for function actions in list views. |
| hidden | Hides the action. A hidden function action cannot be run. |
| type | Button style of a header or bulk button: default, primary, secondary, or danger. Bulk buttons default to secondary. |

Settings actions must define `function` or `link`, and bulk actions must define `function`.

When every item in a view's `actions` or `extra_actions` runs a function, the items are added after the view's default actions. Otherwise the array replaces the defaults: New in a list view's header, Save in a record view's header, and Delete in a record view's Actions menu.

In record views, a function action runs only on a saved record, and is disabled while the record has unsaved changes. Settings actions are disabled while the settings have unsaved changes.

### Dialogs

Without `modal`, a function action runs as soon as it's clicked. With `modal`, the dashboard opens a dialog, and the action runs when the admin user submits it. Use `"modal": {}` for a plain confirmation titled with the action's label.

| Property | Description |
| --- | --- |
| title | Dialog title. Defaults to the action's label. |
| description | Plain text shown above the fields. |
| submit_label | Text on the submit button. Defaults to the action's label. |
| fields | Inputs shown in the dialog, defined as content fields other than action fields. Their values are sent to the function at the top level of req.data, and are not saved on the record or in the settings. |

If the function fails, the dialog stays open with the values the admin user entered, so they can correct them and run the action again.

**An action with a dialog**

```json
{
  "id": "adjust_price",
  "label": "Adjust price",
  "function": "adjust-price",
  "modal": {
    "description": "Changes the price of this product.",
    "submit_label": "Apply",
    "fields": [
      {
        "id": "percent",
        "type": "percent",
        "label": "Change",
        "required": true
      }
    ]
  }
}
```

The `adjust-price` function reads the value as `req.data.percent`.

### Bulk actions

A list view's `bulk_actions` show as buttons in the bulk bar when records are selected, and declaring any makes the list selectable. A bulk action's dialog shows how many records are selected. The function receives the selection in `$action.selection`:

| Property | Description |
| --- | --- |
| all | true when the admin user selected every record matching the list's search and filters. |
| ids | Selected record ids, when all is false. |
| except_ids | Record ids unchecked after selecting all, when all is true. |
| count | Number of selected records shown in the dashboard, or null when unknown. |
| query | Filter for exactly the selected records: the list's search and filters, with the selected or unchecked ids in a top-level $and. Does not include paging or sorting. |

Pass `query` to `req.swell.get` with your own paging to load the selected records. To add conditions, append them to `query.$and`. Replacing `query.$and` or `query.where` drops the selection or the list's filters.

**content/products.json**

```json
{
  "collection": "products",
  "views": [
    {
      "id": "list",
      "type": "list",
      "bulk_actions": [
        {
          "id": "send_to_warehouse",
          "label": "Send to warehouse",
          "function": "send-products",
          "modal": {
            "description": "Sends the selected products to the warehouse."
          }
        }
      ]
    }
  ]
}
```

**functions/send-products.ts**

```typescript
export const config: SwellConfig = {
  description: 'Send the selected products to the warehouse',
  action: true,
};

export default async function (req: SwellRequest) {
  const { query } = req.data.$action.selection;
  let sent = 0;

  for (let page = 1; ; page++) {
    const { results } = await req.swell.get('/products', {
      ...query,
      limit: 100,
      page,
    });

    // Send this page of products to the warehouse
    sent += results.length;

    if (results.length < 100) break;
  }

  return { message: `Sent ${sent} products to the warehouse` };
}
```

A bulk action on a large selection can take longer than a function's timeout. Have it start a workflow instead.

### Action fields

A field with `type: "action"` is a button among a record's fields or the app's settings. It runs a function like other actions, and stores no value. The field's `id` is the action id, `label` is the button text, `description` shows below the button, and `hint` shows as a tooltip. Action fields also support `function`, `loading_label`, `modal`, and `hidden`.

Action fields can go in content files, view fields, and settings files, including inside `field_row` and `field_group`, but not inside a collection field or a dialog. They cannot define `default` or nested `fields`.

In a content file, an action field runs on the saved record, and the function receives its id as `$action.record_id`. In a settings file, it runs without a record, and `$action.settings` names the settings file.

**settings/warehouse.json**

```json
{
  "label": "Warehouse",
  "description": "Connect the app to your warehouse",
  "fields": [
    {
      "id": "api_key",
      "type": "short_text",
      "label": "API key"
    },
    {
      "id": "test_connection",
      "type": "action",
      "label": "Test connection",
      "function": "test-connection",
      "description": "Checks that the API key works"
    }
  ],
  "actions": [
    {
      "id": "sync_all",
      "label": "Sync all products",
      "function": "sync-all",
      "modal": {
        "description": "Sends every product to the warehouse."
      }
    }
  ]
}
```

Here `test_connection` is a button below the API key field, and `sync_all` is an item in the Actions menu of the app's page. Both are disabled while the settings have unsaved changes, so the functions can read the saved values with `req.swell.settings()`.

### Running a workflow

An action can start a [workflow](https://developers.swell.is/apps/functions) instead of running a function, for work that takes longer than a function's timeout. Name the workflow in `function`, and set `action: true` in the workflow's config:

**functions/sync-all.ts**

```typescript
export const config = {
  kind: 'workflow',
  description: 'Send every product to the warehouse',
  action: true,
};

export default class SyncAll {
  async run(req: SwellWorkflowRequest, step: SwellWorkflowStep) {
    let sent = 0;

    for (let page = 1; ; page++) {
      const count = await step.do(`send page ${page}`, async () => {
        const { results } = await req.swell.get('/products', {
          limit: 100,
          page,
        });

        // Send this page of products to the warehouse

        return results.length;
      });

      sent += count;
      if (count < 100) break;
    }

    return { sent };
  }
}
```

The action starts a run and returns right away. The run receives the same `req.data` as an action function, with the dialog values and `$action`, and its `req.workflow.trigger` is `action`.

While the run is active, the dashboard shows its status next to the action, with the step it's on, and the action cannot be run again. When the run ends, the dashboard reports whether it completed, failed, or was canceled, and reloads the page. The workflow's return value is not shown.

A run's `req.data` must be 128 KB or smaller once serialized, the same limit as `workflows.create()`. A bulk action on a few thousand selected records can exceed it, and fails with `workflow_params_too_large`.

Under `swell app dev`, actions start the pushed version of a workflow, since workflows only run after `swell app push`.

### Workflow runs

When an app has workflows, its page in the dashboard has a Workflows tab. It lists the app's runs, started by actions or by functions, with their status, who started them, and their duration. Opening a run shows its steps, its error if it failed, and a link to its logs in the console. Users with permission can cancel an active run there. Canceling a run does not undo steps that already finished.

Access to runs follows the user's Apps permission: view access shows every run, and manage or full access can also cancel runs. Users without access to Apps don't see the tab, and only see the status of the runs they start.

### Permissions

Only admin users can run actions, from the dashboard. Action functions cannot be called through the API or the [function gateway](https://developers.swell.is/apps/functions). When an action runs, the platform checks that the app declares it, that it isn't hidden, and that it names the function being run. Only the values of the dialog fields the action declares are passed to the function.

On plans with user roles, the admin user also needs a role that can manage what the action acts on:

- Settings actions and action fields in settings need manage access to Integrations.
- Actions on records of a standard collection need manage access to that section, such as Products for actions on products.

`conditions` only decide when an action appears, not who can run it. The function receives the admin user's id in `$action.user_id`.

`record_id` and `selection` come from the admin user's request, so treat them as input. Check that a record id is a 24-character hexadecimal id before using it in a URL path, and load the record before acting on it.

### Action context

Action functions, and the workflow runs that actions start, receive these fields in `req.data.$action`:

| Field | Description |
| --- | --- |
| id | The action's id. |
| source | Where the action is declared: settings, field, list, record, or bulk. |
| collection | Collection of the content file that declares the action, such as products, or apps/<app_id>/<name> for app collections. Not set for actions in settings files. |
| settings | Name of the settings file that declares the action. Set for settings actions and for action fields in settings. |
| record_id | Id of the record the action ran on. Set for record actions and for action fields in content files. |
| selection | The selected records. Set for bulk actions. |
| user_id | Id of the admin user who ran the action. |

Functions can pass any parameters to `workflows.create()`, including a `$action` object. In a workflow, only trust `$action` when `req.workflow.trigger` is `action`.

### Local development

Under `swell app dev`, action functions run from your machine, like other functions. `swell inspect functions` shows the trigger of action functions as `action`, and `swell inspect workflow-runs` shows which action started a run.

### Errors

`swell app push` rejects action declarations that don't follow these rules:

- A function action has an `id` that is unique in its group, and doesn't also define `link`.
- Settings actions define `function` or `link`, and bulk actions define `function`.
- An action field defines `function`, doesn't define `default` or `fields`, and isn't inside a collection field or a dialog.
- A function has only one trigger: `route`, `model`, `cron`, or `action`.

When an action cannot run, the dashboard shows the error's message. These codes identify the cause:

| Code | Cause |
| --- | --- |
| action_function_not_found | The function doesn't exist, isn't an action function, or is disabled. |
| action_not_found | The installed app doesn't declare the action, or the action is hidden. |
| action_function_mismatch | The action names a different function. |
| action_forbidden | The user's role doesn't allow the action. |
| action_user_required | The request didn't come from an admin user. |
| action_not_callable | An action function was called through the API instead of the dashboard. |
| workflow_params_too_large | The data for the workflow run is larger than 128 KB. |
