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.

Actions are declared in the app's settings and content files. Where an action is declared determines where it appears in the dashboard:

Declared inAppears as$action.source
actions in a settings fileAn item at the top of the Actions menu on the app's pagesettings
actions in a content viewA button in the view's headerlist or record
extra_actions in a content viewAn item in the view's Actions menulist or record
bulk_actions in a list viewA button in the bulk bar, shown when records are selectedbulk
A field with type actionA button among a record's fields or the app's settingsfield

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.

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
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
{
  "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, 10 seconds by default. For longer work, have the action start a workflow instead.

Actions in settings files and content views support these properties:

PropertyDescription
idIdentifier 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.
labelText on the button or menu item. Defaults to the id, formatted as words.
functionName of the app function or workflow to run, such as send-product for functions/send-product.ts. Cannot be combined with link.
linkURL 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.
targetWhere a link opens, usually blank or self.
hintTooltip shown after hovering over the action for 2 seconds.
loading_labelText shown while the function runs, or while a workflow run starts. Defaults to the label followed by an ellipsis.
modalDialog shown before the function runs. See Dialogs.
conditionsShows 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.
hiddenHides the action. A hidden function action cannot be run.
typeButton 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.

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.

PropertyDescription
titleDialog title. Defaults to the action's label.
descriptionPlain text shown above the fields.
submit_labelText on the submit button. Defaults to the action's label.
fieldsInputs 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
{
  "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.

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:

PropertyDescription
alltrue when the admin user selected every record matching the list's search and filters.
idsSelected record ids, when all is false.
except_idsRecord ids unchecked after selecting all, when all is true.
countNumber of selected records shown in the dashboard, or null when unknown.
queryFilter 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
{
  "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
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.

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
{
  "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().

An action can start a workflow 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
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.

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.

Only admin users can run actions, from the dashboard. Action functions cannot be called through the API or the function gateway. 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 functions, and the workflow runs that actions start, receive these fields in req.data.$action:

FieldDescription
idThe action's id.
sourceWhere the action is declared: settings, field, list, record, or bulk.
collectionCollection of the content file that declares the action, such as products, or apps// for app collections. Not set for actions in settings files.
settingsName of the settings file that declares the action. Set for settings actions and for action fields in settings.
record_idId of the record the action ran on. Set for record actions and for action fields in content files.
selectionThe selected records. Set for bulk actions.
user_idId 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.

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.

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:

CodeCause
action_function_not_foundThe function doesn't exist, isn't an action function, or is disabled.
action_not_foundThe installed app doesn't declare the action, or the action is hidden.
action_function_mismatchThe action names a different function.
action_forbiddenThe user's role doesn't allow the action.
action_user_requiredThe request didn't come from an admin user.
action_not_callableAn action function was called through the API instead of the dashboard.
workflow_params_too_largeThe data for the workflow run is larger than 128 KB.