Visual editing /Visual Editor /

Custom actions

Integrate workflow, automation and other custom tasks into Visual Editor. Custom actions create the ability to perform site and content tasks in a single location.

Visual Editor already handles normalizing and syncing content among any number of content sources. This makes the visual editing environment the perfect candidate for triggering content, workflow, automation, and other tasks for a site.

There are multiple points at which actions can hook into Visual Editor and content flow. See below for explanations, use cases, and examples.

# Types of actions

There are four types of actions:

Each action differs in the following ways:

  • Trigger location (in the UI)
  • Configuration options
  • Callback parameters

See below for use cases and further instruction on working with each of these types.

# Global actions

Global actions are performed on the site as a whole. For example:

  • Trigger a deploy preview for the current version of a site.
  • Send a custom workflow event to reviewers.
  • Run a performance test on the entire site.
  • Check for broken links throughout the site.

These actions are triggered from the top bar, next to the site name.

Global action trigger.

Global actions are configured as a property in the main configuration object.

//stackbit.config.ts
export default defineStackbitConfig({
  stackbitVersion: "0.6.0",
  contentSources: [
    /* ... */
  ],
  actions: [
    {
      type: "global",
      name: "name_of_action",
      run: async options => {
        // Perform the action ...
      }
      // Other options ...
    }
  ]
});

See the configuration reference for more information.

# Bulk document actions

Bulk document actions are performed on a selected set of documents. For example:

  • Send a set of pages to a translation service.

Editors can choose the set of documents on which to trigger the action.

Bulk action modal.

Like global actions, bulk actions are configured as a property in the main configuration object, specified by the type property.

// stackbit.config.ts
export default defineStackbitConfig({
  stackbitVersion: "0.6.0",
  contentSources: [
    /* ... */
  ],
  actions: [
    {
      type: "bulk",
      name: "name_of_action",
      run: async options => {
        // Perform the action ...
      }
      // Other options ...
    }
  ]
});

See the configuration reference for more information.

# Model actions

Model actions are performed on an individual document. For example:

  • Cloning an object based on input values
  • Sending a document to a translation service
  • Taking a snapshot of a document in its current state

These actions can be triggered where the document context is presented. When defined, it will always appear near the title in page and content editing modes.

Model action sidebar.

If using inline editing and if a proper data-sb-object-id annotation has been provided, the triggers will also be available in the toolbar when highlighting the document.

Model action toolbar.

Model actions are configured directly on the model definition. (When using a headless CMS, the model definition is an extension of the schema defined in the source.)

import { PageModel } from "@stackbit/types";

export const Post: PageModel = {
  name: "Post",
  type: "page",
  fields: [],
  // Other properties ...
  actions: [
    {
      name: "generate-title",
      label: "Generate Title",
      run: async options => {
        // Perform the action ...
      }
    }
  ]
};

See the reference for more information.

# Field actions

Field actions are performed against a field on a document. For example:

  • Generate AI content for a specific field (optionally based on some input).
  • Suggest fixing spelling and grammar.
  • Fill certain fields with custom data from an external API.
  • Translate a field using an external API.

These actions can be triggered wherever the field input is displayed.

Field action sidebar.

If using inline editing and proper data-sb-object-id and data-sb-field-path annotations have been provided, the triggers will also be available in the toolbar when highlighting the field.

Field action toolbar.

Field actions are configured as a property on a field within a model definition.

When using a headless CMS, the model definition is an extension of the schema defined in the source. Adding an action on a field that has been defined in an external schema only requires adding the name to identify the field.

import { PageModel } from "@stackbit/types";

export const Post: PageModel = {
  name: "Post",
  type: "page",
  fields: [
    {
      name: "title",
      actions: [
        {
          name: "sanitize-title",
          label: "Sanitize Title",
          run: async options => {
            // Perform the action ...
          }
        }
      ]
    }
  ]
  // Other properties ...
};

See the reference for more information.

# Accept input

Actions can accept input from editors by supplying the inputFields property with field definitions. These field definitions are identical to Visual Editor schema field definitions.

# Supported field types

The following field types are supported:

  • boolean
  • color
  • date
  • datetime
  • enum
  • html
  • markdown
  • number
  • reference
  • slug
  • string
  • text
  • url

# Use input data

The input data is passed to the run function in an inputData object, where the key is the name of the field and the value is the user value. Here's a simple example:

// stackbit.config.ts
export default defineStackbitConfig({
  actions: [
    {
      type: "bulk",
      name: "name_of_action",
      inputFields: [{ name: "prompt", type: "string", required: true }],
      run: async options => {
        const { prompt } = options.inputData;
        // Do something with `prompt` ...
      }
    }
  ]
});

# Handling state

The action trigger can be given a state to provide feedback to the user.

The state can be set in the return object from the run function. Visual Editor will also check for updates to the state using the state property.

# Supported state

The following states are supported:

  • enabled
  • disabled
  • hidden
  • running

# State example

The state function is most useful in long running actions when the state of the action may depend on external factors or maybe the field values of the document itself.

Let's assume the run function calls a translation API that submits a whole document and requires humans to translate the content.

The whole translation process may take several days. We don't expect the run function to run for several days. Instead, after calling the translation API, the run function will return immediately and return the proper state.

const actions = [
  {
    run: async options => {
      // Do something with the translation API ...
      return { state: "running" };
    }
  }
];

This overrides the default Visual Editor behavior, which would change the state back to enabled.

Every time the Studio requests the document with that action, the state function will check with the translation service if that document is still being translated or is finished, and return a matching state.

const actions = [
  {
    state: async options => {
      // Check translation status ...
      return { state: "..." };
    },
    run: async options => {
      // ...
    }
  }
];

# Examples

Here are a few more complete examples to help get started with custom actions.

# Generate a title

This is a model action that uses Faker to generate a random title. This is shared for brevity. A more useful application might send a user prompt to an AI service.

import { PageModel } from "@stackbit/types";

export const Post: PageModel = {
  name: "Post",
  type: "page",
  fields: [{ name: "title" /* ... */ }],
  actions: [
    {
      name: "generate-title",
      label: "Generate Title",
      run: async options => {
        const { faker } = await import("@faker-js/faker");
        const document = options.currentPageDocument;
        if (!document) return;
        // Send feedback in the appropriate context
        const logger = options.getLogger();
        logger.debug(`Running generate-title action on page: ${document.id}`);
        // Generate title
        const newTitle = faker.lorem.words(4);
        logger.debug(`Setting title to: ${newTitle}`);
        // Update the document with the new random title
        options.contentSourceActions.updateDocument({
          document,
          userContext: options.getUserContextForContentSourceType(
            document.srcType
          ),
          operations: [
            {
              opType: "set",
              fieldPath: ["title"],
              modelField: options.model.fields!.find(
                field => field.name === "title"
              ) as FieldString,
              field: { type: "string", value: newTitle }
            }
          ]
        });
        logger.debug("Finished generate-title action");
      }
    }
  ]
};

# Fix formatting on field

In this example, we can force a field into a specific format. (Note that you could more strictly enforce this behavior with document hooks.)

import { PageModel } from "@stackbit/types";

export const Post: PageModel = {
  name: "Post",
  type: "page",
  fields: [
    {
      type: "string",
      name: "title",
      required: true,
      actions: [
        {
          name: "sanitize-title",
          label: "Sanitize Title",
          inputFields: [],
          run: async options => {
            const document = options.currentPageDocument;
            if (!document) return;
            // Send feedback to the appropriate context
            const logger = options.getLogger();
            logger.debug(
              `Running sanitize-title action on page: ${document.id}`
            );
            // Get the current title
            const currentTitleField = document.fields.title;
            if (!currentTitleField || !("value" in currentTitleField)) return;
            // Clean it up
            const sanitizedTitle = currentTitleField.value
              .replace(/\b(\w)/g, s => s.toUpperCase())
              .trim();
            // Store the updated title on the document
            options.contentSourceActions.updateDocument({
              document,
              userContext: options.getUserContextForContentSourceType(
                options.parentDocument.srcType
              ),
              operations: [
                {
                  opType: "set",
                  fieldPath: ["title"],
                  modelField: options.modelField,
                  field: { type: "string", value: sanitizedTitle }
                }
              ]
            });
            logger.debug("Finished sanitize-title action");
          }
        }
      ]
    }
  ]
};

# Status messages

Status messages allow you to customize a success, error, or other type of message to show users in the visual editor.

Custom actions used for status messages use the run method.

If a custom action is run from custom control, then the result property can be used.

The result is returned to the user when the user executes a custom action in custom control using window.stackbit.runCustomAction:

const result = await window.stackbit.runCustomAction({ actionName: 'test' });
console.log(result);

// result is either `result` attribute returned by custom action
// or success/error messages if no `result` provided

# Success status example

To return a success notification, the action’s run method needs to return an object with a success property containing the success message (this is also included in the return type of the run method):

{
  run: async (options) => {
    return {
      success: 'Action complete 🎉'
    }
  }
}

# Error status example

To return an error status, the custom action’s run method needs to return an object with an error property containing a human readable error message.

This message can be returned as a result of running customAction from the custom control.

The error message will be shown in a standard red notification in the visual editor:

run: async (options) => {
  return {
    error: 'oops, something went wrong'
  }
}

The custom action’s run method can also throw an error object. In this case, the error’s message

run: async () => {
  throw new Error('oops, something went wrong');
}

Or, if the run function is asynchronous, it can reject with an error object:

run: async (options) => {
  await new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('oops, something went wrong 😱'))
    }, 3000)
  })
}

If your handler run method throws an error, the message of the error will be returned as an error property with the prepended string Error running action:

async run () {
  throw new Error('oops')